Preserve QWord table cell inline formatting

This commit is contained in:
Курнат Андрей
2026-06-02 09:12:40 +03:00
parent e5537b726e
commit ddb04c32a4
2 changed files with 54 additions and 7 deletions
+33
View File
@@ -376,6 +376,39 @@ describe("wordOffice helpers", () => {
expect(documentXml).toContain("Итог"); expect(documentXml).toContain("Итог");
}); });
it("сохраняет inline-форматирование ячеек таблицы для DOCX", async () => {
const html = `
<table>
<tr>
<td><strong>Итог</strong> <em>готов</em></td>
<td><a href="https://example.com/report">Отчет</a></td>
</tr>
</table>
`;
const blocks = htmlToWordBlocks(html);
const blob = await exportDocxBlob("Формат таблицы.docx", html);
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
const documentXml = (await zip.file("word/document.xml")?.async("string")) ?? "";
const relationshipsXml = (await zip.file("word/_rels/document.xml.rels")?.async("string")) ?? "";
expect(blocks).toEqual([
{
type: "table",
rows: [
[
{ text: "Итог готов", runs: [{ text: "Итог", bold: true }, { text: " " }, { text: "готов", italics: true }] },
{ text: "Отчет", runs: [{ text: "Отчет", href: "https://example.com/report" }] }
]
]
}
]);
expect(documentXml).toContain("<w:tbl>");
expect(documentXml).toContain("<w:b/>");
expect(documentXml).toContain("<w:i/>");
expect(documentXml).toContain("<w:hyperlink");
expect(relationshipsXml).toContain('Target="https://example.com/report"');
});
it("сохраняет размер шрифта из HTML inline-стилей для DOCX", () => { it("сохраняет размер шрифта из HTML inline-стилей для DOCX", () => {
const blocks = htmlToWordBlocks(` const blocks = htmlToWordBlocks(`
<p>Normal <span style="font-size: 18pt;">Large</span> <span style="font-size: 24px;">Pixel size</span></p> <p>Normal <span style="font-size: 18pt;">Large</span> <span style="font-size: 24px;">Pixel size</span></p>
+21 -7
View File
@@ -25,6 +25,8 @@ export interface WordListItem {
runs: WordInlineRun[]; runs: WordInlineRun[];
} }
export type WordTableCell = string | { text: string; runs: WordInlineRun[] };
export interface WordImageBlock { export interface WordImageBlock {
type: "image"; type: "image";
src: string; src: string;
@@ -74,7 +76,7 @@ export type WordBlock =
quote?: boolean; quote?: boolean;
} }
| { type: "list"; ordered: boolean; items: WordListItem[] } | { type: "list"; ordered: boolean; items: WordListItem[] }
| { type: "table"; rows: string[][] } | { type: "table"; rows: WordTableCell[][] }
| WordImageBlock; | WordImageBlock;
type MammothModule = { type MammothModule = {
@@ -405,11 +407,7 @@ function wordBlockToDocxChildren(block: WordBlock, docx: DocxModule) {
children: row.map( children: row.map(
(cell) => (cell) =>
new docx.TableCell({ new docx.TableCell({
children: [ children: wordTableCellToDocxParagraphs(cell, docx)
new docx.Paragraph({
children: [new docx.TextRun(cell)]
})
]
}) })
) )
}) })
@@ -418,6 +416,15 @@ function wordBlockToDocxChildren(block: WordBlock, docx: DocxModule) {
]; ];
} }
function wordTableCellToDocxParagraphs(cell: WordTableCell, docx: DocxModule) {
const runs = typeof cell === "string" ? [{ text: cell }] : cell.runs;
return [
new docx.Paragraph({
children: wordInlineRunsToTextRuns(runs, docx)
})
];
}
export function wordInlineRunsToTextRunOptions(runs: WordInlineRun[]) { export function wordInlineRunsToTextRunOptions(runs: WordInlineRun[]) {
return runs.map(wordInlineRunToTextRunOption); return runs.map(wordInlineRunToTextRunOption);
} }
@@ -647,10 +654,17 @@ function parseHtmlFragment(html: string) {
function tableRows(table: Element) { function tableRows(table: Element) {
return Array.from(table.querySelectorAll("tr")) return Array.from(table.querySelectorAll("tr"))
.map((row) => Array.from(row.querySelectorAll("th,td")).map(elementText)) .map((row) => Array.from(row.querySelectorAll("th,td")).map(tableCellValue))
.filter((row) => row.length > 0); .filter((row) => row.length > 0);
} }
function tableCellValue(cell: Element): WordTableCell {
const runs = elementInlineRuns(cell);
const text = inlineRunsText(runs);
const hasFormattedRuns = runs.some((run) => Object.keys(cleanInlineFormat(run)).length > 0);
return hasFormattedRuns ? { text, runs } : text;
}
function paragraphElementToBlocks(element: Element, options: { quote?: boolean } = {}): WordBlock[] { function paragraphElementToBlocks(element: Element, options: { quote?: boolean } = {}): WordBlock[] {
const segments = collectInlineSegments(Array.from(element.childNodes), {}); const segments = collectInlineSegments(Array.from(element.childNodes), {});
const alignment = paragraphAlignmentFromElement(element); const alignment = paragraphAlignmentFromElement(element);