Support full QWord heading hierarchy in DOCX

This commit is contained in:
Курнат Андрей
2026-06-02 06:43:36 +03:00
parent 1c03ec168c
commit c75efaa766
5 changed files with 52 additions and 21 deletions
+25 -14
View File
@@ -39,8 +39,10 @@ export interface WordParagraphIndent {
hanging?: number;
}
export type WordHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
export type WordBlock =
| { type: "heading"; level: 1 | 2 | 3; text: string; runs: WordInlineRun[]; alignment?: WordParagraphAlignment; indent?: WordParagraphIndent; bookmark?: string }
| { type: "heading"; level: WordHeadingLevel; 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[][] }
@@ -111,7 +113,10 @@ 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 3'] => h3:fresh"
"p[style-name='Heading 3'] => h3:fresh",
"p[style-name='Heading 4'] => h4:fresh",
"p[style-name='Heading 5'] => h5:fresh",
"p[style-name='Heading 6'] => h6:fresh"
];
const xmlParserOptions = {
ignoreAttributes: false,
@@ -239,7 +244,7 @@ export function htmlToWordBlocks(html: string): WordBlock[] {
return;
}
if (tagName === "h1" || tagName === "h2" || tagName === "h3") {
if (isHeadingTagName(tagName)) {
const runs = elementInlineRuns(node);
const text = inlineRunsText(runs);
const alignment = paragraphAlignmentFromElement(node);
@@ -382,20 +387,17 @@ 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 isHeadingTagName(tagName: string): tagName is `h${WordHeadingLevel}` {
return /^h[1-6]$/.test(tagName);
}
function docxHeadingLevel(level: 1 | 2 | 3, docx: DocxModule) {
if (level === 3) {
return docx.HeadingLevel.HEADING_3 ?? docx.HeadingLevel.HEADING_2;
}
function headingLevelFromTagName(tagName: `h${WordHeadingLevel}`): WordHeadingLevel {
return Number.parseInt(tagName.slice(1), 10) as WordHeadingLevel;
}
return level === 1 ? docx.HeadingLevel.HEADING_1 : docx.HeadingLevel.HEADING_2;
function docxHeadingLevel(level: WordHeadingLevel, docx: DocxModule) {
const headingKey = `HEADING_${level}`;
return docx.HeadingLevel[headingKey] ?? docx.HeadingLevel.HEADING_2;
}
function wordInlineRunToTextRunOption(run: WordInlineRun) {
@@ -1025,6 +1027,15 @@ function docxParagraphTagName(properties: XmlRecord) {
if (["heading3", "heading 3"].includes(styleValue)) {
return "h3";
}
if (["heading4", "heading 4"].includes(styleValue)) {
return "h4";
}
if (["heading5", "heading 5"].includes(styleValue)) {
return "h5";
}
if (["heading6", "heading 6"].includes(styleValue)) {
return "h6";
}
return "p";
}