Preserve QWord table cell inline formatting
This commit is contained in:
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user