open-design/apps/daemon/frontmatter.ts

137 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
// Minimal YAML front-matter parser. Handles the subset used by SKILL.md in
// our examples: scalar strings/numbers/booleans, block-literal (|) strings,
// and flat arrays ("- foo"). Keeps the daemon dep-free. If you need real
// YAML (nested objects, flow-style, anchors), swap for `yaml` or `js-yaml`.
export function parseFrontmatter(src) {
const text = src.replace(/^/, '');
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
if (!match) return { data: {}, body: text };
const [, yaml, body] = match;
return { data: parseYamlSubset(yaml), body };
}
function parseYamlSubset(src) {
const lines = src.split(/\r?\n/);
const root = {};
const stack = [{ indent: -1, container: root, key: null }];
let i = 0;
while (i < lines.length) {
const raw = lines[i];
if (/^\s*(#.*)?$/.test(raw)) {
i++;
continue;
}
const indent = raw.match(/^\s*/)[0].length;
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
stack.pop();
}
const top = stack[stack.length - 1];
const line = raw.slice(indent);
// Array item
if (line.startsWith('- ')) {
const value = line.slice(2).trim();
let container = top.container;
if (!Array.isArray(container)) {
// Convert the pending key's value to an array on first `-`.
const parent = stack[stack.length - 2];
if (parent && top.key) {
parent.container[top.key] = [];
container = parent.container[top.key];
top.container = container;
} else {
i++;
continue;
}
}
if (value.includes(':')) {
const obj = {};
const colonIdx = value.indexOf(':');
const key = value.slice(0, colonIdx).trim();
const valRaw = value.slice(colonIdx + 1).trim();
if (valRaw) obj[key] = coerce(valRaw);
container.push(obj);
stack.push({ indent, container: obj, key: null });
} else {
container.push(coerce(value));
}
i++;
continue;
}
// key: value or key: |
const kv = /^([^:]+):\s*(.*)$/.exec(line);
if (!kv) {
i++;
continue;
}
const key = kv[1].trim();
const val = kv[2];
if (val === '' || val === undefined) {
top.container[key] = {};
stack.push({ indent, container: top.container[key], key });
i++;
continue;
}
if (val === '|' || val === '|-' || val === '>' || val === '>-') {
const collected = [];
const childIndent = indent + 2;
i++;
while (i < lines.length) {
const next = lines[i];
if (/^\s*$/.test(next)) {
collected.push('');
i++;
continue;
}
const nIndent = next.match(/^\s*/)[0].length;
if (nIndent < childIndent) break;
collected.push(next.slice(childIndent));
i++;
}
top.container[key] = collected.join('\n').trimEnd();
continue;
}
if (val === '[]') {
top.container[key] = [];
i++;
continue;
}
if (val.startsWith('[') && val.endsWith(']')) {
top.container[key] = val
.slice(1, -1)
.split(',')
.map((s) => coerce(s.trim()))
.filter((v) => v !== '');
i++;
continue;
}
top.container[key] = coerce(val);
i++;
}
return root;
}
function coerce(raw) {
if (raw === undefined) return '';
let v = raw.trim();
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
return v.slice(1, -1);
}
if (v === 'true') return true;
if (v === 'false') return false;
if (v === 'null' || v === '~') return null;
if (/^-?\d+$/.test(v)) return Number(v);
if (/^-?\d*\.\d+$/.test(v)) return Number(v);
return v;
}