From fe8bf16e77fe08efc22fe3bd4a06e29aef264a03 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Mon, 9 Sep 2024 22:13:19 +0700 Subject: [PATCH] fix: better nested todos parsing (#6499) Signed-off-by: Alexander Onnikov --- .../src/markdown/__tests__/markdown.test.ts | 280 ++++++++++++++++++ packages/text/src/markdown/parser.ts | 39 +-- 2 files changed, 302 insertions(+), 17 deletions(-) diff --git a/packages/text/src/markdown/__tests__/markdown.test.ts b/packages/text/src/markdown/__tests__/markdown.test.ts index 2c47c91979..f7b25fd36e 100644 --- a/packages/text/src/markdown/__tests__/markdown.test.ts +++ b/packages/text/src/markdown/__tests__/markdown.test.ts @@ -395,6 +395,286 @@ Lorem ipsum dolor sit amet. } ] } + }, + { + name: 'nested todos', + markdown: `# nested todos +- [ ] todo + - [x] sub todo +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1 }, + content: [ + { + type: 'text', + text: 'nested todos', + marks: [] + } + ] + }, + { + type: 'todoList', + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo', + marks: [] + } + ] + }, + { + type: 'todoList', + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'nested lists', + markdown: `# nested lists +- [ ] todo + - sub list item + - [x] sub todo +- list item + - [x] sub todo + - sub list item +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1 }, + content: [ + { + type: 'text', + text: 'nested lists', + marks: [] + } + ] + }, + { + type: 'todoList', + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo', + marks: [] + } + ] + }, + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub list item', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'todoList', + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'list item', + marks: [] + } + ] + }, + { + type: 'todoList', + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + }, + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub list item', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + name: 'nested todos', + markdown: `# nested todos +- [ ] todo + - [x] sub todo +`, + markup: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1 }, + content: [ + { + type: 'text', + text: 'nested todos', + marks: [] + } + ] + }, + { + type: 'todoList', + content: [ + { + type: 'todoItem', + attrs: { checked: false }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'todo', + marks: [] + } + ] + }, + { + type: 'todoList', + content: [ + { + type: 'todoItem', + attrs: { checked: true }, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'sub todo', + marks: [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } } ] diff --git a/packages/text/src/markdown/parser.ts b/packages/text/src/markdown/parser.ts index e4c2f88c60..647211cecb 100644 --- a/packages/text/src/markdown/parser.ts +++ b/packages/text/src/markdown/parser.ts @@ -621,6 +621,7 @@ export class MarkdownParser { listRule: RuleCore = (state: TaskListStateCore): boolean => { const tokens = state.tokens + const states: Array<{ closeIdx: number, lastItemIdx: number }> = [] // step #1 - convert list items to todo items for (let open = 0; open < tokens.length; open++) { @@ -630,32 +631,36 @@ export class MarkdownParser { } // step #2 - convert lists to proper type - let closeIdx = -1 - let lastItemIdx = -1 + // listCloseIdx and itemCloseIdx tracks position of the list and item close tokens + // because we insert items into the list, the variables keep the position from the + // end of the list so we don't have to count inserts + let listCloseIdx = -1 + let itemCloseIdx = -1 + for (let i = tokens.length - 1; i >= 0; i--) { if (tokens[i].type === 'bullet_list_close') { - closeIdx = i - lastItemIdx = -1 + states.push({ closeIdx: listCloseIdx, lastItemIdx: itemCloseIdx }) + listCloseIdx = tokens.length - i + itemCloseIdx = -1 } else if (tokens[i].type === 'list_item_close' || tokens[i].type === 'todo_item_close') { // when found item close token of different type, split the list - if (lastItemIdx === -1) { - lastItemIdx = i - } else if (tokens[i].type !== tokens[lastItemIdx].type) { + if (itemCloseIdx === -1) { + itemCloseIdx = tokens.length - i + } else if (tokens[i].type !== tokens[tokens.length - itemCloseIdx].type) { tokens.splice(i + 1, 0, new state.Token('bullet_list_open', 'ul', 1)) tokens.splice(i + 1, 0, new state.Token('bullet_list_close', 'ul', -1)) - convertTodoList(tokens, i + 2, closeIdx + 2, lastItemIdx + 2) - closeIdx = i + 1 - lastItemIdx = i + convertTodoList(tokens, i + 2, tokens.length - listCloseIdx, tokens.length - itemCloseIdx) + listCloseIdx = tokens.length - i - 1 + itemCloseIdx = tokens.length - i } - } else if (tokens[i].type === 'bullet_list_open' && tokens[i].level === tokens[closeIdx].level) { - // when found list open token of the same level, decide what to do - if (lastItemIdx !== -1) { - convertTodoList(tokens, i, closeIdx, lastItemIdx) + } else if (tokens[i].type === 'bullet_list_open') { + if (itemCloseIdx !== -1) { + convertTodoList(tokens, i, tokens.length - listCloseIdx, tokens.length - itemCloseIdx) } - // Reset closeIdx and lastItemIdx for the next list - closeIdx = -1 - lastItemIdx = -1 + const prevState = states.pop() ?? { closeIdx: -1, lastItemIdx: -1 } + listCloseIdx = prevState.closeIdx + itemCloseIdx = prevState.lastItemIdx } }