mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
feat(triage): add minimal YAML frontmatter parser/serializer
This commit is contained in:
90
scripts/features/lib/frontmatter.mjs
Normal file
90
scripts/features/lib/frontmatter.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Minimal YAML frontmatter reader/writer for idea files.
|
||||
* Supports: scalars (string/number/bool), nested object (one level), inline arrays [a, b].
|
||||
* Not a general YAML lib — kept simple intentionally.
|
||||
*/
|
||||
|
||||
const DELIM = "---";
|
||||
|
||||
function parseScalar(raw) {
|
||||
const s = raw.trim();
|
||||
if (s === "true") return true;
|
||||
if (s === "false") return false;
|
||||
if (s === "null") return null;
|
||||
if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s);
|
||||
if (s.startsWith("[") && s.endsWith("]")) {
|
||||
const inner = s.slice(1, -1).trim();
|
||||
if (!inner) return [];
|
||||
return inner.split(",").map((x) => parseScalar(x.trim()));
|
||||
}
|
||||
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||
return s.slice(1, -1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function parseFrontmatter(text) {
|
||||
if (typeof text !== "string") return null;
|
||||
if (!text.startsWith(`${DELIM}\n`)) return null;
|
||||
const rest = text.slice(DELIM.length + 1);
|
||||
const closeIdx = rest.indexOf(`\n${DELIM}`);
|
||||
if (closeIdx < 0) return null;
|
||||
const body = rest.slice(0, closeIdx);
|
||||
|
||||
const out = {};
|
||||
let currentNested = null;
|
||||
|
||||
for (const line of body.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
if (line.startsWith(" ") && currentNested) {
|
||||
const m = line.match(/^\s{2}([\w_-]+):\s*(.*)$/);
|
||||
if (m) currentNested[m[1]] = parseScalar(m[2]);
|
||||
continue;
|
||||
}
|
||||
const m = line.match(/^([\w_-]+):\s*(.*)$/);
|
||||
if (!m) continue;
|
||||
const key = m[1];
|
||||
const val = m[2];
|
||||
if (val === "") {
|
||||
currentNested = {};
|
||||
out[key] = currentNested;
|
||||
} else {
|
||||
currentNested = null;
|
||||
out[key] = parseScalar(val);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function serializeScalar(v) {
|
||||
if (v === null) return "null";
|
||||
if (typeof v === "boolean") return String(v);
|
||||
if (typeof v === "number") return String(v);
|
||||
if (Array.isArray(v)) return `[${v.map(serializeScalar).join(", ")}]`;
|
||||
return String(v);
|
||||
}
|
||||
|
||||
export function serializeFrontmatter(meta, body) {
|
||||
const lines = [DELIM];
|
||||
for (const [k, v] of Object.entries(meta)) {
|
||||
if (v && typeof v === "object" && !Array.isArray(v)) {
|
||||
lines.push(`${k}:`);
|
||||
for (const [nk, nv] of Object.entries(v)) {
|
||||
lines.push(` ${nk}: ${serializeScalar(nv)}`);
|
||||
}
|
||||
} else {
|
||||
lines.push(`${k}: ${serializeScalar(v)}`);
|
||||
}
|
||||
}
|
||||
lines.push(DELIM, "", body);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function stripFrontmatter(text) {
|
||||
if (typeof text !== "string") return text;
|
||||
if (!text.startsWith(`${DELIM}\n`)) return text;
|
||||
const rest = text.slice(DELIM.length + 1);
|
||||
const closeIdx = rest.indexOf(`\n${DELIM}`);
|
||||
if (closeIdx < 0) return text;
|
||||
return rest.slice(closeIdx + DELIM.length + 1).replace(/^\n+/, "");
|
||||
}
|
||||
79
tests/unit/feature-triage/frontmatter.test.mjs
Normal file
79
tests/unit/feature-triage/frontmatter.test.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
parseFrontmatter,
|
||||
serializeFrontmatter,
|
||||
stripFrontmatter,
|
||||
} from "../../../scripts/features/lib/frontmatter.mjs";
|
||||
|
||||
describe("parseFrontmatter", () => {
|
||||
it("returns null when no frontmatter", () => {
|
||||
assert.equal(parseFrontmatter("# Heading\nbody"), null);
|
||||
});
|
||||
|
||||
it("parses simple key-value frontmatter", () => {
|
||||
const md = `---
|
||||
issue: 1046
|
||||
last_synced_at: 2026-05-18T14:30:00Z
|
||||
last_synced_comment_id: 1234567890
|
||||
---
|
||||
|
||||
# Body`;
|
||||
const r = parseFrontmatter(md);
|
||||
assert.equal(r.issue, 1046);
|
||||
assert.equal(r.last_synced_at, "2026-05-18T14:30:00Z");
|
||||
assert.equal(r.last_synced_comment_id, 1234567890);
|
||||
});
|
||||
|
||||
it("parses nested snapshot block", () => {
|
||||
const md = `---
|
||||
issue: 1046
|
||||
snapshot:
|
||||
thumbs: 8
|
||||
commenters: 4
|
||||
labels: [enhancement, ui]
|
||||
state: open
|
||||
---
|
||||
|
||||
body`;
|
||||
const r = parseFrontmatter(md);
|
||||
assert.equal(r.snapshot.thumbs, 8);
|
||||
assert.equal(r.snapshot.commenters, 4);
|
||||
assert.deepEqual(r.snapshot.labels, ["enhancement", "ui"]);
|
||||
assert.equal(r.snapshot.state, "open");
|
||||
});
|
||||
|
||||
it("returns null on malformed delimiters", () => {
|
||||
assert.equal(parseFrontmatter("---\nfoo: bar\n# no close"), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serializeFrontmatter", () => {
|
||||
it("round-trips a parsed object", () => {
|
||||
const original = `---
|
||||
issue: 1046
|
||||
last_synced_comment_id: 1234567890
|
||||
snapshot:
|
||||
thumbs: 8
|
||||
commenters: 4
|
||||
state: open
|
||||
---
|
||||
|
||||
# Body
|
||||
content here`;
|
||||
const meta = parseFrontmatter(original);
|
||||
const re = serializeFrontmatter(meta, "# Body\ncontent here");
|
||||
const parsed2 = parseFrontmatter(re);
|
||||
assert.deepEqual(parsed2, meta);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripFrontmatter", () => {
|
||||
it("returns body after frontmatter", () => {
|
||||
const md = "---\nissue: 1\n---\n\n# Body";
|
||||
assert.equal(stripFrontmatter(md), "# Body");
|
||||
});
|
||||
it("returns full text if no frontmatter", () => {
|
||||
assert.equal(stripFrontmatter("# Body"), "# Body");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user