feat(bgm): add board game manifest loader

Parse yaml/json/toml and markdown code blocks into packages, expanding
$variants via typed-csv and collecting parts, surfaces, and setups by
include patterns. Ships zod validation, a vitest config, and 16 tests.
This commit is contained in:
2026-08-09 18:59:14 +08:00
parent 1cfec40c99
commit c0967ab71f
14 changed files with 1242 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest';
import { scanMarkdown } from './markdown.js';
describe('scanMarkdown', () => {
it('extracts a fenced code block with a file= name', () => {
const md = [
'# Title',
'',
'```yaml file=parts/cargo.yaml',
'role: part',
'```',
'',
'text after',
].join('\n');
const { files, fences } = scanMarkdown(md, 'harbor/harbor.md');
expect(fences).toHaveLength(1);
expect(fences[0]).toMatchObject({
info: 'yaml file=parts/cargo.yaml',
content: 'role: part',
startLine: 3,
endLine: 5,
});
expect(files).toHaveLength(1);
expect(files[0]).toMatchObject({
name: 'harbor/parts/cargo.yaml',
kind: 'yaml',
text: 'role: part',
source: 'harbor/harbor.md:3-5',
});
});
it('auto-names a block without file= from its content hash', () => {
const md = '```yaml\nrole: part\n```';
const { files } = scanMarkdown(md, 'harbor/harbor.md');
expect(files).toHaveLength(1);
expect(files[0]!.name).toMatch(/^harbor\/[0-9a-f]{8}\.yaml$/);
expect(files[0]!.kind).toBe('yaml');
});
it('ignores non-definition languages', () => {
const md = '```js\nconst x = 1;\n```';
const { files, fences } = scanMarkdown(md, 'harbor/harbor.md');
expect(files).toHaveLength(0);
expect(fences).toHaveLength(1);
});
it('ignores indented code blocks', () => {
const md = ' role: part\n';
const { files } = scanMarkdown(md, 'harbor/harbor.md');
expect(files).toHaveLength(0);
});
it('names a csv block with file= as csv', () => {
const md = '```csv file=parts/seats.csv\nseat,x\nstring,number\n0,40\n```';
const { files } = scanMarkdown(md, 'harbor/harbor.md');
expect(files[0]).toMatchObject({ name: 'harbor/parts/seats.csv', kind: 'csv' });
});
it('tracks line numbers across multiple blocks', () => {
const md = [
'```yaml file=a.yaml',
'role: part',
'```',
'',
'```yaml file=b.yaml',
'role: part',
'```',
].join('\n');
const { fences } = scanMarkdown(md, 'harbor/harbor.md');
expect(fences.map((f) => f.startLine)).toEqual([1, 5]);
});
});