feat(word-export): enhance Word document generation with alignment options and normalization for CJK text
Frontend CI / Frontend (push) Has been cancelled
Frontend CI / Frontend (push) Has been cancelled
This commit is contained in:
@@ -60,6 +60,10 @@ type MarkdownTokenLike = {
|
|||||||
|
|
||||||
type DocxModule = typeof import('docx')
|
type DocxModule = typeof import('docx')
|
||||||
|
|
||||||
|
type BrowserSetImmediate = (handler: (...args: unknown[]) => void, ...args: unknown[]) => number
|
||||||
|
|
||||||
|
type BrowserClearImmediate = (handle?: number) => void
|
||||||
|
|
||||||
type WordTextStyle = {
|
type WordTextStyle = {
|
||||||
bold?: boolean
|
bold?: boolean
|
||||||
italics?: boolean
|
italics?: boolean
|
||||||
@@ -258,6 +262,7 @@ async function buildWordDocumentBlob(
|
|||||||
markdown: string,
|
markdown: string,
|
||||||
untitledLabel: string,
|
untitledLabel: string,
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
|
return withWordExportScheduler(async () => {
|
||||||
const docx = await import('docx')
|
const docx = await import('docx')
|
||||||
const documentTitle = title || untitledLabel
|
const documentTitle = title || untitledLabel
|
||||||
const children = await buildWordChildren(docx, title, markdown)
|
const children = await buildWordChildren(docx, title, markdown)
|
||||||
@@ -328,6 +333,7 @@ async function buildWordDocumentBlob(
|
|||||||
},
|
},
|
||||||
paragraph: {
|
paragraph: {
|
||||||
spacing: { after: 160, line: 360 },
|
spacing: { after: 160, line: 360 },
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
heading1: {
|
heading1: {
|
||||||
@@ -340,6 +346,7 @@ async function buildWordDocumentBlob(
|
|||||||
paragraph: {
|
paragraph: {
|
||||||
spacing: { before: 260, after: 180 },
|
spacing: { before: 260, after: 180 },
|
||||||
keepNext: true,
|
keepNext: true,
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
heading2: {
|
heading2: {
|
||||||
@@ -352,6 +359,7 @@ async function buildWordDocumentBlob(
|
|||||||
paragraph: {
|
paragraph: {
|
||||||
spacing: { before: 240, after: 140 },
|
spacing: { before: 240, after: 140 },
|
||||||
keepNext: true,
|
keepNext: true,
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
heading3: {
|
heading3: {
|
||||||
@@ -364,6 +372,7 @@ async function buildWordDocumentBlob(
|
|||||||
paragraph: {
|
paragraph: {
|
||||||
spacing: { before: 200, after: 120 },
|
spacing: { before: 200, after: 120 },
|
||||||
keepNext: true,
|
keepNext: true,
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hyperlink: {
|
hyperlink: {
|
||||||
@@ -386,6 +395,7 @@ async function buildWordDocumentBlob(
|
|||||||
paragraph: {
|
paragraph: {
|
||||||
spacing: { after: 260 },
|
spacing: { after: 260 },
|
||||||
keepNext: true,
|
keepNext: true,
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -412,6 +422,41 @@ async function buildWordDocumentBlob(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return docx.Packer.toBlob(document)
|
return docx.Packer.toBlob(document)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withWordExportScheduler<T>(work: () => Promise<T>): Promise<T> {
|
||||||
|
const scope = globalThis as typeof globalThis & {
|
||||||
|
setImmediate?: BrowserSetImmediate
|
||||||
|
clearImmediate?: BrowserClearImmediate
|
||||||
|
}
|
||||||
|
const hadSetImmediate = Object.prototype.hasOwnProperty.call(scope, 'setImmediate')
|
||||||
|
const hadClearImmediate = Object.prototype.hasOwnProperty.call(scope, 'clearImmediate')
|
||||||
|
const previousSetImmediate = scope.setImmediate
|
||||||
|
const previousClearImmediate = scope.clearImmediate
|
||||||
|
|
||||||
|
scope.setImmediate = (handler: (...args: unknown[]) => void, ...args: unknown[]) =>
|
||||||
|
window.setTimeout(() => handler(...args), 0)
|
||||||
|
scope.clearImmediate = (handle?: number) => {
|
||||||
|
if (typeof handle === 'number') {
|
||||||
|
window.clearTimeout(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await work()
|
||||||
|
} finally {
|
||||||
|
if (hadSetImmediate) {
|
||||||
|
scope.setImmediate = previousSetImmediate
|
||||||
|
} else {
|
||||||
|
delete scope.setImmediate
|
||||||
|
}
|
||||||
|
if (hadClearImmediate) {
|
||||||
|
scope.clearImmediate = previousClearImmediate
|
||||||
|
} else {
|
||||||
|
delete scope.clearImmediate
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildWordChildren(
|
async function buildWordChildren(
|
||||||
@@ -436,7 +481,7 @@ async function buildWordChildren(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!children.length) {
|
if (!children.length) {
|
||||||
children.push(new docx.Paragraph(''))
|
children.push(new docx.Paragraph({ alignment: docx.AlignmentType.LEFT }))
|
||||||
}
|
}
|
||||||
|
|
||||||
return children
|
return children
|
||||||
@@ -454,6 +499,7 @@ async function buildWordBlock(
|
|||||||
return [
|
return [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
style: 'ArticleTitle',
|
style: 'ArticleTitle',
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: await buildWordInlineChildren(docx, element),
|
children: await buildWordInlineChildren(docx, element),
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
@@ -462,6 +508,7 @@ async function buildWordBlock(
|
|||||||
if (tagName === 'h1' || tagName === 'h2' || tagName === 'h3') {
|
if (tagName === 'h1' || tagName === 'h2' || tagName === 'h3') {
|
||||||
return [
|
return [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
heading:
|
heading:
|
||||||
tagName === 'h1'
|
tagName === 'h1'
|
||||||
? docx.HeadingLevel.HEADING_1
|
? docx.HeadingLevel.HEADING_1
|
||||||
@@ -476,6 +523,7 @@ async function buildWordBlock(
|
|||||||
if (tagName === 'h4' || tagName === 'h5' || tagName === 'h6') {
|
if (tagName === 'h4' || tagName === 'h5' || tagName === 'h6') {
|
||||||
return [
|
return [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
heading: docx.HeadingLevel.HEADING_3,
|
heading: docx.HeadingLevel.HEADING_3,
|
||||||
children: await buildWordInlineChildren(docx, element),
|
children: await buildWordInlineChildren(docx, element),
|
||||||
}),
|
}),
|
||||||
@@ -494,6 +542,7 @@ async function buildWordBlock(
|
|||||||
const quoteChildren = await buildWordInlineChildren(docx, element, { color: DOCX_MUTED_TEXT_COLOR })
|
const quoteChildren = await buildWordInlineChildren(docx, element, { color: DOCX_MUTED_TEXT_COLOR })
|
||||||
return [
|
return [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: quoteChildren.length ? quoteChildren : [new docx.TextRun('')],
|
children: quoteChildren.length ? quoteChildren : [new docx.TextRun('')],
|
||||||
border: {
|
border: {
|
||||||
left: {
|
left: {
|
||||||
@@ -511,6 +560,7 @@ async function buildWordBlock(
|
|||||||
if (tagName === 'pre') {
|
if (tagName === 'pre') {
|
||||||
return [
|
return [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: [
|
children: [
|
||||||
new docx.TextRun({
|
new docx.TextRun({
|
||||||
text: element.textContent ?? '',
|
text: element.textContent ?? '',
|
||||||
@@ -528,6 +578,7 @@ async function buildWordBlock(
|
|||||||
if (tagName === 'hr') {
|
if (tagName === 'hr') {
|
||||||
return [
|
return [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
border: {
|
border: {
|
||||||
bottom: {
|
bottom: {
|
||||||
color: 'E5E7EB',
|
color: 'E5E7EB',
|
||||||
@@ -557,6 +608,7 @@ async function buildWordBlock(
|
|||||||
const inlineChildren = await buildWordInlineChildren(docx, element)
|
const inlineChildren = await buildWordInlineChildren(docx, element)
|
||||||
return [
|
return [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: inlineChildren.length ? inlineChildren : [new docx.TextRun('')],
|
children: inlineChildren.length ? inlineChildren : [new docx.TextRun('')],
|
||||||
numbering: listState
|
numbering: listState
|
||||||
? { reference: listState.ordered ? 'article-number' : 'article-bullet', level: listState.level }
|
? { reference: listState.ordered ? 'article-number' : 'article-bullet', level: listState.level }
|
||||||
@@ -570,6 +622,7 @@ async function buildWordBlock(
|
|||||||
return paragraphChildren.length
|
return paragraphChildren.length
|
||||||
? [
|
? [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: paragraphChildren,
|
children: paragraphChildren,
|
||||||
spacing: { after: 160, line: 360 },
|
spacing: { after: 160, line: 360 },
|
||||||
}),
|
}),
|
||||||
@@ -607,6 +660,7 @@ async function buildWordList(
|
|||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: inlineRuns.length ? inlineRuns : [new docx.TextRun('')],
|
children: inlineRuns.length ? inlineRuns : [new docx.TextRun('')],
|
||||||
numbering: {
|
numbering: {
|
||||||
reference: ordered ? 'article-number' : 'article-bullet',
|
reference: ordered ? 'article-number' : 'article-bullet',
|
||||||
@@ -670,6 +724,7 @@ async function buildWordTableCell(docx: DocxModule, cell: Element): Promise<Word
|
|||||||
return new docx.TableCell({
|
return new docx.TableCell({
|
||||||
children: [
|
children: [
|
||||||
new docx.Paragraph({
|
new docx.Paragraph({
|
||||||
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: inlineRuns.length ? inlineRuns : [new docx.TextRun('')],
|
children: inlineRuns.length ? inlineRuns : [new docx.TextRun('')],
|
||||||
spacing: { after: 0, line: 300 },
|
spacing: { after: 0, line: 300 },
|
||||||
}),
|
}),
|
||||||
@@ -680,7 +735,7 @@ async function buildWordTableCell(docx: DocxModule, cell: Element): Promise<Word
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyWordTableCell(docx: DocxModule): WordTableCell {
|
function emptyWordTableCell(docx: DocxModule): WordTableCell {
|
||||||
return new docx.TableCell({ children: [new docx.Paragraph('')] })
|
return new docx.TableCell({ children: [new docx.Paragraph({ alignment: docx.AlignmentType.LEFT })] })
|
||||||
}
|
}
|
||||||
|
|
||||||
function wordTableBorder(docx: DocxModule) {
|
function wordTableBorder(docx: DocxModule) {
|
||||||
@@ -786,7 +841,7 @@ async function buildWordImageParagraph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new docx.Paragraph({
|
return new docx.Paragraph({
|
||||||
alignment: docx.AlignmentType.CENTER,
|
alignment: docx.AlignmentType.LEFT,
|
||||||
children: [imageRun],
|
children: [imageRun],
|
||||||
spacing: { before: 100, after: 180 },
|
spacing: { before: 100, after: 180 },
|
||||||
keepLines: true,
|
keepLines: true,
|
||||||
@@ -1024,7 +1079,17 @@ function fitImageSize(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWordText(text: string): string {
|
function normalizeWordText(text: string): string {
|
||||||
return text.replace(/\s+/g, ' ')
|
return normalizeCjkWordText(text.replace(/\s+/g, ' '))
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCjkWordText(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/(?<=[\p{Script=Han}]) (?=[\p{Script=Han}])/gu, '')
|
||||||
|
.replace(/(?<=[\p{Script=Han}]) (?=[,。!?;:、)】》』」])/gu, '')
|
||||||
|
.replace(/(?<=[(【《『「]) (?=[\p{Script=Han}])/gu, '')
|
||||||
|
.replace(/(?<=[,。!?;:、]) (?=[\p{Script=Han}])/gu, '')
|
||||||
|
.replace(/(?<=[\p{Script=Han}]) (?=[a-zA-Z] [\p{Script=Han}])/gu, '')
|
||||||
|
.replace(/(?<=[\p{Script=Han}] [a-zA-Z]) (?=[\p{Script=Han}])/gu, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDirectText(element: Element): string {
|
function getDirectText(element: Element): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user