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.
224 lines
5.5 KiB
Ruby
224 lines
5.5 KiB
Ruby
require "json"
|
|
|
|
module Pressa
|
|
module Config
|
|
class ParseError < StandardError; end
|
|
|
|
class SimpleToml
|
|
def self.load_file(path)
|
|
new.parse(File.read(path))
|
|
rescue Errno::ENOENT
|
|
raise ParseError, "Config file not found: #{path}"
|
|
end
|
|
|
|
def parse(content)
|
|
root = {}
|
|
current_table = root
|
|
lines = content.each_line.to_a
|
|
|
|
line_index = 0
|
|
while line_index < lines.length
|
|
line = lines[line_index]
|
|
line_number = line_index + 1
|
|
source = strip_comments(line).strip
|
|
if source.empty?
|
|
line_index += 1
|
|
next
|
|
end
|
|
|
|
if source =~ /\A\[\[(.+)\]\]\z/
|
|
current_table = start_array_table(root, Regexp.last_match(1), line_number)
|
|
line_index += 1
|
|
next
|
|
end
|
|
|
|
if source =~ /\A\[(.+)\]\z/
|
|
current_table = start_table(root, Regexp.last_match(1), line_number)
|
|
line_index += 1
|
|
next
|
|
end
|
|
|
|
key, raw_value = parse_assignment(source, line_number)
|
|
while needs_continuation?(raw_value)
|
|
line_index += 1
|
|
raise ParseError, "Unterminated value for key '#{key}' at line #{line_number}" if line_index >= lines.length
|
|
|
|
continuation = strip_comments(lines[line_index]).strip
|
|
next if continuation.empty?
|
|
|
|
raw_value = "#{raw_value} #{continuation}"
|
|
end
|
|
|
|
if current_table.key?(key)
|
|
raise ParseError, "Duplicate key '#{key}' at line #{line_number}"
|
|
end
|
|
|
|
current_table[key] = parse_value(raw_value, line_number)
|
|
line_index += 1
|
|
end
|
|
|
|
root
|
|
end
|
|
|
|
private
|
|
|
|
def start_array_table(root, raw_path, line_number)
|
|
keys = parse_path(raw_path, line_number)
|
|
parent = ensure_path(root, keys[0..-2], line_number)
|
|
table_name = keys.last
|
|
|
|
parent[table_name] ||= []
|
|
array = parent[table_name]
|
|
unless array.is_a?(Array)
|
|
raise ParseError, "Expected array for '[[#{raw_path}]]' at line #{line_number}"
|
|
end
|
|
|
|
table = {}
|
|
array << table
|
|
table
|
|
end
|
|
|
|
def start_table(root, raw_path, line_number)
|
|
keys = parse_path(raw_path, line_number)
|
|
ensure_path(root, keys, line_number)
|
|
end
|
|
|
|
def ensure_path(root, keys, line_number)
|
|
cursor = root
|
|
|
|
keys.each do |key|
|
|
cursor[key] ||= {}
|
|
unless cursor[key].is_a?(Hash)
|
|
raise ParseError, "Expected table path '#{keys.join(".")}' at line #{line_number}"
|
|
end
|
|
|
|
cursor = cursor[key]
|
|
end
|
|
|
|
cursor
|
|
end
|
|
|
|
def parse_path(raw_path, line_number)
|
|
keys = raw_path.split(".").map(&:strip)
|
|
if keys.empty? || keys.any? { |part| part.empty? || part !~ /\A[A-Za-z0-9_]+\z/ }
|
|
raise ParseError, "Invalid table path '#{raw_path}' at line #{line_number}"
|
|
end
|
|
keys
|
|
end
|
|
|
|
def parse_assignment(source, line_number)
|
|
separator = index_of_unquoted(source, "=")
|
|
raise ParseError, "Invalid assignment at line #{line_number}" unless separator
|
|
|
|
key = source[0...separator].strip
|
|
value = source[(separator + 1)..].strip
|
|
|
|
if key.empty? || key !~ /\A[A-Za-z0-9_]+\z/
|
|
raise ParseError, "Invalid key '#{key}' at line #{line_number}"
|
|
end
|
|
raise ParseError, "Missing value for key '#{key}' at line #{line_number}" if value.empty?
|
|
|
|
[key, value]
|
|
end
|
|
|
|
def parse_value(raw_value, line_number)
|
|
JSON.parse(raw_value)
|
|
rescue JSON::ParserError
|
|
raise ParseError, "Unsupported TOML value '#{raw_value}' at line #{line_number}"
|
|
end
|
|
|
|
def needs_continuation?(source)
|
|
in_string = false
|
|
escaped = false
|
|
depth = 0
|
|
|
|
source.each_char do |char|
|
|
if in_string
|
|
if escaped
|
|
escaped = false
|
|
elsif char == "\\"
|
|
escaped = true
|
|
elsif char == '"'
|
|
in_string = false
|
|
end
|
|
|
|
next
|
|
end
|
|
|
|
case char
|
|
when '"'
|
|
in_string = true
|
|
when "[", "{"
|
|
depth += 1
|
|
when "]", "}"
|
|
depth -= 1
|
|
end
|
|
end
|
|
|
|
in_string || depth.positive?
|
|
end
|
|
|
|
def strip_comments(line)
|
|
output = +""
|
|
in_string = false
|
|
escaped = false
|
|
|
|
line.each_char do |char|
|
|
if in_string
|
|
output << char
|
|
|
|
if escaped
|
|
escaped = false
|
|
elsif char == "\\"
|
|
escaped = true
|
|
elsif char == '"'
|
|
in_string = false
|
|
end
|
|
|
|
next
|
|
end
|
|
|
|
case char
|
|
when '"'
|
|
in_string = true
|
|
output << char
|
|
when "#"
|
|
break
|
|
else
|
|
output << char
|
|
end
|
|
end
|
|
|
|
output
|
|
end
|
|
|
|
def index_of_unquoted(source, target)
|
|
in_string = false
|
|
escaped = false
|
|
|
|
source.each_char.with_index do |char, index|
|
|
if in_string
|
|
if escaped
|
|
escaped = false
|
|
elsif char == "\\"
|
|
escaped = true
|
|
elsif char == '"'
|
|
in_string = false
|
|
end
|
|
|
|
next
|
|
end
|
|
|
|
if char == '"'
|
|
in_string = true
|
|
next
|
|
end
|
|
|
|
return index if char == target
|
|
end
|
|
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
end
|