feat(bgm): support role= on code block info strings
Parse role.type#id from the fence info string and merge it into each parsed object, erroring on conflicts with the content. id on the info string can't combine with $variants. Auto-named blocks now hash their content instead of the info string, so identical blocks dedupe.
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import { posix } from 'node:path';
|
||||
import { marked } from 'marked';
|
||||
import { BgmError, type DefFile } from './types.js';
|
||||
import { BgmError, ROLES, type DefFile, type Role, type RoleMeta } from './types.js';
|
||||
|
||||
/** The languages that count as definition files; others are ignored. */
|
||||
const DEF_LANGS = new Set(['yaml', 'yml', 'json', 'toml']);
|
||||
@@ -60,13 +60,14 @@ export function scanMarkdown(text: string, sourcePath: string): MarkdownResult {
|
||||
|
||||
fences.push({ startLine, endLine, info, content: token.text });
|
||||
|
||||
const name = parseInfo(info);
|
||||
const name = parseInfo(info, token.text);
|
||||
if (name) {
|
||||
files.push({
|
||||
name: posix.join(posix.dirname(sourcePath), name),
|
||||
text: token.text,
|
||||
source: `${sourcePath}:${startLine}-${endLine}`,
|
||||
kind: kindOf(name),
|
||||
role: parseRole(info, `${sourcePath}:${startLine}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -78,13 +79,35 @@ export function scanMarkdown(text: string, sourcePath: string): MarkdownResult {
|
||||
* Parse a fence's info string for a `file=` segment and derive the virtual
|
||||
* file name. Blocks without `file=` are auto-named from their content hash.
|
||||
*/
|
||||
function parseInfo(info: string): string | null {
|
||||
function parseInfo(info: string, content: string): string | null {
|
||||
const fileMatch = /file=(\S+)/.exec(info);
|
||||
if (fileMatch) return fileMatch[1]!;
|
||||
|
||||
const lang = info.split(/\s+/)[0];
|
||||
if (!lang || !DEF_LANGS.has(lang)) return null;
|
||||
return `./${hash(info)}.yaml`;
|
||||
return `./${contentHash(content)}.yaml`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a fence's info string for a `role=` segment into role metadata.
|
||||
* `role=part.cargo` -> `{ role: 'part', type: 'cargo' }`;
|
||||
* `role=surface.game#main` -> `{ role: 'surface', type: 'game', id: 'main' }`;
|
||||
* `role=package` -> `{ role: 'package' }`. Returns `undefined` when absent.
|
||||
*/
|
||||
function parseRole(info: string, source: string): RoleMeta | undefined {
|
||||
const match = /role=(\S+)/.exec(info);
|
||||
if (!match) return undefined;
|
||||
const spec = match[1]!;
|
||||
const [role, rest] = spec.split('.');
|
||||
if (!role || !ROLES.has(role as Role)) {
|
||||
throw new BgmError(`Invalid role "${spec}"`, source);
|
||||
}
|
||||
if (role === 'package') {
|
||||
if (rest) throw new BgmError(`Package role takes no type or id`, source);
|
||||
return { role: 'package' };
|
||||
}
|
||||
const [type, id] = (rest ?? '').split('#');
|
||||
return { role: role as Role, type: type || undefined, id: id || undefined };
|
||||
}
|
||||
|
||||
/** Derive the def file type from its name's extension. */
|
||||
@@ -96,9 +119,9 @@ function kindOf(name: string): DefFile['kind'] {
|
||||
return 'yaml';
|
||||
}
|
||||
|
||||
/** A stable content hash for auto-named blocks. */
|
||||
function hash(text: string): string {
|
||||
return crypto.createHash('sha1').update(text).digest('hex').slice(0, 8);
|
||||
/** Hash the block's content, so identical blocks dedupe to the same name. */
|
||||
function contentHash(content: string): string {
|
||||
return crypto.createHash('sha1').update(content).digest('hex').slice(0, 8);
|
||||
}
|
||||
|
||||
/** The 1-based line number where `raw` starts within `text`. */
|
||||
|
||||
Reference in New Issue
Block a user