Skip to content

fix: preserve empty task list items when parsing markdown - #4696

Merged
Jocs merged 6 commits into
marktext:developfrom
Renakoni:fix/empty-task-list-items
Jun 26, 2026
Merged

Jocs merged 6 commits into
marktext:developfrom
Renakoni:fix/empty-task-list-items

Conversation

@Renakoni

Copy link
Copy Markdown
Contributor

Closes #4680

Summary

Fix Markdown parsing for empty task list items.

Previously, empty task items such as - [ ] and - [x] were parsed as normal bullet list items whose text was the literal string [ ] or [x]. As a result, MarkText rendered them without a checkbox, and a single task list could be split into a task list plus a normal bullet list.

For example:

- [ ] a
- [ ] 

was parsed as:

  • one task list item for - [ ] a
  • one normal bullet list item containing [ ]

This PR fixes the token compatibility layer so empty checkbox markers are restored as task list items before markdownToState builds the document state.

Type of change

  • Bug fix (non-breaking, fixes an issue)
  • New feature (non-breaking, adds functionality)
  • Breaking change (causes existing functionality to change)
  • Documentation update

Test plan

  • New tests added (or explain why not needed)
  • Manually tested on: Windows 11 Pro 10.0.26200 with MarkText 0.20.0-beta.2 to reproduce the original issue; fix verified with targeted unit tests and Playwright validation on develop.

Test commands:

pnpm --filter @muyajs/core exec vitest run src/state/__tests__/markdownToState.spec.ts
pnpm --filter @muyajs/core exec vitest run src/state/__tests__/markdownToState.spec.ts src/state/__tests__/listSerialization.spec.ts
pnpm --filter @muyajs/core lint:types

Results:

markdownToState.spec.ts: 28 passed
markdownToState.spec.ts + listSerialization.spec.ts: 48 passed
lint:types: passed

Additional Playwright validation:

  • Started the Muya e2e host in Chromium.
  • Loaded - [ ] a\n- [ ] \n, - [ ] \n, and - [x] \n into the editor.
  • Verified the editor rendered task lists, task list items, and checkboxes for all cases.
  • Verified the parsed state preserved task-list-item semantics and the correct checked value.
  • Verified no normal bullet list was rendered for the mixed empty-task case.

Full @muyajs/core test note:

1279 passed, 1 failed

The remaining failure was an unrelated existing 5s timeout in src/ui/tableChessboard/__tests__/tableChessboard.spec.ts when importing the package entrypoint.

Notes for reviewers [optional]

The root cause is in the parsing compatibility path:

  • packages/muya/src/utils/marked/lexBlock.ts
  • packages/muya/src/utils/marked/compatibleTaskList.ts
  • packages/muya/src/state/markdownToState.ts

For non-empty task items, marked provides task: true, so compatibleTaskList.ts classifies the item as a task item.

For empty task items such as - [ ] , marked provides a normal list item with task: false and text [ ]. Because the compatibility layer only checked item.task, markdownToState had no way to recover the intended checkbox semantics.

This PR adds a narrow normalization step in compatibleTaskList.ts: if an unordered list item's raw Markdown is only a bullet marker plus an empty task checkbox marker, and the parsed text is only [ ], [x], or [X], it restores the token to a task item and clears the marker text before state conversion.

Regression tests cover:

  • An empty unchecked task item after a populated task item.
  • A single empty unchecked task item.
  • A single empty checked task item.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

Copilot AI review requested due to automatic review settings June 24, 2026 16:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Muya’s marked token compatibility layer so empty GFM task list items (e.g. - [ ] / - [x] ) are preserved as task-list items instead of degrading into normal bullet list items containing the literal text [ ] / [x].

Changes:

  • Add a normalization step in compatibleTaskList to detect unordered list items whose raw markdown is only an empty task marker and convert them into task list items with empty content.
  • Add regression tests in markdownToState.spec.ts covering empty unchecked/checked task items (standalone and following a populated task).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/muya/src/utils/marked/compatibleTaskList.ts Normalizes empty task markers into proper task list item tokens before state conversion.
