From 89daa4fd53b44c9dc2f93dd2398876c5e5114003 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Mon, 24 Aug 2026 08:43:28 +0000 Subject: [PATCH] fix: Support two-space list indentation tolerance --- packages/markdown-parser/src/index.spec.ts | 31 +++++++++++++++++++ packages/markdown-parser/src/rules/blocks.ts | 32 ++++++++++++++++++-- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/markdown-parser/src/index.spec.ts b/packages/markdown-parser/src/index.spec.ts index 1f828f2..fadd95d 100644 --- a/packages/markdown-parser/src/index.spec.ts +++ b/packages/markdown-parser/src/index.spec.ts @@ -87,4 +87,35 @@ describe('test @fuyeor/markdown-parser', () => { RangeError, ); }); + + it.each([ + ['modern two-space indentation', ' - example'], + ['legacy three-space marker alignment', ' - example'], + ])('recognizes %s as a nested list', (_description, childLine) => { + const ast = createFuyeorMarkdownParser()(`1. example\n${childLine}`); + const rootList = ast[0]; + + expect(rootList.type).toBe('list'); + expect(rootList.children).toHaveLength(1); + expect( + rootList.children?.[0].children?.some((node) => node.type === 'list'), + ).toBe(true); + }); + + it('uses two-space steps for deeper list nesting', () => { + const ast = createFuyeorMarkdownParser()( + '1. root\n - level 1\n - level 2', + ); + const rootItem = ast[0].children?.[0]; + const levelOneList = rootItem?.children?.find( + (node) => node.type === 'list', + ); + const levelOneItem = levelOneList?.children?.[0]; + const levelTwoList = levelOneItem?.children?.find( + (node) => node.type === 'list', + ); + + expect(levelOneList).toBeDefined(); + expect(levelTwoList).toBeDefined(); + }); }); diff --git a/packages/markdown-parser/src/rules/blocks.ts b/packages/markdown-parser/src/rules/blocks.ts index e1e01aa..c67c8c5 100644 --- a/packages/markdown-parser/src/rules/blocks.ts +++ b/packages/markdown-parser/src/rules/blocks.ts @@ -234,6 +234,9 @@ export const blockquoteRule: BlockRule = { }, }; +const LIST_ITEM_PATTERN = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)/; +const LIST_INDENT_STEP = 2; + /** * parse list syntax (- xxx) */ @@ -245,7 +248,7 @@ export const listRule: BlockRule = { if (!line) return null; // match list header: supports -, *, + and 1., 99) - const match = line.match(/^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)/); + const match = line.match(LIST_ITEM_PATTERN); if (!match) return null; const baseIndent = match[1].length; @@ -258,7 +261,7 @@ export const listRule: BlockRule = { while (state.lineIndex + consumedLines < state.lineCount) { const currentLine = state.lines[state.lineIndex + consumedLines]; - const itemMatch = currentLine.match(/^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)/); + const itemMatch = currentLine.match(LIST_ITEM_PATTERN); // to determine if an item is a new list item: // the indentation must be consistent with the baseline @@ -269,6 +272,8 @@ export const listRule: BlockRule = { // detect subsequent lines belonging to this item // (lines with indentation deeper than the marker). const markerTotalWidth = baseIndent + marker.length + 1; + // Normalize nested list markers to two-space logical levels, tolerating odd legacy indentation. + const nestedListIndent = baseIndent + LIST_INDENT_STEP; while ( state.lineIndex + consumedLines + itemConsumedLines < @@ -283,7 +288,28 @@ export const listRule: BlockRule = { } const nextIndent = nextLine.match(/^(\s*)/)![1].length; - if (nextIndent >= markerTotalWidth) { + const firstContentChar = nextLine[nextIndent]; + const isListMarkerCandidate = + firstContentChar === '-' || + firstContentChar === '*' || + firstContentChar === '+' || + (firstContentChar >= '0' && firstContentChar <= '9'); + let normalizedContentStart = markerTotalWidth; + + if (nextIndent >= nestedListIndent && isListMarkerCandidate) { + const relativeIndent = nextIndent - baseIndent; + const nestingLevel = Math.floor(relativeIndent / LIST_INDENT_STEP); + const contentIndent = (nestingLevel - 1) * LIST_INDENT_STEP; + normalizedContentStart = nextIndent - contentIndent; + } + + if ( + normalizedContentStart !== markerTotalWidth && + LIST_ITEM_PATTERN.test(nextLine) + ) { + itemLines.push(nextLine.slice(normalizedContentStart)); + itemConsumedLines++; + } else if (nextIndent >= markerTotalWidth) { // remove the indentation corresponding to the width itemLines.push(nextLine.slice(markerTotalWidth)); itemConsumedLines++;