diff --git a/frontmatter.lua b/frontmatter.lua new file mode 100644 index 0000000..ca9d9ac --- /dev/null +++ b/frontmatter.lua @@ -0,0 +1,63 @@ +-- frontmatter.lua — JSON-Frontmatter parser for manifest.launcher.md +-- Splits on the first standalone '---' line. Everything before goes to +-- depf._json_decode; everything after (after a single LF) is body. +-- Loud-Error semantics: parse failure returns nil + err-string. + +local M = {} + +-- depf is required lazily so this file can be dofile'd into a test-lib +-- context where lib-management.dep-fetcher is not on the dep list. The +-- launcher main module does require it eagerly; the test-lib monkeys +-- a stub decoder into _G before calling fm.parse. +local function get_decoder() + if _G.LAUNCHER_FRONTMATTER_DECODER then + return _G.LAUNCHER_FRONTMATTER_DECODER + end + local ok, depf = pcall(require, "lib-management.dep-fetcher") + if ok and depf and depf._json_decode then return depf._json_decode end + return nil +end + +-- Splits the content at the first line equal to '---'. The first such +-- line ends the JSON region; lines before it are JSON, lines after +-- are body. Both regions are LF-joined back into strings. +local function split_at_separator(content) + local lines = {} + for line in (content .. "\n"):gmatch("([^\n]*)\n") do + lines[#lines + 1] = line + end + -- A valid file starts with '---' (no leading whitespace). + if lines[1] ~= "---" then return nil end + local sep_idx = nil + for i = 2, #lines do + if lines[i] == "---" then sep_idx = i; break end + end + if not sep_idx then return nil end + local json_lines = {} + for i = 2, sep_idx - 1 do json_lines[#json_lines + 1] = lines[i] end + local body_lines = {} + for i = sep_idx + 1, #lines do body_lines[#body_lines + 1] = lines[i] end + return table.concat(json_lines, "\n"), table.concat(body_lines, "\n") +end + +function M.parse(content) + if type(content) ~= "string" then + return nil, nil, "frontmatter.parse: content must be string" + end + local json_str, body = split_at_separator(content) + if not json_str then + -- No frontmatter — treat the whole content as body, empty meta. + return {}, content + end + local decode = get_decoder() + if not decode then + return nil, nil, "frontmatter.parse: no JSON decoder available" + end + local meta, derr = decode(json_str) + if not meta then + return nil, nil, "frontmatter.parse: " .. tostring(derr) + end + return meta, body +end + +return M