fix(git-sync): complete A1 heading alignment — green suite + nested path (on 5d45f5a8)
QA follow-up on 5d45f5a8: that commit taught the converter to export heading
textAlign (<hN style>) but left the converter-gate heading test still asserting
the OLD dropped behavior (expects a bare '## text'), so jest was RED — the G1
green-suite gate was not actually met. Two gaps closed:
1. Flip the heading KNOWN-DIVERGENCE gate test to assert the round trip now
PRESERVES alignment (exported as <h2 style="text-align:center"> and recovered
on import), matching the shipped converter behavior. Suite is green again.
2. blockToHtml (the nested-container path: heading/paragraph inside a
column/table/callout) still emitted bare <hN>/<p>, dropping textAlign for
nested blocks. Carry the style there too, symmetric with the processNode path.
Also add #7 (table inside a column) and #8 (multi-block table cell) to the
lossless round-trip CORPUS so both survive export->import through the real
editor-ext schema (columns widthMode pre-authored at its normalize fixpoint).
Verified: server jest 193 suites / 2142 tests green, git-sync vitest 704 green,
no type errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -248,6 +248,67 @@ const CORPUS: Record<string, any> = {
|
||||
],
|
||||
}),
|
||||
|
||||
// #8 — a table with a MULTI-BLOCK cell (two paragraphs). A GFM pipe table
|
||||
// cannot hold two blocks without flattening them; the converter emits a
|
||||
// lossless HTML <table> instead, and the two blocks must survive the round trip.
|
||||
'table (multi-block cell, #8)': doc({
|
||||
type: 'table',
|
||||
content: [
|
||||
{
|
||||
type: 'tableRow',
|
||||
content: [
|
||||
{
|
||||
type: 'tableHeader',
|
||||
attrs: { colspan: 1, rowspan: 1, colwidth: null },
|
||||
content: [para(text('H'))],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tableRow',
|
||||
content: [
|
||||
{
|
||||
type: 'tableCell',
|
||||
attrs: { colspan: 1, rowspan: 1, colwidth: null },
|
||||
content: [para(text('first')), para(text('second'))],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
// #7 — a table nested inside a column. Columns render as HTML containers, and a
|
||||
// table inside one must stay an HTML <table> (a GFM pipe table cannot live
|
||||
// inside an HTML block), round-tripping without being unwrapped or lost.
|
||||
// `widthMode` is pre-authored at its materialized `normal` default (SPEC §11).
|
||||
'table inside a column (#7)': doc({
|
||||
type: 'columns',
|
||||
attrs: { layout: 'two', widthMode: 'normal' },
|
||||
content: [
|
||||
{
|
||||
type: 'column',
|
||||
content: [
|
||||
{
|
||||
type: 'table',
|
||||
content: [
|
||||
{
|
||||
type: 'tableRow',
|
||||
content: [
|
||||
{
|
||||
type: 'tableHeader',
|
||||
attrs: { colspan: 1, rowspan: 1, colwidth: null },
|
||||
content: [para(text('C7'))],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'column', content: [para(text('right'))] },
|
||||
],
|
||||
}),
|
||||
|
||||
// --- editor-ext nodes/marks beyond the original corpus (item #7) ----------
|
||||
// Each of these was verified to round-trip CLEANLY through the real gate
|
||||
// (export -> markdown -> import -> editor-ext Yjs write path). Fixtures are
|
||||
@@ -457,21 +518,14 @@ describe('git-sync converter §13.1 image dimensions preserved (was KNOWN DIVERG
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KNOWN DIVERGENCE — HEADING text alignment (item #7; isolated, not silently
|
||||
// dropped). PARAGRAPH alignment now round-trips (exported as
|
||||
// `<p style="text-align:...">`, re-parsed by the paragraph parseHTML) and lives
|
||||
// in the green CORPUS above; only HEADING alignment still diverges:
|
||||
//
|
||||
// • A heading's `textAlign` is NOT exported at all — a heading emits plain
|
||||
// markdown `## text` with no alignment syntax — so any non-default heading
|
||||
// alignment is dropped on a full round trip.
|
||||
//
|
||||
// If the converter is ever taught to export + re-parse heading alignment, this
|
||||
// assertion flips and an aligned-heading fixture should be promoted into the
|
||||
// green CORPUS above.
|
||||
// HEADING text alignment — now round-trips (item A1; formerly a KNOWN DIVERGENCE).
|
||||
// Symmetric with the paragraph fix: a heading's non-default `textAlign` is
|
||||
// exported as a styled `<hN style="text-align:…">` (was a bare ATX `## text`
|
||||
// that dropped it) and re-parsed by the heading + textAlign parseHTML on import,
|
||||
// so a non-default heading alignment SURVIVES a full round trip.
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('git-sync converter §13.1 KNOWN DIVERGENCE (heading text alignment dropped)', () => {
|
||||
it('drops a heading textAlign (headings do not export alignment at all)', async () => {
|
||||
describe('git-sync converter §13.1 heading text alignment round-trips', () => {
|
||||
it('preserves a heading textAlign across the markdown round trip', async () => {
|
||||
const alignedHeading = doc({
|
||||
type: 'heading',
|
||||
attrs: { level: 2, textAlign: 'center' },
|
||||
@@ -480,9 +534,11 @@ describe('git-sync converter §13.1 KNOWN DIVERGENCE (heading text alignment dro
|
||||
|
||||
const { md, canonNormalized } = await runGate(alignedHeading);
|
||||
|
||||
// Export is a plain markdown heading — no alignment syntax.
|
||||
expect(md.trim()).toBe('## centered heading');
|
||||
expect(docsCanonicallyEqual(alignedHeading, canonNormalized)).toBe(false);
|
||||
// Export is a styled <h2> (was a lossy bare `## centered heading`).
|
||||
expect(md.trim()).toBe(
|
||||
'<h2 style="text-align:center">centered heading</h2>',
|
||||
);
|
||||
expect(docsCanonicallyEqual(alignedHeading, canonNormalized)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -955,11 +955,28 @@ export function convertProseMirrorToMarkdown(content: any): string {
|
||||
const blockToHtml = (block: any): string => {
|
||||
const children = block.content || [];
|
||||
switch (block.type) {
|
||||
case "paragraph":
|
||||
return `<p>${inlineToHtml(children)}</p>`;
|
||||
case "paragraph": {
|
||||
// Carry textAlign here too (symmetric with the processNode paragraph
|
||||
// case): a paragraph nested inside an HTML container (column/table/
|
||||
// callout) would otherwise drop its alignment on the round trip.
|
||||
const pAlign = block.attrs?.textAlign;
|
||||
const pStyle =
|
||||
pAlign && pAlign !== "left"
|
||||
? ` style="text-align:${escapeAttr(pAlign)}"`
|
||||
: "";
|
||||
return `<p${pStyle}>${inlineToHtml(children)}</p>`;
|
||||
}
|
||||
case "heading": {
|
||||
const level = block.attrs?.level || 1;
|
||||
return `<h${level}>${inlineToHtml(children)}</h${level}>`;
|
||||
// Same for a heading nested in an HTML container: emit the alignment as
|
||||
// an inline style (symmetric with the processNode heading case) so it is
|
||||
// not silently dropped. Clamp the level to a valid HTML heading tag.
|
||||
const level = Math.min(6, Math.max(1, block.attrs?.level || 1));
|
||||
const hAlign = block.attrs?.textAlign;
|
||||
const hStyle =
|
||||
hAlign && hAlign !== "left"
|
||||
? ` style="text-align:${escapeAttr(hAlign)}"`
|
||||
: "";
|
||||
return `<h${level}${hStyle}>${inlineToHtml(children)}</h${level}>`;
|
||||
}
|
||||
case "bulletList":
|
||||
return `<ul>${children
|
||||
|
||||
Reference in New Issue
Block a user