-
Notifications
You must be signed in to change notification settings - Fork 13.5k
fix: support nested markdown lists #40297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,26 @@ let skipBold = false; | |
| let skipItalic = false; | ||
| let skipStrikethrough = false; | ||
| let skipReferences = false; | ||
|
|
||
| function buildNestedList(flatItems) { | ||
| const root = []; | ||
| const stack = [{ level: -1, children: root }]; | ||
|
|
||
| for (const item of flatItems) { | ||
| const level = item.indent ?? 0; | ||
|
|
||
| // Pop stack until we find a parent whose level < current | ||
| while (stack.length > 1 && stack[stack.length - 1].level >= level) { | ||
| stack.pop(); | ||
| } | ||
|
|
||
| const node = { ...item, children: [] }; | ||
| stack[stack.length - 1].children.push(node); | ||
| stack.push({ level, children: node.children }); | ||
| } | ||
|
|
||
| return root; | ||
| } | ||
|
Comment on lines
+46
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In CommonMark, tabs are treated specially when they affect block structure (including list indentation): 1) General rule for tabs Tabs are not automatically expanded to spaces. However, in contexts where whitespace defines block structure, a tab behaves as if it were replaced by spaces using a tab stop of 4 characters. 2) How this impacts list indentation When deciding whether a line is part of a list item (including sublist nesting / continuation blocks), CommonMark’s list-item parsing uses indentation based on the “width” of the list marker plus the following required spaces (and the general tab rule applies in the whitespace that determines indentation). A tab used to indent a continuation paragraph within a list item is equivalent to indenting with four spaces (because that tab stop is 4 characters). The spec includes an explicit example showing that effect. Practical implication: to nest/align content under list items, you can rely on tabs contributing indentation in 4-character steps; e.g., a single tab provides the same indentation “width” as four spaces for these list-structure decisions. Sources: CommonMark spec section on tabs (2.2) and the list-item indentation example demonstrating a tab as equivalent to four spaces. Citations:
🏁 Script executed: # First, let's locate and examine the grammar.pegjs file
cd packages/message-parser && ls -la src/
# Read the full grammar.pegjs file to see the context around lines 46-64
wc -l src/grammar.pegjsRepository: RocketChat/Rocket.Chat Length of output: 634 🏁 Script executed: # Read the grammar.pegjs file, focusing on the buildNestedList function and surrounding context
head -100 packages/message-parser/src/grammar.pegjs | tail -60Repository: RocketChat/Rocket.Chat Length of output: 1167 🏁 Script executed: # Search for where indent is captured and used in list-item rules
rg -n "indent" packages/message-parser/src/grammar.pegjs -A 2 -B 2Repository: RocketChat/Rocket.Chat Length of output: 940 🏁 Script executed: # Look for OrderedListItem, UnorderedListHyphenItem, UnorderedListAsteriskItem rules
rg -n "OrderedListItem|UnorderedListHyphenItem|UnorderedListAsteriskItem" packages/message-parser/src/grammar.pegjs -A 5 -B 1Repository: RocketChat/Rocket.Chat Length of output: 1268 Indentation must use tab-stop column width, not character count, to match CommonMark behavior.
Use a tab-aware function to compute column width: Example implementationconst indentWidth = (s) => {
let c = 0;
for (const ch of s) c += ch === '\t' ? 4 - (c % 4) : 1;
return c;
};Then replace 🤖 Prompt for AI Agents |
||
| }} | ||
|
|
||
| Start | ||
|
|
@@ -174,30 +194,44 @@ TaskFlag = "x" { return true; } / " " { return false; } | |
| * 3. Item Three | ||
| * | ||
| */ | ||
| OrderedList = items:OrderedListItem+ { return orderedList(items); } | ||
|
|
||
| OrderedListItem = number:Digits "." [ \t]+ text:Inline { return listItem(text, parseInt(number, 10)); } | ||
|
|
||
| /** | ||
| * | ||
| * Unordered List | ||
| * Ordered List (with nesting support) | ||
| * e.g: | ||
| * - Item One | ||
| * - Item Two | ||
| * * Item Three | ||
| * * Item Four | ||
| * | ||
| * 1. Item One | ||
| * 1. Nested Item | ||
| */ | ||
| UnorderedList = items:(UnorderedListHyphenItem+ / UnorderedListAsteriskItem+) { return unorderedList(items); } | ||
|
|
||
| UnorderedListHyphenItem = "-" [ \t]+ text:Inline { return listItem(text); } | ||
| OrderedList = items:OrderedListItem+ { return orderedList(buildNestedList(items)); } | ||
|
|
||
| UnorderedListAsteriskItem = "*" [ \t]+ text:UnorderedListItemContent { return listItem(text); } | ||
| OrderedListItem | ||
| = indent:$([ \t]*) number:Digits "." [ \t]+ text:Inline | ||
| { return listItem(text, parseInt(number, 10), indent.length); } | ||
|
|
||
| UnorderedListItemContent = value:UnorderedListItemContentItem+ !"*" EndOfLine? { return reducePlainTexts(value); } | ||
| /** | ||
| * Unordered List (with nesting support) | ||
| */ | ||
| UnorderedList = items:(UnorderedListHyphenItem / UnorderedListAsteriskItem)+ | ||
| { return unorderedList(buildNestedList(items)); } | ||
|
|
||
| UnorderedListHyphenItem | ||
| = indent:$([ \t]*) "-" [ \t]+ text:Inline | ||
| { return listItem(text, undefined, indent.length); } | ||
|
|
||
| UnorderedListAsteriskItem | ||
| = indent:$([ \t]*) "*" [ \t]+ text:UnorderedListAsteriskItemContent | ||
| &{ | ||
| const plainText = text.map((item) => item.value ?? '').join(''); | ||
| return plainText !== '*' && !(plainText.endsWith('*') && !plainText.startsWith('*')); | ||
| } | ||
| { return listItem(text, undefined, indent.length); } | ||
|
|
||
| UnorderedListItemContentItem = InlineItemPattern / !"*" @Any | ||
| UnorderedListAsteriskItemContent | ||
| = value:UnorderedListAsteriskContentItem+ EndOfLine? | ||
| { return reducePlainTexts(value); } | ||
|
|
||
| UnorderedListAsteriskContentItem | ||
| = !EndOfLine !("*" [ \t]) @InlineItemPattern | ||
| / !EndOfLine !("*" [ \t]) @Any | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| /** | ||
| * | ||
| * KaTex | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.