diff --git a/packages/gazzodown/src/blocks/OrderedListBlock.tsx b/packages/gazzodown/src/blocks/OrderedListBlock.tsx
index 6c6fc2734e34b..8eb800ff2f0b7 100644
--- a/packages/gazzodown/src/blocks/OrderedListBlock.tsx
+++ b/packages/gazzodown/src/blocks/OrderedListBlock.tsx
@@ -7,11 +7,23 @@ type OrderedListBlockProps = {
items: MessageParser.ListItem[];
};
+const renderChildren = (children: MessageParser.ListItem[]): ReactElement => (
+
+ {children.map((item, index) => (
+ -
+ {item.value}
+ {item.children?.length ? renderChildren(item.children) : null}
+
+ ))}
+
+);
+
const OrderedListBlock = ({ items }: OrderedListBlockProps): ReactElement => (
-
- {items.map(({ value, number }, index) => (
- -
- {value}
+
+ {items.map((item, index) => (
+ -
+ {item.value}
+ {item.children?.length ? renderChildren(item.children) : null}
))}
diff --git a/packages/gazzodown/src/blocks/UnorderedListBlock.tsx b/packages/gazzodown/src/blocks/UnorderedListBlock.tsx
index 500e525011107..f1f72ee0f035c 100644
--- a/packages/gazzodown/src/blocks/UnorderedListBlock.tsx
+++ b/packages/gazzodown/src/blocks/UnorderedListBlock.tsx
@@ -7,11 +7,23 @@ type UnorderedListBlockProps = {
items: MessageParser.ListItem[];
};
+const renderChildren = (children: MessageParser.ListItem[]): ReactElement => (
+
+ {children.map((item, index) => (
+ -
+ {item.value}
+ {item.children?.length ? renderChildren(item.children) : null}
+
+ ))}
+
+);
+
const UnorderedListBlock = ({ items }: UnorderedListBlockProps): ReactElement => (
-
+
{items.map((item, index) => (
-
{item.value}
+ {item.children?.length ? renderChildren(item.children) : null}
))}
diff --git a/packages/message-parser/src/definitions.ts b/packages/message-parser/src/definitions.ts
index fdc7179b7c6b7..de3fc4d961eeb 100644
--- a/packages/message-parser/src/definitions.ts
+++ b/packages/message-parser/src/definitions.ts
@@ -17,6 +17,8 @@ export type ListItem = {
type: 'LIST_ITEM';
value: Inlines[];
number?: number;
+ indent?: number;
+ children?: ListItem[];
};
export type Tasks = {
diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs
index 871c661e51a48..489a918fe83b7 100644
--- a/packages/message-parser/src/grammar.pegjs
+++ b/packages/message-parser/src/grammar.pegjs
@@ -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;
+}
}}
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
/**
*
* KaTex
diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts
index 9ac83ec8c9001..9fcb26919df2c 100644
--- a/packages/message-parser/src/utils.ts
+++ b/packages/message-parser/src/utils.ts
@@ -131,10 +131,12 @@ export const orderedList = generate('ORDERED_LIST');
export const unorderedList = generate('UNORDERED_LIST');
-export const listItem = (text: Inlines[], number?: number): ListItem => ({
+export const listItem = (text: Inlines[], number?: number, indent = 0): ListItem => ({
type: 'LIST_ITEM',
value: text,
...(number !== undefined && { number }),
+ indent,
+ children: [],
});
export const mentionUser = (() => {
@@ -191,7 +193,7 @@ export const reducePlainTexts = (values: Paragraph['value']): Paragraph['value']
let needsSlowPath = false;
for (let i = 0; i < flattenableValues.length; i++) {
const v = flattenableValues[i];
- if (Array.isArray(v) || (v as Inlines).type === 'EMOJI') {
+ if (Array.isArray(v) || v.type === 'EMOJI') {
needsSlowPath = true;
break;
}
diff --git a/packages/message-parser/tests/helpers.ts b/packages/message-parser/tests/helpers.ts
index 8c8bf448f08a5..29f20fb11b1d8 100644
--- a/packages/message-parser/tests/helpers.ts
+++ b/packages/message-parser/tests/helpers.ts
@@ -60,6 +60,8 @@ export const unorderedList = generate('UNORDERED_LIST');
export const listItem = (value: unknown[], number?: number) => ({
type: 'LIST_ITEM' as const,
value,
+ children: [],
+ indent: 0,
...(number !== undefined ? { number } : {}),
});
diff --git a/packages/message-parser/tests/unorderedList.test.ts b/packages/message-parser/tests/unorderedList.test.ts
index c7fbd1182767a..fadc32e870b83 100644
--- a/packages/message-parser/tests/unorderedList.test.ts
+++ b/packages/message-parser/tests/unorderedList.test.ts
@@ -45,10 +45,15 @@ test.each([
* *Fourth item*
`.trim(),
[
- unorderedList([listItem([plain('First item')])]),
- unorderedList([listItem([plain('Second item')]), listItem([plain('Third item')]), listItem([bold([plain('Fourth item')])])]),
+ unorderedList([
+ listItem([plain('First item')]),
+ listItem([plain('Second item')]),
+ listItem([plain('Third item')]),
+ listItem([bold([plain('Fourth item')])]),
+ ]),
],
],
+ [`* A * B`, [unorderedList([listItem([plain('A ')]), listItem([plain('B')])])]],
// [
// `
// * First item