feat(launcher): add frontmatter.lua JSON-frontmatter parser

Parses manifest.launcher.md leading JSON-frontmatter (delimited by
standalone '---' lines), returns (meta, body). Reuses
lib-management.dep-fetcher._json_decode so we do not add a second JSON
decoder to the codebase. Lazy require so test-lib contexts can swap a
stub decoder via _G.LAUNCHER_FRONTMATTER_DECODER.
This commit is contained in:
Calic
2026-06-11 01:21:27 +02:00
parent 8b833bbb35
commit 942e05ba3e

63
frontmatter.lua Normal file
View File

@@ -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