From 08fe16bdf61209a99a1f581be36c68ca23aace0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Mon, 1 Jun 2026 05:41:46 +0300 Subject: [PATCH] Import QWord DOCX paragraph alignment --- README.md | 2 +- src/io/wordOffice.ts | 13 +++++++++---- src/io/wordOfficeImport.test.ts | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c4344ba..5950eaf 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ npm run build ## Поддержка форматов -- QWord открывает `.docx` и сохраняет `.docx`; при импорте базовых DOCX-абзацев читает явные цвета текста и подсветку из OOXML, а при экспорте DOCX сохраняются заголовки, абзацы, выравнивание абзацев, списки, простые таблицы, внешние гиперссылки, базовые растровые изображения и базовое inline-форматирование текста: полужирный, курсив, подчеркивание, цвет текста и подсветка. +- QWord открывает `.docx` и сохраняет `.docx`; при импорте базовых DOCX-абзацев читает выравнивание, явные цвета текста и подсветку из OOXML, а при экспорте DOCX сохраняются заголовки, абзацы, выравнивание абзацев, списки, простые таблицы, внешние гиперссылки, базовые растровые изображения и базовое inline-форматирование текста: полужирный, курсив, подчеркивание, цвет текста и подсветка. - QExcell открывает `.xlsx` и сохраняет `.xlsx`; сетка расширяется по фактически заполненным ячейкам, поддерживает несколько листов, формулы за пределами стартового диапазона A-H, базовые merged cells, внешние гиперссылки ячеек, базовые диаграммы листа и базовое форматирование ячеек: полужирный, курсив, подчеркивание, заливку и числовые форматы XLSX для дат, процентов, денежных и дробных значений. - QPowerPoint открывает `.pptx` и сохраняет `.pptx`; импорт учитывает порядок слайдов из `presentation.xml`, читает заметки докладчика через relationships слайда и сохраняет базовые растровые изображения слайдов. - QOutlook открывает `.eml` и одиночные сообщения Outlook `.msg`, сохраняет письма в `.eml`; импорт EML поддерживает простые текстовые письма, `multipart/alternative`, `multipart/mixed`, `text/plain`, fallback из `text/html` в читаемый текст, base64, quoted-printable и MIME-вложения. Импорт MSG читает тему, отправителя, получателей, текст/HTML body и вложения, если эти поля доступны в MSG-файле. Новые письма можно сохранять в EML вместе с добавленными файлами. `.pst` и экспорт в `.msg` пока не реализованы. diff --git a/src/io/wordOffice.ts b/src/io/wordOffice.ts index a07a455..d80c830 100644 --- a/src/io/wordOffice.ts +++ b/src/io/wordOffice.ts @@ -556,7 +556,7 @@ async function styledDocxHtmlFromArrayBuffer(arrayBuffer: ArrayBuffer) { const parser = new XMLParser(xmlParserOptions); const parsed = parser.parse(documentXml); const result = parsedDocxDocumentToHtml(parsed); - return result.hasInlineColorOrHighlight ? result.html : ""; + return result.hasInlineColorOrHighlight || result.hasParagraphAlignment ? result.html : ""; } catch { return ""; } @@ -570,17 +570,20 @@ function parsedDocxDocumentToHtml(documentXml: unknown) { const document = childRecord(documentXml, "w:document"); const body = childRecord(document, "w:body"); let hasInlineColorOrHighlight = false; + let hasParagraphAlignment = false; const paragraphs = toArray(body["w:p"]) .map((paragraph) => { const result = docxParagraphToHtml(paragraph); hasInlineColorOrHighlight = hasInlineColorOrHighlight || result.hasInlineColorOrHighlight; + hasParagraphAlignment = hasParagraphAlignment || result.hasParagraphAlignment; return result.html; }) .filter(Boolean); return { html: paragraphs.join(""), - hasInlineColorOrHighlight + hasInlineColorOrHighlight, + hasParagraphAlignment }; } @@ -589,6 +592,7 @@ function docxParagraphToHtml(paragraph: unknown) { const properties = childRecord(record, "w:pPr"); const tagName = docxParagraphTagName(properties); const style = docxParagraphStyleAttribute(properties); + const hasParagraphAlignment = Boolean(style); let hasInlineColorOrHighlight = false; const runs = docxParagraphInlineRecords(record).map((run) => { const result = docxRunToHtml(run); @@ -598,12 +602,13 @@ function docxParagraphToHtml(paragraph: unknown) { const content = runs.join(""); if (!content.trim()) { - return { html: "", hasInlineColorOrHighlight }; + return { html: "", hasInlineColorOrHighlight, hasParagraphAlignment }; } return { html: `<${tagName}${style ? ` style="${style}"` : ""}>${content}`, - hasInlineColorOrHighlight + hasInlineColorOrHighlight, + hasParagraphAlignment }; } diff --git a/src/io/wordOfficeImport.test.ts b/src/io/wordOfficeImport.test.ts index efa7bf8..2e429ba 100644 --- a/src/io/wordOfficeImport.test.ts +++ b/src/io/wordOfficeImport.test.ts @@ -20,4 +20,24 @@ describe("wordOffice DOCX import", () => { } ]); }); + + it("imports DOCX paragraph alignment from OOXML", async () => { + const blob = await exportDocxBlob( + "Alignment.docx", + '

Centered

Right

Justified

' + ); + const file = new File([blob], "Alignment.docx", { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + }); + const imported = await importDocxFile(file); + + expect(imported.html).toContain('style="text-align: center;"'); + expect(imported.html).toContain('style="text-align: right;"'); + expect(imported.html).toContain('style="text-align: justify;"'); + expect(htmlToWordBlocks(imported.html)).toEqual([ + { type: "paragraph", text: "Centered", runs: [{ text: "Centered" }], alignment: "center" }, + { type: "paragraph", text: "Right", runs: [{ text: "Right" }], alignment: "right" }, + { type: "paragraph", text: "Justified", runs: [{ text: "Justified" }], alignment: "both" } + ]); + }); });