Skip to content

Commit 27249fe

Browse files
committed
fix(website): keep blank-separated indented bullets in one nested list
A blank line inside an entry closes the open block, so the next indented bullet found no open list and started its own. The generator writes each commit subject as a blank-separated line, which CommonMark reads as one loose list, so this was the dominant shape rather than an edge case: 327 of the corpus's 346 nested lists came out holding a single item, and 34 of 229 files had an entry whose one list rendered as several adjacent ones, each carrying its own margin. An indented bullet now resumes the TRAILING nested list rather than the open one, so a blank line between bullets keeps them together while a paragraph in between still separates the lists.
1 parent c176cf6 commit 27249fe

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

website/modules/changelog/utils/render-entry.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,15 @@ export function renderEntryBody(md: string): string {
102102
openItem(line.slice(2).trim());
103103
} else if (itemOpen && /^ {2,}[-*] /.test(line)) {
104104
const text = line.trim().slice(2).trim();
105-
if (openBlock && openBlock.kind === 'ul') openBlock.items.push([text]);
105+
// Resume the TRAILING nested list rather than the open one. A blank
106+
// line between indented bullets is a LOOSE list in CommonMark, still
107+
// one list, and the generator writes exactly that shape (one `*`
108+
// commit subject per blank-separated line). Keying off the open block
109+
// would emit a separate single-item list per bullet, each with its own
110+
// margin, splitting one list into several. A paragraph in between is a
111+
// different matter, and genuinely does start a new list.
112+
const last = blocks[blocks.length - 1];
113+
if (last && last.kind === 'ul') { last.items.push([text]); openBlock = last; }
106114
else pushBlock({ kind: 'ul', items: [[text]] });
107115
} else if (itemOpen && /^ {2,}\S/.test(line)) {
108116
const text = line.trim();

website/test/changelog/render-entry.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,51 @@ test('a single-line entry still renders as bare text in its item', () => {
108108
assert.ok(!html.includes('<p class="my-2'), 'no paragraph wrapper for a body-less entry');
109109
});
110110

111+
test('blank-separated indented bullets stay ONE nested list', () => {
112+
// The dominant generated shape, and the one the tight-list fixture above
113+
// cannot exercise: the generator writes each commit subject as its own
114+
// ` * ` line separated by a whitespace-only line. CommonMark reads that
115+
// as one LOOSE list. Emitting a single-item list per bullet would give
116+
// each its own margin and visually split the list apart.
117+
const md = bodyOf(`${CHANGELOG_DIR}cli/0.10.11.md`);
118+
const html = renderEntryBody(md);
119+
120+
const first = entryItems(html)[0];
121+
const nested = first.match(/<ul class="list-disc pl-5 space-y-1[^"]*">[\s\S]*?<\/ul>/g) || [];
122+
assert.equal(nested.length, 1, 'two blank-separated bullets form one list, not two');
123+
assert.equal((nested[0].match(/<li>/g) || []).length, 2);
124+
});
125+
126+
test('a paragraph between indented bullets does start a new nested list', () => {
127+
// The counterfactual for the rule above. Resuming the trailing list is
128+
// correct across a blank line only; real prose in between separates them.
129+
const html = renderEntryBody([
130+
'- **entry**',
131+
' - first list',
132+
'',
133+
' Prose that interrupts.',
134+
'',
135+
' - second list',
136+
].join('\n'));
137+
138+
const nested = html.match(/<ul class="list-disc pl-5 space-y-1[^"]*">[\s\S]*?<\/ul>/g) || [];
139+
assert.equal(nested.length, 2);
140+
assert.equal(countEntryItems(html), 1);
141+
});
142+
143+
test('no changelog file renders a fragmented nested list', () => {
144+
// Whole-corpus form of the two tests above. Adjacent nested lists inside
145+
// one entry are always a list that got split, since nothing can sit
146+
// between them without separating them legitimately.
147+
for (const [label, md] of everyEntryFile()) {
148+
const html = renderEntryBody(md);
149+
assert.ok(
150+
!/<\/ul><ul class="list-disc pl-5 space-y-1/.test(html),
151+
`${label}: a nested list was split into adjacent single lists`,
152+
);
153+
}
154+
});
155+
111156
test('a generated entry keeps its commit-body content', () => {
112157
const md = bodyOf(`${CHANGELOG_DIR}server/0.8.56.md`);
113158
const html = renderEntryBody(md);

0 commit comments

Comments
 (0)