mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Replace the Swift site generator with a Ruby and Phlex implementation. Loads site and projects from TOML, derive site metadata from posts. Migrate from make to bake and add standardrb and code coverage tasks. Update CI and docs to match the new workflow, and remove unused assets/dependencies plus obsolete tooling.
217 lines
7 KiB
Ruby
217 lines
7 KiB
Ruby
require "pressa/site"
|
|
require "pressa/posts/plugin"
|
|
require "pressa/projects/plugin"
|
|
require "pressa/utils/markdown_renderer"
|
|
require "pressa/config/simple_toml"
|
|
|
|
module Pressa
|
|
module Config
|
|
class ValidationError < StandardError; end
|
|
|
|
class Loader
|
|
REQUIRED_SITE_KEYS = %w[author email title description url].freeze
|
|
REQUIRED_PROJECT_KEYS = %w[name title description url].freeze
|
|
|
|
def initialize(source_path:)
|
|
@source_path = source_path
|
|
end
|
|
|
|
def build_site(url_override: nil)
|
|
site_config = load_toml("site.toml")
|
|
|
|
validate_required!(site_config, REQUIRED_SITE_KEYS, context: "site.toml")
|
|
|
|
site_url = url_override || site_config["url"]
|
|
plugins = build_plugins(site_config)
|
|
|
|
Site.new(
|
|
author: site_config["author"],
|
|
email: site_config["email"],
|
|
title: site_config["title"],
|
|
description: site_config["description"],
|
|
url: site_url,
|
|
image_url: normalize_image_url(site_config["image_url"], site_url),
|
|
scripts: build_scripts(site_config["scripts"], context: "site.toml scripts"),
|
|
styles: build_styles(site_config["styles"], context: "site.toml styles"),
|
|
plugins:,
|
|
renderers: [
|
|
Utils::MarkdownRenderer.new
|
|
]
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
def load_toml(filename)
|
|
path = File.join(@source_path, filename)
|
|
SimpleToml.load_file(path)
|
|
rescue ParseError => e
|
|
raise ValidationError, e.message
|
|
end
|
|
|
|
def build_projects(projects_config)
|
|
projects = projects_config["projects"]
|
|
raise ValidationError, "Missing required top-level array 'projects' in projects.toml" unless projects
|
|
raise ValidationError, "Expected 'projects' to be an array in projects.toml" unless projects.is_a?(Array)
|
|
|
|
projects.map.with_index do |project, index|
|
|
unless project.is_a?(Hash)
|
|
raise ValidationError, "Project entry #{index + 1} must be a table in projects.toml"
|
|
end
|
|
|
|
validate_required!(project, REQUIRED_PROJECT_KEYS, context: "projects.toml project ##{index + 1}")
|
|
|
|
Projects::Project.new(
|
|
name: project["name"],
|
|
title: project["title"],
|
|
description: project["description"],
|
|
url: project["url"]
|
|
)
|
|
end
|
|
end
|
|
|
|
def validate_required!(hash, keys, context:)
|
|
missing = keys.reject do |key|
|
|
hash[key].is_a?(String) && !hash[key].strip.empty?
|
|
end
|
|
|
|
return if missing.empty?
|
|
|
|
raise ValidationError, "Missing required #{context} keys: #{missing.join(", ")}"
|
|
end
|
|
|
|
def build_plugins(site_config)
|
|
plugin_names = parse_plugin_names(site_config["plugins"])
|
|
|
|
plugin_names.map.with_index do |plugin_name, index|
|
|
case plugin_name
|
|
when "posts"
|
|
Posts::Plugin.new
|
|
when "projects"
|
|
build_projects_plugin(site_config)
|
|
else
|
|
raise ValidationError, "Unknown plugin '#{plugin_name}' at site.toml plugins[#{index}]"
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_plugin_names(value)
|
|
return [] if value.nil?
|
|
raise ValidationError, "Expected site.toml plugins to be an array" unless value.is_a?(Array)
|
|
|
|
seen = {}
|
|
|
|
value.map.with_index do |plugin_name, index|
|
|
unless plugin_name.is_a?(String) && !plugin_name.strip.empty?
|
|
raise ValidationError, "Expected site.toml plugins[#{index}] to be a non-empty String"
|
|
end
|
|
|
|
normalized_plugin_name = plugin_name.strip
|
|
if seen[normalized_plugin_name]
|
|
raise ValidationError, "Duplicate plugin '#{normalized_plugin_name}' in site.toml plugins"
|
|
end
|
|
seen[normalized_plugin_name] = true
|
|
|
|
normalized_plugin_name
|
|
end
|
|
end
|
|
|
|
def build_projects_plugin(site_config)
|
|
projects_plugin = hash_or_empty(site_config["projects_plugin"], "site.toml projects_plugin")
|
|
projects_config = load_toml("projects.toml")
|
|
projects = build_projects(projects_config)
|
|
|
|
Projects::Plugin.new(
|
|
projects:,
|
|
scripts: build_scripts(projects_plugin["scripts"], context: "site.toml projects_plugin.scripts"),
|
|
styles: build_styles(projects_plugin["styles"], context: "site.toml projects_plugin.styles")
|
|
)
|
|
end
|
|
|
|
def hash_or_empty(value, context)
|
|
return {} if value.nil?
|
|
return value if value.is_a?(Hash)
|
|
|
|
raise ValidationError, "Expected #{context} to be a table"
|
|
end
|
|
|
|
def build_scripts(value, context:)
|
|
entries = array_or_empty(value, context)
|
|
|
|
entries.map.with_index do |item, index|
|
|
case item
|
|
when String
|
|
validate_asset_path!(
|
|
item,
|
|
context: "#{context}[#{index}]"
|
|
)
|
|
Script.new(src: item, defer: true)
|
|
when Hash
|
|
src = item["src"]
|
|
raise ValidationError, "Expected #{context}[#{index}].src to be a String" unless src.is_a?(String) && !src.empty?
|
|
validate_asset_path!(
|
|
src,
|
|
context: "#{context}[#{index}].src"
|
|
)
|
|
|
|
defer = item.key?("defer") ? item["defer"] : true
|
|
unless [true, false].include?(defer)
|
|
raise ValidationError, "Expected #{context}[#{index}].defer to be a Boolean"
|
|
end
|
|
|
|
Script.new(src:, defer:)
|
|
else
|
|
raise ValidationError, "Expected #{context}[#{index}] to be a String or table"
|
|
end
|
|
end
|
|
end
|
|
|
|
def build_styles(value, context:)
|
|
entries = array_or_empty(value, context)
|
|
|
|
entries.map.with_index do |item, index|
|
|
case item
|
|
when String
|
|
validate_asset_path!(
|
|
item,
|
|
context: "#{context}[#{index}]"
|
|
)
|
|
Stylesheet.new(href: item)
|
|
when Hash
|
|
href = item["href"]
|
|
raise ValidationError, "Expected #{context}[#{index}].href to be a String" unless href.is_a?(String) && !href.empty?
|
|
validate_asset_path!(
|
|
href,
|
|
context: "#{context}[#{index}].href"
|
|
)
|
|
|
|
Stylesheet.new(href:)
|
|
else
|
|
raise ValidationError, "Expected #{context}[#{index}] to be a String or table"
|
|
end
|
|
end
|
|
end
|
|
|
|
def array_or_empty(value, context)
|
|
return [] if value.nil?
|
|
return value if value.is_a?(Array)
|
|
|
|
raise ValidationError, "Expected #{context} to be an array"
|
|
end
|
|
|
|
def normalize_image_url(value, site_url)
|
|
return nil if value.nil?
|
|
return value if value.start_with?("http://", "https://")
|
|
|
|
normalized = value.start_with?("/") ? value : "/#{value}"
|
|
"#{site_url}#{normalized}"
|
|
end
|
|
|
|
def validate_asset_path!(value, context:)
|
|
return if value.start_with?("/", "http://", "https://")
|
|
|
|
raise ValidationError, "Expected #{context} to start with / or use http(s) scheme"
|
|
end
|
|
end
|
|
end
|
|
end
|