diff --git a/src/__tests__/content.test.ts b/src/__tests__/content.test.ts index edc9c61..48a610e 100644 --- a/src/__tests__/content.test.ts +++ b/src/__tests__/content.test.ts @@ -258,6 +258,37 @@ describe('parseDoc', () => { expect(page.title).toBe('indented-code'); }); + it.each([ + ['four spaces', ' '], + ['a tab', '\t'], + ])('keeps component attributes indented with %s', (_kind, indent) => { + const page = parseDoc( + 'tutorial/indented-attributes.md', + [''].join('\n'), + ); + + expect(page.components).toEqual([ + { attributes: { lede: 'Lede text', title: 'Hero' }, name: 'PageHero' }, + ]); + expect(page.title).toBe('Hero'); + expect(page.summary).toBe('Lede text'); + }); + + it('keeps a component nested four spaces under a list item', () => { + const page = parseDoc( + 'tutorial/list-component.md', + '1. Step\n\n
', + ); + + expect(page.components).toEqual([{ attributes: { caption: 'Step one' }, name: 'Figure' }]); + }); + + it('keeps an indented lazy continuation in the summary', () => { + const page = parseDoc('tutorial/lazy.md', 'Intro line one\n continued lazily here'); + + expect(page.summary).toBe('Intro line one continued lazily here'); + }); + it('keeps shorter nested fences inside a longer fenced example', () => { const page = parseDoc( 'tutorial/nested-fence.md', diff --git a/src/content.ts b/src/content.ts index 62fa644..8d76139 100644 --- a/src/content.ts +++ b/src/content.ts @@ -132,22 +132,56 @@ function transitionFence(line: string, current: MarkdownFence | undefined): Fenc return { delimiter: false, fence: current }; } -function isIndentedCodeLine(line: string): boolean { - const candidate = quoteContainer(line).rest; - if (/^(?: {4}|\t)/.test(candidate)) return true; - const list = /^ {0,3}(?:[-+*]|\d{1,9}[.)])([ \t]+)(.*)$/.exec(candidate); - if (!list) return false; - const padding = list[1] ?? ''; - return padding.includes('\t') || padding.length >= 5 || /^(?: {4}|\t)/.test(list[2] ?? ''); +function indentWidth(line: string): number { + let width = 0; + for (const character of line) { + if (character === ' ') width += 1; + else if (character === '\t') width += 4 - (width % 4); + else break; + } + return width; } -function extractTitle(body: string): string | undefined { +function proseLines(body: string): (string | undefined)[] { let fence: MarkdownFence | undefined; - for (const line of body.split(/\r?\n/)) { + let afterBreak = true; + let inCode = false; + let listIndent = 0; + return body.split(/\r?\n/).map((line) => { const transition = transitionFence(line, fence); fence = transition.fence; - if (transition.delimiter || fence) continue; - if (isIndentedCodeLine(line)) continue; + if (transition.delimiter || fence) { + afterBreak = true; + inCode = false; + return undefined; + } + const candidate = quoteContainer(line).rest; + if (candidate.trim() === '') { + afterBreak = true; + return line; + } + const width = indentWidth(candidate); + if (afterBreak && width < listIndent) listIndent = 0; + inCode = width - listIndent >= 4 && (afterBreak || inCode); + afterBreak = false; + if (inCode) return undefined; + const item = /^ {0,3}(?:[-+*]|\d{1,9}[.)])([ \t]+)(.*)$/.exec(candidate); + if (!item) return line; + const padding = item[1] ?? ''; + const marker = candidate.length - (item[2] ?? '').length - padding.length; + if (padding.includes('\t') || padding.length >= 5 || /^(?: {4}|\t)/.test(item[2] ?? '')) { + listIndent = marker + 1; + inCode = true; + return undefined; + } + listIndent = marker + padding.length; + return line; + }); +} + +function extractTitle(body: string): string | undefined { + for (const line of proseLines(body)) { + if (line === undefined) continue; const match = /^#\s+(.+?)\s*$/.exec(line); if (match?.[1]) return cleanInline(match[1].replace(/\s+#+\s*$/, '')); } @@ -164,7 +198,6 @@ function truncate(value: string, length: number): string { function extractSummary(body: string): string { const paragraphs: string[] = []; let current: string[] = []; - let fence: MarkdownFence | undefined; let inContainer = false; let inTag = false; let hiddenTag: 'script' | 'style' | undefined; @@ -174,14 +207,8 @@ function extractSummary(body: string): string { current = []; }; - for (const rawLine of body.split(/\r?\n/)) { - const transition = transitionFence(rawLine, fence); - fence = transition.fence; - if (transition.delimiter || fence) { - finishParagraph(); - continue; - } - if (isIndentedCodeLine(rawLine)) { + for (const rawLine of proseLines(body)) { + if (rawLine === undefined) { finishParagraph(); continue; } @@ -251,14 +278,10 @@ function parseAttributes(source: string): Readonly } function componentSource(body: string): string { - const lines: string[] = []; - let fence: MarkdownFence | undefined; - for (const line of body.split(/\r?\n/)) { - const transition = transitionFence(line, fence); - fence = transition.fence; - if (!transition.delimiter && !fence && !isIndentedCodeLine(line)) lines.push(line); - } - return lines.join('\n').replace(//g, ' '); + return proseLines(body) + .filter((line) => line !== undefined) + .join('\n') + .replace(//g, ' '); } function extractComponents(body: string): DocComponent[] {