diff --git a/packages/prosemirror-markdown/src/lib/markdown-converter.ts b/packages/prosemirror-markdown/src/lib/markdown-converter.ts index e96cbad3..eae8c578 100644 --- a/packages/prosemirror-markdown/src/lib/markdown-converter.ts +++ b/packages/prosemirror-markdown/src/lib/markdown-converter.ts @@ -1492,11 +1492,16 @@ export function convertProseMirrorToMarkdown( } let t = escapeHtmlText(n.text || ""); // #515: wrap `` INNERMOST first (before the array-order mark loop), - // then skip `code` in the loop. Import (`generateJSON`) always yields the - // code mark LAST in the array (canonical order `[emphasis, code]`), so an - // order-sensitive loop would flip `` to `` on - // re-export and break the byte fixpoint. Code-innermost is stable in both - // directions and matches the markdown path (case "text" / run factoring). + // then skip `code` in the loop. The imported mark order is NOT fixed — it + // DEPENDS on the emphasis extension: import (`generateJSON`) yields code + // LAST for bold/italic/strike (`[emphasis, code]`) but code FIRST for the + // `==`-highlight extension (`[code, highlight]`). So we cannot rely on a + // fixed array position; the invariant is instead "wrap `` innermost + // regardless of the imported order". That keeps `` nested inside the + // emphasis tag both directions (preserving the byte fixpoint — an order- + // sensitive loop would flip ``↔`` depending on + // which order it happened to see) and matches the markdown path (case + // "text" / run factoring). if ((n.marks || []).some((m: any) => m.type === "code")) { t = `${t}`; } diff --git a/packages/prosemirror-markdown/test/generative/text-arbitraries.ts b/packages/prosemirror-markdown/test/generative/text-arbitraries.ts index ae15692a..266fd963 100644 --- a/packages/prosemirror-markdown/test/generative/text-arbitraries.ts +++ b/packages/prosemirror-markdown/test/generative/text-arbitraries.ts @@ -143,8 +143,10 @@ export const markedTextRunArb: fc.Arbitrary = fc.oneof( // #515: code COMBINED with a bare-delimiter emphasis mark. The converter nests // the backtick span inside the emphasis delimiters (`` **`x`** ``) and, when // such runs sit adjacent, factors a shared mark or falls back to schema-HTML. - // Mark order is `[emphasis, code]` — the canonical order the HTML->PM import - // yields (code last) — so the P1 semantic round-trip is order-exact. + // Mark order here is `[emphasis, code]` — the order the HTML->PM import yields + // for bold/italic/strike specifically (code last). This is NOT universal: the + // `==`-highlight case below imports code FIRST — so match each case to its own + // imported order for the order-exact P1 round-trip (do not assume a fixed order). fc .tuple(safeTextArb, fc.constantFrom('bold', 'italic', 'strike')) .map(([t, m]) => ({ type: 'text', text: t, marks: [{ type: m }, { type: 'code' }] })),