Support QWord Heading 3 DOCX roundtrip

This commit is contained in:
Курнат Андрей
2026-06-02 06:37:49 +03:00
parent c4fd683ef3
commit 1c03ec168c
5 changed files with 46 additions and 6 deletions
+25 -5
View File
@@ -40,7 +40,7 @@ export interface WordParagraphIndent {
}
export type WordBlock =
| { type: "heading"; level: 1 | 2; text: string; runs: WordInlineRun[]; alignment?: WordParagraphAlignment; indent?: WordParagraphIndent; bookmark?: string }
| { type: "heading"; level: 1 | 2 | 3; text: string; runs: WordInlineRun[]; alignment?: WordParagraphAlignment; indent?: WordParagraphIndent; bookmark?: string }
| { type: "paragraph"; text: string; runs: WordInlineRun[]; alignment?: WordParagraphAlignment; indent?: WordParagraphIndent; bookmark?: string }
| { type: "list"; ordered: boolean; items: WordListItem[] }
| { type: "table"; rows: string[][] }
@@ -110,7 +110,8 @@ type DocxListInfo = { ordered: boolean; level: number; key: string };
const mammothStyleMap = [
"p[style-name='Title'] => h1:fresh",
"p[style-name='Heading 1'] => h1:fresh",
"p[style-name='Heading 2'] => h2:fresh"
"p[style-name='Heading 2'] => h2:fresh",
"p[style-name='Heading 3'] => h3:fresh"
];
const xmlParserOptions = {
ignoreAttributes: false,
@@ -238,7 +239,7 @@ export function htmlToWordBlocks(html: string): WordBlock[] {
return;
}
if (tagName === "h1" || tagName === "h2") {
if (tagName === "h1" || tagName === "h2" || tagName === "h3") {
const runs = elementInlineRuns(node);
const text = inlineRunsText(runs);
const alignment = paragraphAlignmentFromElement(node);
@@ -247,7 +248,7 @@ export function htmlToWordBlocks(html: string): WordBlock[] {
if (text) {
blocks.push({
type: "heading",
level: tagName === "h1" ? 1 : 2,
level: headingLevelFromTagName(tagName),
text,
runs,
...(alignment ? { alignment } : {}),
@@ -299,7 +300,7 @@ function wordBlockToDocxChildren(block: WordBlock, docx: DocxModule) {
const children = wordInlineRunsToTextRuns(block.runs, docx);
return [
new docx.Paragraph({
heading: block.level === 1 ? docx.HeadingLevel.HEADING_1 : docx.HeadingLevel.HEADING_2,
heading: docxHeadingLevel(block.level, docx),
spacing: { after: 160 },
...docxAlignmentOption(block.alignment, docx),
...docxIndentOption(block.indent),
@@ -381,6 +382,22 @@ export function wordInlineRunsToTextRunOptions(runs: WordInlineRun[]) {
return runs.map(wordInlineRunToTextRunOption);
}
function headingLevelFromTagName(tagName: string): 1 | 2 | 3 {
if (tagName === "h3") {
return 3;
}
return tagName === "h2" ? 2 : 1;
}
function docxHeadingLevel(level: 1 | 2 | 3, docx: DocxModule) {
if (level === 3) {
return docx.HeadingLevel.HEADING_3 ?? docx.HeadingLevel.HEADING_2;
}
return level === 1 ? docx.HeadingLevel.HEADING_1 : docx.HeadingLevel.HEADING_2;
}
function wordInlineRunToTextRunOption(run: WordInlineRun) {
const options: Record<string, unknown> = { text: run.text };
if (run.bold) {
@@ -1005,6 +1022,9 @@ function docxParagraphTagName(properties: XmlRecord) {
if (["heading2", "heading 2"].includes(styleValue)) {
return "h2";
}
if (["heading3", "heading 3"].includes(styleValue)) {
return "h3";
}
return "p";
}