packages/muya/src/state/tests/markdownToState.spec.ts Adds regression tests ensuring empty task list items become task-list-item states with correct checked meta.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Jocs

Jocs commented Jun 25, 2026

Copy link
Copy Markdown
Member

I can still produce this issue on your branch.

Comment thread packages/muya/src/utils/marked/compatibleTaskList.ts Outdated
@Renakoni

Copy link
Copy Markdown
Contributor Author

@Jocs
Thanks for checking this.
I verified this against marked's token output:

Input: "- [ ] \n"
raw: "- [ ]"
text: "[ ]"
matches[1]: " "
`[${matches[1]}]`: "[ ]"

Input: "- [x] \n"
raw: "- [x]"
text: "[x]"
matches[1]: "x"
`[${matches[1]}]`: "[x]"

Input: "- [X] \n"
raw: "- [X]"
text: "[X]"
matches[1]: "X"
`[${matches[1]}]`: "[X]"

So the condition is intentional: it verifies that the list item text is exactly the empty task marker and does not contain real task content before rewriting the token as a task-list item.

That said, I agree the inline template expression is easy to misread. I can make it clearer without changing behavior:

const marker = `[${matches[1]}]`;
if (text !== marker)
    return;

I also re-tested the original reported cases on fix/empty-task-list-items with clean files:

- [ ] a
- [ ] 
- [ ] 
- [x] 
QQ_1782379993893

All three now render as task-list items with checkboxes in pnpm run dev, and the focused markdownToState tests pass.

If there is another case that still reproduces the issue, please share the exact Markdown input and reproduction steps. I may have missed a different token shape, and I would be happy to add coverage for it.

@Renakoni

Copy link
Copy Markdown
Contributor Author

Follow-up on empty task list items

I checked the incomplete cases on top of fix/empty-task-list-items and found three related gaps.

The first gap is the lazy-continuation shape:

- [ ] 
text

With the current marked token stream, this is not emitted as a task item. It is emitted as a normal list item whose raw source starts with a task marker, while the parsed text starts with the synthetic marker text:

raw:  "- [ ] \ntext"
text: "[ ] \ntext"
task: false

The original fix only handled the empty-only shape where text === "[ ]" or text === "[x]", so it correctly fixed clean empty task items but still rejected this lazy-continuation variant.

The follow-up change keeps the original guard narrow: it only normalizes unordered list items whose raw source starts with a valid task marker and whose parsed text contains the matching synthetic marker prefix. It then strips only that synthetic marker from the token text while preserving the continuation content as the task item's paragraph. Invalid forms such as - [] and - [ ]text still remain normal bullet items.

The second gap is editor rendering. Parser-created empty list items had children: []. That can serialize correctly after the first follow-up, but it is still a bad editable state: a list item with no content descendant has no paragraph/content block for the editor to render or place the caret into. In the desktop editor, this made the source look correct while the visual editor only showed the non-empty task items.

The follow-up now normalizes empty list_item containers to contain an empty paragraph:

children: [{ name: "paragraph", text: "" }]

This matches the existing emptyStates model for bullet/task lists and keeps empty task items visible/editable in the editor.

The third gap is serialization. Programmatic or legacy states can still contain empty list items with children: []. stateToMarkdown previously emitted the item marker but no newline for that case, so consecutive empty items could be serialized as one physical line, for example:

- [ ] - [ ] 

The serializer now returns a complete line for empty list items. This covers both empty task items and plain empty bullet items without changing non-empty list item serialization.

Expected behavior after the follow-up:

- [ ] 
text

is parsed as one unchecked task item with text as its content. Saving may normalize it to:

- [ ] text

which preserves the Markdown semantics.

Regression coverage added:

