mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
137 lines
3.7 KiB
TypeScript
137 lines
3.7 KiB
TypeScript
// @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;
|
||
}
|