mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-04-04 10:35:46 +00:00
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
|