-- 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 -- Drop the trailing empty entry produced by the final LF appended above, -- so the body matches user expectation ('body text' rather than 'body text\n'). if #body_lines > 0 and body_lines[#body_lines] == "" then body_lines[#body_lines] = nil 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