fix: better nested todos parsing (#6499)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-09-09 22:13:19 +07:00 committed by GitHub
parent cabc77acb1
commit fe8bf16e77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 302 additions and 17 deletions

View File

@ -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: []
}
]
}
]
}
]
}
]
}
]
}
]
}
}
]

View File

@ -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
}
}