Repository navigation
fix: prevent nested empty list items from becoming setext headings - #4697
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a Markdown round-trip ambiguity in @muyajs/core where a nested empty - bullet item immediately following non-empty paragraph text can be reparsed as a setext heading underline, corrupting the list structure on reload.
Changes:
- Track the previously-serialized block state and force a blank line before a
bullet-listthat starts with an empty-item when it follows a non-empty paragraph. - Extend list-block serialization to accept a
forceLineBreakflag that triggers insertion of a separator newline. - Add a regression test that serializes the problematic state, reparses it, and asserts no
setext-headingis produced and nested items are preserved.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/muya/src/state/stateToMarkdown.ts | Adds detection + forced blank line insertion to avoid setext-heading ambiguity for nested empty - lists. |
| packages/muya/src/state/tests/listSerialization.spec.ts | Adds regression coverage validating safe serialization and correct reparse structure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
According to standard Markdown parsing, an empty nested list should indeed be parsed as setext, and forcibly inserting a blank line is indeed one solution, but it breaks loose/tight lists. Are there better alternatives? I tried adjusting the indentation and the marker, but that doesn't seem to work either. |
|
I agree the blank-line fix is not ideal. It avoids the setext ambiguity, but I confirmed locally that the emitted form reparses the outer list as A better serializer-side alternative seems to be changing only the ambiguous nested unordered-list marker. For example: - a
*
*This reparses without a setext heading, preserves both nested empty items, and keeps the outer list tight. I also checked ordered parents and different outer bullet markers; the issue follows the nested empty The tradeoff is source marker style ( I also found we should cover a second round-trip case ( |
|
Thanks for pointing this out. I agree the blank-line fix was not ideal. I reworked the fix so it no longer forces a tight list to become loose. Instead, the serializer now only handles the - a
*
*This avoids the setext-heading ambiguity while keeping the parent list tight. The original state is not mutated; the I also fixed the second round-trip case for parser-created empty list items. After reopening, empty list items can Covered cases include:
Validation: All passed. |
|
I found that the marked(we use) is not fully follow commonmark spec, so change marker works. |
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]>
Closes #4683
Summary
Fix Markdown serialization for nested empty dash bullet items that can be reparsed as a setext heading underline.
When a list item contains paragraph text followed by a nested bullet list whose first item is empty, the serializer previously produced this ambiguous Markdown:
On reload, the first nested empty dash marker can be interpreted as the setext H2 underline for
a, so the parent item text becomes a heading and one nested empty list item is lost.This PR makes the serializer emit a round-trip-safe form for that specific boundary:
The blank line prevents the nested empty dash bullet from being consumed as a setext heading underline while preserving the list structure.
Type of change
Test plan
develop.Test commands:
pnpm --filter @muyajs/core exec vitest run src/state/__tests__/listSerialization.spec.ts src/state/__tests__/markdownToState.spec.ts pnpm --filter @muyajs/core lint:typesResults:
Additional round-trip validation:
Additional Playwright validation:
bandcfrom:window.muya.getMarkdown()emitted the safe form:Playwright result:
Full
@muyajs/coretest note:The remaining failure was an unrelated existing 5s timeout in
src/ui/tableChessboard/__tests__/tableChessboard.spec.tswhen importing the package entrypoint.Notes for reviewers [optional]
The root cause is a serializer/parser ambiguity around setext headings.
The editor can hold this state after normal editing:
If serialized without a separator, the first nested empty dash marker is immediately below the parent paragraph line:
When parsed again, that first
-can be consumed as a setext heading underline fora. This changes the document structure and drops one nested empty item.The fix is intentionally narrow and lives in
packages/muya/src/state/stateToMarkdown.ts. When a non-empty paragraph is followed by a bullet list whose marker is-and whose first item is empty, the serializer forces a blank line before that nested list. Normal non-empty nested lists are left unchanged.Regression coverage was added in
packages/muya/src/state/__tests__/listSerialization.spec.ts.By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.