Preserve DOCX table column spans
This commit is contained in:
@@ -349,6 +349,22 @@ describe("wordOffice helpers", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves HTML table colspan as DOCX gridSpan", async () => {
|
||||
const html = '<table><tr><td colspan="2">Merged</td><td>Tail</td></tr></table>';
|
||||
const blocks = htmlToWordBlocks(html);
|
||||
const blob = await exportDocxBlob("Table colspan.docx", html);
|
||||
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
||||
const documentXml = (await zip.file("word/document.xml")?.async("string")) ?? "";
|
||||
|
||||
expect(blocks).toEqual([
|
||||
{
|
||||
type: "table",
|
||||
rows: [[{ text: "Merged", runs: [{ text: "Merged" }], colSpan: 2 }, "Tail"]]
|
||||
}
|
||||
]);
|
||||
expect(documentXml).toMatch(/<w:gridSpan\b[^>]*w:val="2"[^>]*\/>/);
|
||||
});
|
||||
|
||||
it("сохраняет пустые ячейки и строки таблицы для DOCX", async () => {
|
||||
const html = `
|
||||
<table>
|
||||
|
||||
+31
-6
@@ -25,7 +25,7 @@ export interface WordListItem {
|
||||
runs: WordInlineRun[];
|
||||
}
|
||||
|
||||
export type WordTableCell = string | { text: string; runs: WordInlineRun[] };
|
||||
export type WordTableCell = string | { text: string; runs: WordInlineRun[]; colSpan?: number };
|
||||
|
||||
export interface WordImageBlock {
|
||||
type: "image";
|
||||
@@ -406,9 +406,7 @@ function wordBlockToDocxChildren(block: WordBlock, docx: DocxModule) {
|
||||
new docx.TableRow({
|
||||
children: row.map(
|
||||
(cell) =>
|
||||
new docx.TableCell({
|
||||
children: wordTableCellToDocxParagraphs(cell, docx)
|
||||
})
|
||||
new docx.TableCell(wordTableCellToDocxOptions(cell, docx))
|
||||
)
|
||||
})
|
||||
)
|
||||
@@ -416,6 +414,14 @@ function wordBlockToDocxChildren(block: WordBlock, docx: DocxModule) {
|
||||
];
|
||||
}
|
||||
|
||||
function wordTableCellToDocxOptions(cell: WordTableCell, docx: DocxModule) {
|
||||
const colSpan = typeof cell === "string" ? undefined : cell.colSpan;
|
||||
return {
|
||||
children: wordTableCellToDocxParagraphs(cell, docx),
|
||||
...(colSpan ? { columnSpan: colSpan } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function wordTableCellToDocxParagraphs(cell: WordTableCell, docx: DocxModule) {
|
||||
const runs = typeof cell === "string" ? [{ text: cell }] : cell.runs;
|
||||
return [
|
||||
@@ -662,7 +668,8 @@ 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;
|
||||
const colSpan = normalizedTableSpan(cell.getAttribute("colspan"));
|
||||
return hasFormattedRuns || colSpan ? { text, runs, ...(colSpan ? { colSpan } : {}) } : text;
|
||||
}
|
||||
|
||||
function paragraphElementToBlocks(element: Element, options: { quote?: boolean } = {}): WordBlock[] {
|
||||
@@ -1122,7 +1129,9 @@ function orderedDocxTableToHtml(
|
||||
})
|
||||
.filter(Boolean);
|
||||
const content = paragraphs.length > 0 ? paragraphs.join("") : escapeHtml(orderedDocxCellText(cell));
|
||||
return content ? `<td>${content}</td>` : "<td></td>";
|
||||
const colSpan = orderedDocxTableCellColspan(cell);
|
||||
const attributes = colSpan ? ` colspan="${colSpan}"` : "";
|
||||
return content ? `<td${attributes}>${content}</td>` : `<td${attributes}></td>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -1133,6 +1142,12 @@ function orderedDocxTableToHtml(
|
||||
return { html: rows.length > 0 ? `<table><tbody>${rows.join("")}</tbody></table>` : "", hasImage, hasLineBreak, hasPageBreak, hasTab };
|
||||
}
|
||||
|
||||
function orderedDocxTableCellColspan(cell: unknown) {
|
||||
const properties = firstOrderedElement(orderedElementChildren(cell), "w:tcPr");
|
||||
const gridSpan = firstOrderedElement(orderedElementChildren(properties), "w:gridSpan");
|
||||
return normalizedTableSpan(docxAttribute(orderedElementAttributes(gridSpan), "w:val") || docxAttribute(orderedElementAttributes(gridSpan), "val"));
|
||||
}
|
||||
|
||||
function orderedDocxCellText(cell: unknown) {
|
||||
return orderedDirectElements(cell, "w:p")
|
||||
.flatMap((paragraph) => orderedDocxParagraphInlineRecords(paragraph, {}))
|
||||
@@ -1998,6 +2013,16 @@ function numericAttribute(element: Element, name: string) {
|
||||
return Number.isFinite(value) && value > 0 ? Math.round(value) : undefined;
|
||||
}
|
||||
|
||||
function normalizedTableSpan(value?: string | null) {
|
||||
const raw = value?.trim() ?? "";
|
||||
if (!/^\d+$/.test(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const span = Number.parseInt(raw, 10);
|
||||
return Number.isSafeInteger(span) && span > 1 ? Math.min(span, 63) : undefined;
|
||||
}
|
||||
|
||||
function cssPixelValue(style: string, property: "width" | "height") {
|
||||
const match = new RegExp(`${property}\\s*:\\s*(\\d+(?:\\.\\d+)?)px`, "i").exec(style);
|
||||
if (!match) {
|
||||
|
||||
@@ -312,6 +312,25 @@ describe("wordOffice DOCX import", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("imports DOCX horizontal merged table cells as HTML colspan", async () => {
|
||||
const blob = await exportDocxBlob(
|
||||
"Table colspan.docx",
|
||||
'<table><tr><td colspan="2">Merged</td><td>Tail</td></tr></table>'
|
||||
);
|
||||
const file = new File([blob], "Table colspan.docx", {
|
||||
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
});
|
||||
const imported = await importDocxFile(file);
|
||||
|
||||
expect(imported.html).toContain('<td colspan="2"><p>Merged</p></td>');
|
||||
expect(htmlToWordBlocks(imported.html)).toEqual([
|
||||
{
|
||||
type: "table",
|
||||
rows: [[{ text: "Merged", runs: [{ text: "Merged" }], colSpan: 2 }, "Tail"]]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("imports DOCX bullet and numbered lists from OOXML numbering", async () => {
|
||||
const blob = await exportDocxBlob(
|
||||
"Lists.docx",
|
||||
|
||||
Reference in New Issue
Block a user