diff --git a/lib/prose-lint.ts b/lib/prose-lint.ts index a844ec350..c06450c3b 100644 --- a/lib/prose-lint.ts +++ b/lib/prose-lint.ts @@ -110,13 +110,13 @@ export function maskProse(text: string): string { if (fence) { if (open && open[1]!.startsWith(fence[0]!) && open[1]!.length >= fence.length) fence = null; return ''; } if (comment) { const end = line.indexOf('-->'); if (end < 0) return ''; comment = false; line = ' '.repeat(end + 3) + line.slice(end + 3); } if (open) { fence = open[1]!; return ''; } - const tail = line.lastIndexOf('', tail) < 0) { comment = true; line = line.slice(0, tail); } - return line - .replace(//g, (m) => ' '.repeat(m.length)) + line = line .replace(/`[^`\n]*`/g, (m) => 'CODE' + ' '.repeat(Math.max(0, m.length - 4))) .replace(/\]\([^)\s]+\)/g, (m) => ']' + ' '.repeat(m.length - 1)) .replace(/https?:\/\/\S+/g, (m) => 'URL' + ' '.repeat(Math.max(0, m.length - 3))); + const tail = line.lastIndexOf('', tail) < 0) { comment = true; line = line.slice(0, tail); } + return line.replace(//g, (m) => ' '.repeat(m.length)); }); return out.join('\n'); } @@ -172,7 +172,11 @@ export function unreleasedBlock(text: string): { text: string; line: number } | export function sectionsOnly(text: string, names: string[]): string | null { const wanted = new Set(names.map(n => n.trim().toLowerCase()).filter(Boolean)); let keep = false, found = false; + let fence: string | null = null; const out = text.split('\n').map((line) => { + const open = /^\s*(`{3,}|~{3,})/.exec(line); + if (fence) { if (open && open[1]!.startsWith(fence[0]!) && open[1]!.length >= fence.length) fence = null; return keep ? line : ''; } + if (open) { fence = open[1]!; return keep ? line : ''; } const h = /^##\s+(.+?)\s*$/.exec(line); if (h) { keep = wanted.has(h[1]!.toLowerCase()); found ||= keep; return ''; } return keep ? line : ''; diff --git a/test/prose-lint.test.ts b/test/prose-lint.test.ts index f33d9b8c2..cf5245559 100644 --- a/test/prose-lint.test.ts +++ b/test/prose-lint.test.ts @@ -137,6 +137,8 @@ describe('selections', () => { expect(picked.split('\n')).toHaveLength(body.split('\n').length); expect(lintProse(picked, 'b.md').map(f => [f.rule, f.line])).toEqual([['vocabulary', 2], ['vocabulary', 2], ['arrow', 9]]); expect(sectionsOnly(body, ['Why'])).toBeNull(); + const fenced = ['## Summary', 'Fine.', '', '## Pre-Landing Review', '```codex-review', '## Summary', 'The parser showcases a vibrant result.', '```', '', '## Blast radius', '```', '## Why', 'a fenced heading showcases nothing', '```', 'but this vibrant line is read.'].join('\n'); + expect(lintProse(sectionsOnly(fenced, ['Summary', 'Blast radius'])!, 'b.md').map(f => [f.rule, f.line])).toEqual([['vocabulary', 15]]); const gated = run([path.join(FIXTURES, 'slopped-changelog.md'), '--sections', '[1.0.0] - 2026-10-01', '--gate']); expect(gated.code).toBe(1); expect(gated.out).toMatch(/^PROSE: \S+slopped-changelog\.md:16 /m); @@ -148,6 +150,8 @@ describe('selections', () => { test('a multi-line HTML comment is masked through its closing marker and prose after it is read', () => { const text = ['Plain start. The parser showcases it.', 'Showcasing a vibrant end.'].join('\n'); expect(lintProse(text, 'x.md').map(f => [f.rule, f.line])).toEqual([['vocabulary', 3], ['vocabulary', 4], ['vocabulary', 4]]); + const quoted = ['Use `