Skip to content

Commit f727b97

Browse files
committed
fix(changelog): preserve fenced code blocks in custom PR changelog entries
Code blocks (```lang ... ```) in custom Changelog Entry sections of PR descriptions were silently dropped during parsing. Root cause: two functions only handled a subset of marked.lexer token types: 1. extractNestedContent() only handled 'list' tokens — code blocks inside list items were skipped 2. parseTokensToEntries() only handled 'list' and 'paragraph' — standalone code blocks after paragraphs were skipped Fix: add 'code' token handling to both functions: - In extractNestedContent: render as indented fenced code block (2-space indent, matching sub-bullet level), properly handled during recursive indentation for deeper nesting - In parseTokensToEntries: attach standalone code blocks as nestedContent on the previous entry, or create a new entry if no previous one exists
1 parent adb112b commit f727b97

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

src/utils/__tests__/changelog-extract.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,96 @@ Intro paragraph:
197197
});
198198
});
199199
});
200+
201+
describe('extractChangelogEntry code blocks', () => {
202+
it('preserves fenced code block inside a bullet item', () => {
203+
const prBody = `### Changelog Entry
204+
205+
- Added a new \`foo\` function:
206+
\`\`\`go
207+
func foo() {
208+
fmt.Println("Hello")
209+
}
210+
\`\`\`
211+
- Another entry`;
212+
213+
const result = extractChangelogEntry(prBody);
214+
expect(result).toHaveLength(2);
215+
expect(result![0].text).toBe('Added a new `foo` function:');
216+
expect(result![0].nestedContent).toContain('```go');
217+
expect(result![0].nestedContent).toContain('func foo()');
218+
expect(result![1].text).toBe('Another entry');
219+
expect(result![1].nestedContent).toBeUndefined();
220+
});
221+
222+
it('preserves code block without language specifier', () => {
223+
const prBody = `### Changelog Entry
224+
225+
- Example code:
226+
\`\`\`
227+
some code here
228+
\`\`\``;
229+
230+
const result = extractChangelogEntry(prBody);
231+
expect(result).toHaveLength(1);
232+
expect(result![0].text).toBe('Example code:');
233+
expect(result![0].nestedContent).toContain('```');
234+
expect(result![0].nestedContent).toContain('some code here');
235+
// Should not have a language tag after the opening fence
236+
expect(result![0].nestedContent).not.toMatch(/```\S/);
237+
});
238+
239+
it('preserves standalone code block after paragraph', () => {
240+
const prBody = `### Changelog Entry
241+
242+
Here is a change with code:
243+
244+
\`\`\`go
245+
func foo() {}
246+
\`\`\``;
247+
248+
const result = extractChangelogEntry(prBody);
249+
expect(result).toHaveLength(1);
250+
expect(result![0].text).toBe('Here is a change with code:');
251+
expect(result![0].nestedContent).toContain('```go');
252+
expect(result![0].nestedContent).toContain('func foo() {}');
253+
// Verify 2-space indentation for proper nesting under list items
254+
expect(result![0].nestedContent).toBe(' ```go\n func foo() {}\n ```');
255+
});
256+
257+
it('preserves code block in loose list item', () => {
258+
const prBody = `### Changelog Entry
259+
260+
- First entry with code:
261+
262+
\`\`\`go
263+
func foo() {}
264+
\`\`\`
265+
266+
- Second entry`;
267+
268+
const result = extractChangelogEntry(prBody);
269+
expect(result).toHaveLength(2);
270+
expect(result![0].text).toBe('First entry with code:');
271+
expect(result![0].nestedContent).toContain('```go');
272+
expect(result![0].nestedContent).toContain('func foo() {}');
273+
expect(result![1].text).toBe('Second entry');
274+
});
275+
276+
it('preserves nested list items alongside code blocks', () => {
277+
const prBody = `### Changelog Entry
278+
279+
- Main entry
280+
- Sub item
281+
\`\`\`js
282+
const x = 1;
283+
\`\`\``;
284+
285+
const result = extractChangelogEntry(prBody);
286+
expect(result).toHaveLength(1);
287+
expect(result![0].text).toBe('Main entry');
288+
expect(result![0].nestedContent).toContain('- Sub item');
289+
expect(result![0].nestedContent).toContain('```js');
290+
expect(result![0].nestedContent).toContain('const x = 1;');
291+
});
292+
});

src/utils/changelog.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,15 @@ function extractNestedContent(tokens: Token[]): string {
337337
nestedLines.push(indentedDeeper);
338338
}
339339
}
340+
} else if (token.type === 'code') {
341+
// Preserve fenced code blocks (e.g., ```go ... ```) as nested content
342+
const codeToken = token as Tokens.Code;
343+
const fence = codeToken.lang ? `\`\`\`${codeToken.lang}` : '```';
344+
nestedLines.push(` ${fence}`);
345+
for (const line of codeToken.text.split('\n')) {
346+
nestedLines.push(` ${line}`);
347+
}
348+
nestedLines.push(' ```');
340349
}
341350
}
342351

@@ -390,6 +399,30 @@ function parseTokensToEntries(tokens: Token[]): ChangelogEntryItem[] | null {
390399
if (text) {
391400
entries.push({ text });
392401
}
402+
} else if (token.type === 'code') {
403+
// Standalone code blocks become nested content on the previous entry,
404+
// or a new entry if there is no previous entry.
405+
// Indent with 2 spaces so the block nests properly under the list item
406+
// in the final markdown (consistent with extractNestedContent).
407+
const codeToken = token as Tokens.Code;
408+
const fence = codeToken.lang ? `\`\`\`${codeToken.lang}` : '```';
409+
const indentedLines = [` ${fence}`];
410+
for (const line of codeToken.text.split('\n')) {
411+
indentedLines.push(` ${line}`);
412+
}
413+
indentedLines.push(' ```');
414+
const codeBlock = indentedLines.join('\n');
415+
416+
if (entries.length > 0) {
417+
// Attach to previous entry as nested content
418+
const prev = entries[entries.length - 1];
419+
prev.nestedContent = prev.nestedContent
420+
? `${prev.nestedContent}\n${codeBlock}`
421+
: codeBlock;
422+
} else {
423+
// No previous entry - create one with the code block as content
424+
entries.push({ text: '', nestedContent: codeBlock });
425+
}
393426
}
394427
}
395428

0 commit comments

Comments
 (0)