require_relative '../site' require_relative '../posts/plugin' require_relative '../projects/plugin' require_relative '../utils/markdown_renderer' require_relative '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') projects_config = load_toml('projects.toml') validate_required!(site_config, REQUIRED_SITE_KEYS, context: 'site.toml') site_url = url_override || site_config['url'] projects_plugin = hash_or_empty(site_config['projects_plugin'], 'site.toml projects_plugin') projects = build_projects(projects_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: [ Posts::Plugin.new, 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') ) ], 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 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 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? 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 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? 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 end end end