Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/generator/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,62 @@ describe('renderMarkdown', () => {
expect(md).not.toContain('Hidden paragraph.');
expect(md).not.toContain('A.');
});
// Regression (#122): notes/continuations/vanish siblings must NOT consume a CSI
// number. A "Related Sections" pr1 whose first children are specifier-note banners
// rendered its 1..n list starting at the note count (e.g. "5." instead of "1.").
it('numbers pr2 siblings from 1, skipping leading note siblings', () => {
const tree: SpecTree = {
id: '00000000-0000-0000-0000-000000000001',
section: '09 05 00',
title: 'Numbering',
parts: [
{
id: '00000000-0000-0000-0000-000000000002',
type: 'part',
text: 'GENERAL',
meta: {},
children: [
{
id: '00000000-0000-0000-0000-000000000003',
type: 'article',
text: 'SUMMARY',
meta: {},
children: [
{
id: '00000000-0000-0000-0000-000000000004',
type: 'pr1',
text: 'Related Sections:',
meta: {},
children: [
{
id: 'n1',
type: 'note',
text: 'banner one',
children: [],
meta: { vanish: true },
},
{
id: 'n2',
type: 'note',
text: 'banner two',
children: [],
meta: { vanish: true },
},
{ id: 'r1', type: 'pr2', text: 'Section 01 30 00', children: [], meta: {} },
{ id: 'r2', type: 'pr2', text: 'Section 01 33 00', children: [], meta: {} },
],
},
],
},
],
},
],
};
const md = renderMarkdown(tree);
expect(md).toContain(' 1. Section 01 30 00');
expect(md).toContain(' 2. Section 01 33 00');
expect(md).not.toContain('3. Section 01 30 00');
});
it('renders empty tree without error', () => {
const empty: SpecTree = {
id: '00000000-0000-0000-0000-000000000001',
Expand Down
47 changes: 35 additions & 12 deletions src/generator/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@ export function getLabel(type: NodeType, index: number, partNumber = 1): string

const INDENT = ' ';

// notes render as [NOTE] blockquotes, continuations as plain text, and vanish
// nodes not at all — none carry a CSI number, so none may consume an ordinal.
// Counting them shifted numbered siblings (#122): specifier-note banners pushed
// a 1..15 "Related Sections" list to 5..20.
function consumesNumber(node: SpecNode): boolean {
return node.type !== 'note' && node.type !== 'continuation' && !node.meta.vanish;
}

// Render a node's children, advancing the CSI ordinal only past numbered siblings
// so notes/continuations/vanish nodes interleave without disturbing the sequence.
function renderChildren(
children: readonly SpecNode[],
render: (child: SpecNode, ordinal: number) => string
): string {
let ordinal = 0;
const out: string[] = [];
for (const child of children) {
out.push(render(child, ordinal));
if (consumesNumber(child)) ordinal += 1;
}
return out.join('');
}

function renderPrNode(node: SpecNode, index: number, depth: number): string {
// note nodes always render as [NOTE] blockquotes regardless of meta.vanish — editorial
// notes are structural metadata visible to spec writers, not owner-facing content.
Expand All @@ -49,26 +72,26 @@ function renderPrNode(node: SpecNode, index: number, depth: number): string {
}
const pad = INDENT.repeat(depth);
const label = getLabel(node.type, index);
return [
`\n${pad}${label} ${node.text}`,
...node.children.map((child, i) => renderPrNode(child, i, depth + 1)),
].join('');
return (
`\n${pad}${label} ${node.text}` +
renderChildren(node.children, (child, ordinal) => renderPrNode(child, ordinal, depth + 1))
);
}

function renderArticle(node: SpecNode, index: number, partNumber: number): string {
const label = getLabel('article', index, partNumber);
return [
`\n### ${label} ${node.text}\n`,
...node.children.map((child, i) => renderPrNode(child, i, 0)),
].join('');
return (
`\n### ${label} ${node.text}\n` +
renderChildren(node.children, (child, ordinal) => renderPrNode(child, ordinal, 0))
);
}

function renderPart(node: SpecNode, index: number): string {
const label = getLabel('part', index);
return [
`\n## ${label} ${node.text}\n`,
...node.children.map((child, i) => renderArticle(child, i, index + 1)),
].join('');
return (
`\n## ${label} ${node.text}\n` +
renderChildren(node.children, (child, ordinal) => renderArticle(child, ordinal, index + 1))
);
}

export function renderMarkdown(tree: SpecTree): string {
Expand Down