diff options
Diffstat (limited to 'subagents/frontmatter.lua')
| -rw-r--r-- | subagents/frontmatter.lua | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/subagents/frontmatter.lua b/subagents/frontmatter.lua new file mode 100644 index 0000000..94fe2ac --- /dev/null +++ b/subagents/frontmatter.lua @@ -0,0 +1,96 @@ +-- Split a Markdown profile into its YAML frontmatter and its body. +-- +-- The format is the common one other agent harnesses use: if the very first +-- line of the file is exactly `---`, everything up to the next line that is +-- exactly `---` is a YAML mapping, and everything after that closing fence is +-- the body. Trailing carriage returns are tolerated so CRLF files parse. +-- +-- Anything unusual degrades to "no metadata, body only" with a warning rather +-- than an error, because the body is the part the user cannot afford to lose: +-- +-- * no opening fence -> the whole file is the body, no warning +-- * unterminated opening fence-> the whole file is the body, no warning +-- (a lone `---` at the top of a prose file is a horizontal rule, not a +-- broken header, so this case is deliberately silent) +-- * empty fenced block -> empty mapping, no warning +-- * lyaml missing or erroring -> body after the fence, warning returned +-- * YAML document not a map -> body after the fence, warning returned +-- +-- In the warning cases the fenced block is dropped rather than folded back +-- into the body: an unparseable header is noise the child agent should not be +-- asked to read. The body itself is never rewritten — no trimming, no +-- normalisation — so a prompt round-trips verbatim. + +local M = {} + +local function trim(s) + return (s:gsub("^%s+", ""):gsub("%s+$", "")) +end + +-- Iterate lines, yielding the line plus its start offset and the offset just +-- past its newline, so the caller can slice the original text exactly. +local function lines(text) + local pos = 1 + return function() + if pos > #text then + return nil + end + local start = pos + local nl = text:find("\n", pos, true) + local line + if nl then + line = text:sub(start, nl - 1) + pos = nl + 1 + else + line = text:sub(start) + pos = #text + 1 + end + return line, start, pos + end +end + +-- parse(text) -> data|nil, body, warning|nil +function M.parse(text) + if type(text) ~= "string" or text == "" then + return nil, "", nil + end + + local next_line = lines(text) + local first, _, after_first = next_line() + if first == nil or trim(first) ~= "---" then + return nil, text, nil + end + + local block_stop, body_start + for line, start, after in next_line do + if trim(line) == "---" then + block_stop = start - 1 + body_start = after + break + end + end + if body_start == nil then + return nil, text, nil + end + + local block = text:sub(after_first, block_stop) + local body = text:sub(body_start) + if trim(block) == "" then + return {}, body, nil + end + + local ok_lyaml, lyaml = pcall(require, "lyaml") + if not ok_lyaml then + return nil, body, "lyaml is not installed; ignoring the YAML frontmatter" + end + local ok, data = pcall(lyaml.load, block) + if not ok then + return nil, body, "YAML frontmatter did not parse: " .. tostring(data) + end + if type(data) ~= "table" then + return nil, body, "YAML frontmatter is not a mapping; ignoring it" + end + return data, body, nil +end + +return M |
