feat(voyage)!: marketplace handoff — rename plugins/ultraplan-local to plugins/voyage [skip-docs]
Session 5 of voyage-rebrand (V6). Operator-authorized cross-plugin scope. - git mv plugins/ultraplan-local plugins/voyage (rename detected, history preserved) - .claude-plugin/marketplace.json: voyage entry replaces ultraplan-local - CLAUDE.md: voyage row in plugin list, voyage in design-system consumer list - README.md: bulk rename ultra*-local commands -> trek* commands; ultraplan-local refs -> voyage; type discriminators (type: trekbrief/trekreview); session-title pattern (voyage:<command>:<slug>); v4.0.0 release-note paragraph - plugins/voyage/.claude-plugin/plugin.json: homepage/repository URLs point to monorepo voyage path - plugins/voyage/verify.sh: drop URL whitelist exception (no longer needed) Closes voyage-rebrand. bash plugins/voyage/verify.sh PASS 7/7. npm test 361/361.
This commit is contained in:
parent
8f1bf9b7b4
commit
7a90d348ad
149 changed files with 26 additions and 33 deletions
|
|
@ -1,158 +0,0 @@
|
|||
// lib/util/frontmatter.mjs
|
||||
// Hand-rolled YAML-frontmatter parser.
|
||||
//
|
||||
// Supported subset:
|
||||
// - String scalars (quoted or unquoted)
|
||||
// - Numbers (integer + float)
|
||||
// - Booleans (true / false)
|
||||
// - null
|
||||
// - Single-level dicts
|
||||
// - Lists of scalars (- value)
|
||||
//
|
||||
// Deliberately rejects: nested dicts in lists, multi-line strings,
|
||||
// anchors/aliases, tags, flow style ({...} / [...]).
|
||||
//
|
||||
// Why no js-yaml: zero-deps invariant. Templates emit only this subset.
|
||||
|
||||
import { issue, ok, fail } from './result.mjs';
|
||||
|
||||
const FRONTMATTER_RE = /^?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/;
|
||||
|
||||
/**
|
||||
* Split raw markdown into { frontmatter, body }.
|
||||
* Returns { hasFrontmatter: false } when no leading --- block exists.
|
||||
*/
|
||||
export function splitFrontmatter(text) {
|
||||
if (typeof text !== 'string') return { hasFrontmatter: false, body: '' };
|
||||
const stripped = text.replace(/^/, '');
|
||||
const m = stripped.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
|
||||
if (!m) return { hasFrontmatter: false, body: stripped };
|
||||
return {
|
||||
hasFrontmatter: true,
|
||||
frontmatter: m[1],
|
||||
body: m[2] || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a YAML-frontmatter string into a JS object.
|
||||
* @returns {import('./result.mjs').Result}
|
||||
*/
|
||||
export function parseFrontmatter(yamlText) {
|
||||
if (typeof yamlText !== 'string') {
|
||||
return fail(issue('FM_INPUT', 'Frontmatter input is not a string'));
|
||||
}
|
||||
const lines = yamlText.split(/\r?\n/);
|
||||
const out = {};
|
||||
const errors = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.trim() === '' || line.trimStart().startsWith('#')) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const indentMatch = line.match(/^(\s*)/);
|
||||
const indent = indentMatch ? indentMatch[0].length : 0;
|
||||
if (indent > 0) {
|
||||
errors.push(issue('FM_INDENT', `Unexpected indentation at line ${i + 1}`, 'Top-level keys only; nested dicts unsupported.'));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
||||
if (!kv) {
|
||||
errors.push(issue('FM_SYNTAX', `Cannot parse line ${i + 1}: ${line}`));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = kv[1];
|
||||
const rest = kv[2];
|
||||
|
||||
if (rest === '' || rest === undefined) {
|
||||
const list = [];
|
||||
let j = i + 1;
|
||||
while (j < lines.length) {
|
||||
const next = lines[j];
|
||||
if (next.trim() === '') { j++; continue; }
|
||||
const itemMatch = next.match(/^(\s+)-\s+(.*)$/);
|
||||
if (!itemMatch) break;
|
||||
const itemIndent = itemMatch[1].length;
|
||||
const firstContent = itemMatch[2];
|
||||
const dictKeyMatch = firstContent.match(/^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
||||
if (dictKeyMatch) {
|
||||
const item = {};
|
||||
item[dictKeyMatch[1]] = parseScalar(dictKeyMatch[2]);
|
||||
let k = j + 1;
|
||||
while (k < lines.length) {
|
||||
const cont = lines[k];
|
||||
if (cont.trim() === '') { k++; continue; }
|
||||
const contMatch = cont.match(/^(\s+)([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
|
||||
if (!contMatch) break;
|
||||
if (contMatch[1].length <= itemIndent + 1) break;
|
||||
item[contMatch[2]] = parseScalar(contMatch[3]);
|
||||
k++;
|
||||
}
|
||||
list.push(item);
|
||||
j = k;
|
||||
} else {
|
||||
list.push(parseScalar(firstContent));
|
||||
j++;
|
||||
}
|
||||
}
|
||||
if (list.length > 0) {
|
||||
out[key] = list;
|
||||
i = j;
|
||||
} else {
|
||||
out[key] = null;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
out[key] = parseScalar(rest);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (errors.length > 0) return { valid: false, errors, warnings: [], parsed: out };
|
||||
return ok(out);
|
||||
}
|
||||
|
||||
function parseScalar(raw) {
|
||||
const s = raw.trim();
|
||||
if (s === '') return '';
|
||||
if (s === 'null' || s === '~') return null;
|
||||
if (s === 'true') return true;
|
||||
if (s === 'false') return false;
|
||||
if (s === '[]') return [];
|
||||
if (s === '{}') return {};
|
||||
if (/^-?\d+$/.test(s)) return Number.parseInt(s, 10);
|
||||
if (/^-?\d+\.\d+$/.test(s)) return Number.parseFloat(s);
|
||||
if (s.startsWith('"') && s.endsWith('"')) {
|
||||
return s.slice(1, -1).replace(/\\(.)/g, (_, ch) => {
|
||||
if (ch === 'n') return '\n';
|
||||
if (ch === 't') return '\t';
|
||||
if (ch === 'r') return '\r';
|
||||
return ch;
|
||||
});
|
||||
}
|
||||
if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1);
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a markdown file's frontmatter directly from its full text.
|
||||
* @returns {import('./result.mjs').Result}
|
||||
*/
|
||||
export function parseDocument(text) {
|
||||
const split = splitFrontmatter(text);
|
||||
if (!split.hasFrontmatter) {
|
||||
return fail(issue('FM_MISSING', 'No frontmatter block found'));
|
||||
}
|
||||
const result = parseFrontmatter(split.frontmatter);
|
||||
return { ...result, parsed: { frontmatter: result.parsed, body: split.body } };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue