Merge pull request 'fix(prosemirror-markdown): hardBreak переживает round-trip (trailing/consecutive/table-cell) (#549)' (#553) from fix/549-hardbreak-md-canon into develop
Reviewed-on: #553
This commit was merged in pull request #553.
This commit is contained in:
@@ -987,9 +987,14 @@ export function convertProseMirrorToMarkdown(
|
||||
// like a paragraph followed by a list don't collide into "line1- a".
|
||||
// Then collapse newlines and escape pipes so a cell containing "|" or a
|
||||
// line break cannot corrupt the surrounding GFM row.
|
||||
// #549: a GFM pipe cell is single-line — the generic ` \n`/newline
|
||||
// collapse below flattens a hardBreak into a space, dropping it. Convert
|
||||
// the two-space hardBreak marker to `<br>` FIRST (GFM cells allow inline
|
||||
// `<br>`, which re-imports to a hardBreak) so intra-cell breaks survive.
|
||||
return nodeContent
|
||||
.map(processNode)
|
||||
.join(" ")
|
||||
.replace(/ {2}\n/g, "<br>")
|
||||
.replace(/\r?\n/g, " ")
|
||||
.replace(/\|/g, "\\|");
|
||||
}
|
||||
@@ -1338,6 +1343,27 @@ export function convertProseMirrorToMarkdown(
|
||||
parts[i] = mathInlineHtml(nodes[i].attrs?.text || "");
|
||||
}
|
||||
}
|
||||
// #549: position-aware hardBreak. The two-space ` \n` form (emitted by the
|
||||
// processNode hardBreak case) only re-imports to a hardBreak when it is
|
||||
// FOLLOWED by same-paragraph text. It is silently lost when the break is
|
||||
// (a) the last inline child — the top-level `.trim()` strips the trailing
|
||||
// ` \n`, and even a non-last paragraph's trailing break sits before the
|
||||
// `\n\n` block gap — or (b) immediately followed by another hardBreak — the
|
||||
// intervening whitespace-only line reads as a paragraph separator on
|
||||
// re-parse, splitting the run. Emit `<br>` there instead: marked passes the
|
||||
// inline HTML break through and generateJSON rebuilds a hardBreak in every
|
||||
// position. A safe mid-paragraph break keeps the ` \n` form so the existing
|
||||
// golden output stays byte-stable. Skipped inside footnote bodies, whose
|
||||
// `^[…]` inline form collapses breaks to spaces (unchanged; already lossy).
|
||||
if (!inFootnoteBody) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i]?.type !== "hardBreak") continue;
|
||||
const next = nodes[i + 1];
|
||||
if (!next || next.type === "hardBreak" || (parts[i + 1] ?? "") === "") {
|
||||
parts[i] = "<br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join("");
|
||||
};
|
||||
|
||||
|
||||
@@ -295,3 +295,44 @@ describe('git-sync converter: lose-prone atoms keep their VALUES across a round
|
||||
expect(allText(src)).toContain('shared body');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #549: hardBreak COUNT/position preservation across a pm -> md -> pm round
|
||||
// trip. The FIXTURES block above only proves a mid-paragraph hardBreak survives
|
||||
// as a node; these pin the three sub-cases whose ` \n` two-space form was
|
||||
// silently dropped on re-import (trailing / consecutive / table cell), plus a
|
||||
// mid-paragraph regression guard so the byte-stable ` \n` form is not lost.
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('#549: hardBreak survives every round-trip position', () => {
|
||||
const HB = { type: 'hardBreak' };
|
||||
const countHardBreaks = (n: any): number => {
|
||||
let c = n?.type === 'hardBreak' ? 1 : 0;
|
||||
if (Array.isArray(n?.content)) for (const ch of n.content) c += countHardBreaks(ch);
|
||||
return c;
|
||||
};
|
||||
const roundTripCount = async (d: any): Promise<number> =>
|
||||
countHardBreaks(await markdownToProseMirror(convertProseMirrorToMarkdown(d)));
|
||||
|
||||
it('mid-paragraph break is still preserved (regression guard)', async () => {
|
||||
expect(await roundTripCount(doc(P(T('a'), HB, T('b'))))).toBe(1);
|
||||
});
|
||||
|
||||
it('(1) trailing break at the end of the last paragraph survives', async () => {
|
||||
expect(await roundTripCount(doc(P(T('a'), HB)))).toBe(1);
|
||||
});
|
||||
|
||||
it('(2) two consecutive breaks in one paragraph survive', async () => {
|
||||
expect(await roundTripCount(doc(P(T('a'), HB, HB, T('b'))))).toBe(2);
|
||||
});
|
||||
|
||||
it('(3) a break inside a table cell survives', async () => {
|
||||
const table = doc({
|
||||
type: 'table',
|
||||
content: [
|
||||
{ type: 'tableRow', content: [{ type: 'tableHeader', content: [P(T('h'))] }] },
|
||||
{ type: 'tableRow', content: [{ type: 'tableCell', content: [P(T('a'), HB, T('b'))] }] },
|
||||
],
|
||||
});
|
||||
expect(await roundTripCount(table)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user