pnpm --filter @muyajs/core test -- src/state/__tests__/markdownToState.spec.ts
pnpm --filter @muyajs/core test -- src/state/__tests__/listSerialization.spec.ts
pnpm --filter @muyajs/core test -- src/block/gfm/taskListItem/__tests__/emptyTaskItemRender.spec.ts
pnpm --filter @muyajs/core test -- src/state/__tests__/markdownToState.spec.ts src/state/__tests__/listSerialization.spec.ts src/state/__tests__/nestedMixedLists.spec.ts
pnpm --filter @muyajs/core test -- src/block/content/paragraphContent/__tests__/enterListItemEmptyFirst.spec.ts src/block/content/paragraphContent/__tests__/enterHandler.spec.ts src/block/content/paragraphContent/__tests__/backspaceUnwrap.spec.ts src/clipboard/__tests__/pasteListMerge.spec.ts src/state/__tests__/nestedMixedLists.spec.ts
pnpm --filter @muyajs/core exec vitest run --testTimeout 30000
pnpm --filter @muyajs/core lint:types
pnpm --filter @muyajs/core lint
pnpm typecheck
git diff --check HEAD

All passed locally. The lint command only reports existing warnings.

@Renakoni

Copy link
Copy Markdown
Contributor Author

Updated this branch after merging the latest develop.

I also folded the plain empty bullet-list regression coverage from the narrower empty-list PR into this PR, as
suggested. This PR now covers both:

  • empty task-list items, including consecutive empty unchecked/checked task items;
  • plain empty bullet-list items, including adjacent empty items before a following paragraph and an empty item between
    populated siblings.

While resolving the merge, I also fixed two regressions introduced by the interaction with newer develop changes:

  • narrowed the synthetic task-marker detection to spaces/tabs only, so [x] parent from HTML paste is not
    misclassified as a real task-list item;
  • updated the [Bug] keyboard navigation issue with lists #4644 arrow-navigation regression to construct a genuinely childless list item directly, because parsed
    empty list items are intentionally preserved as editable empty paragraphs in this PR.

Validation:

pnpm --filter @muyajs/core test -- src/state/__tests__/listSerialization.spec.ts src/state/__tests__/
markdownToState.spec.ts src/block/gfm/taskListItem/__tests__/emptyTaskItemRender.spec.ts src/utils/__tests__/
htmlTaskListPaste.spec.ts src/block/base/__tests__/arrowNavigation.spec.ts
pnpm --filter @muyajs/core exec vitest run --testTimeout 20000
pnpm --filter @muyajs/core lint:types
pnpm --filter @muyajs/core lint

@Jocs
Jocs merged commit 68ce87c into marktext:develop Jun 26, 2026
6 checks passed
hisaboh added a commit to hisaboh/remarks that referenced this pull request Jun 27, 2026
upstream が持ち込んだ主なもの:
- list serialization 刷新(空項目保持 marktext#4696/marktext#4697、markerOverride/_isLooseParentList)
- code fence 長の保持 marktext#4755、ソースモード undo/redo、math/diagram スクロール
- flush()/_flushOperationCache() による pending-edit 同期 marktext#2938
- plain-inline-spaces 保持の paste marktext#4706、大規模依存アップグレード marktext#4597 ほか

採用/保持の判断:
- フォーク保持: CLAUDE.md、setWrapCodeBlocks、PDF 描画
- 両立: fence info(フォーク) + fenceLength(upstream)、whole-line paste(フォーク) +
  plain-inline-spaces(upstream)、@tauri-apps/cli 保持 + upstream バージョンアップ採用
- upstream 採用: flush 機構(フォークの flushPendingChanges / _commitOperationCache /
  _rafHandle / _isGoing を除去、呼び出し側 editor.vue を flush() に更新)

波及修正:
- フォークの空 paragraph 往復保持を top-level (_listType 空) に限定し、upstream の
  空リスト項目処理と両立(muya unit 10件の回帰を解消)
- pnpm-lock.yaml をマージ後 package.json から再生成

検証(全緑): muya lint:types / check-circular / unit 1397 / conformance 1347、
desktop typecheck / unit 695、build:tauri 成功・実機動作確認 OK

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Empty task list items are parsed as normal bullet items and lose their checkbox

3 participants