diff --git a/static/js/shared.js b/static/js/shared.js index 119dd3f..7c88780 100644 --- a/static/js/shared.js +++ b/static/js/shared.js @@ -2,6 +2,10 @@ const tags = ['left', 'center', 'justify', 'right']; +// Match `text-align: ` in a CSS style attribute. The leading +// boundary keeps us from accidentally matching `vertical-align` etc. +const STYLE_TEXT_ALIGN_RE = /(?:^|;|\s)text-align\s*:\s*(left|center|right|justify)\b/i; + exports.collectContentPre = (hookName, context, cb) => { const tname = context.tname; const state = context.state; @@ -13,6 +17,17 @@ exports.collectContentPre = (hookName, context, cb) => { if (tagIndex >= 0) { lineAttributes.align = tags[tagIndex]; } + // Pick up `style="text-align:..."` on imported HTML so a round-trip + // through HTML or DOCX preserves alignment. Etherpad core's + // `getLineHTMLForExport` already serializes the alignment to an + // inline style on `

` / ``, but without this the importer + // dropped it on the way back. + if (context.styl) { + const m = STYLE_TEXT_ALIGN_RE.exec(context.styl); + if (m) { + lineAttributes.align = m[1].toLowerCase(); + } + } return cb(); }; diff --git a/static/tests/backend/specs/exportHTML.ts b/static/tests/backend/specs/exportHTML.ts index ec0ab46..333050a 100644 --- a/static/tests/backend/specs/exportHTML.ts +++ b/static/tests/backend/specs/exportHTML.ts @@ -146,6 +146,33 @@ describe('export alignment to HTML', function () { }); }); + context('when imported HTML uses style="text-align"', function () { + // Regression for the round-trip: getLineHTMLForExport already emits + //

on export, so importing that HTML (or + // any HTML produced by mammoth from a DOCX) needs collectContentPre + // to read the style attribute. Without the fix, alignment was lost + // on import even though the legacy /

// + // tags worked. + for (const align of ['left', 'center', 'right', 'justify']) { + it(`preserves text-align:${align} from inline style`, async function () { + // Set the pad HTML using `style="text-align:..."` directly. + const padHtml = `

Hello world

`; + const newPadID = randomString(5); + await createPad(newPadID); + await setHTML(newPadID, buildHTML(padHtml)); + const res = await agent.get(getHTMLEndPointFor(newPadID)) + .set('Authorization', await generateJWTToken()); + const html = res.body.data.html; + const expected = + new RegExp(`

Hello world

`); + if (html.search(expected) === -1) { + throw new Error( + `Expected ${expected} in re-exported HTML, got: ${html}`); + } + }); + } + }); + context('when pad text is heading', function () { before(async function () { html = () => buildHTML('

Hello world

');