-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathImportDocxNative.ts
More file actions
83 lines (78 loc) · 3.12 KB
/
Copy pathImportDocxNative.ts
File metadata and controls
83 lines (78 loc) · 3.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
'use strict';
const mammoth = require('mammoth');
const JSZip = require('jszip');
// mammoth strips paragraph alignment (<w:jc>) when it converts a docx to
// HTML; it has no equivalent style-mapping for justification. To keep
// alignment through the round-trip we walk the docx's document.xml
// directly, pull the `w:val` from each `<w:p>`'s `<w:jc>`, and inject a
// matching `style="text-align:..."` onto the corresponding block element
// in mammoth's output. Match is by document order: the Nth `<p>` /
// `<h1>...<h6>` in mammoth's output corresponds to the Nth `<w:p>` in
// the docx.
const PARA_RE = /<w:p\b[^>]*>([\s\S]*?)<\/w:p>/g;
const JC_RE = /<w:jc\s+w:val=["']([^"']+)["']/;
// Word's `w:jc` accepts more values than CSS text-align; map the ones
// we want to surface and skip the rest.
const JC_TO_CSS: Record<string, string> = {
left: 'left',
start: 'left',
center: 'center',
right: 'right',
end: 'right',
both: 'justify',
justify: 'justify',
distribute: 'justify',
};
const extractAlignmentMap = async (buffer: Buffer): Promise<Array<string|null>> => {
const aligns: Array<string|null> = [];
try {
const zip = await JSZip.loadAsync(buffer);
const file = zip.file('word/document.xml');
if (!file) return aligns;
const xml: string = await file.async('text');
let m: RegExpExecArray | null;
while ((m = PARA_RE.exec(xml)) !== null) {
const jcMatch = JC_RE.exec(m[1]);
const css = jcMatch ? JC_TO_CSS[jcMatch[1].toLowerCase()] : null;
aligns.push(css || null);
}
} catch {
// Best-effort — if the docx structure is anything other than a
// standard document.xml, fall back to no alignment.
}
return aligns;
};
const applyAlignmentToHtml = (html: string, aligns: Array<string|null>): string => {
if (aligns.length === 0 || !aligns.some((a) => a && a !== 'left')) return html;
let i = 0;
return html.replace(/<(p|h[1-6])(\b[^>]*)>/gi, (whole: string, tag: string, attrs: string) => {
const align = aligns[i++];
if (!align || align === 'left') return whole;
if (/\bstyle\s*=/.test(attrs)) {
return `<${tag}${attrs.replace(
/\bstyle\s*=\s*(['"])([^'"]*)\1/i,
(_full: string, q: string, val: string) => `style=${q}${val}; text-align:${align}${q}`)}>`;
}
return `<${tag}${attrs} style="text-align:${align}">`;
});
};
export const docxBufferToHtml = async (buffer: Buffer): Promise<string> => {
const aligns = await extractAlignmentMap(buffer);
const result = await mammoth.convertToHtml(
{buffer},
{
// Preserve empty paragraphs so blank pad lines survive a
// round-trip. mammoth defaults to true and drops them, which
// collapses blank lines in the middle of a pad's content.
ignoreEmptyParagraphs: false,
convertImage: mammoth.images.imgElement(async (image: any) => {
const buf: Buffer = await image.read();
const contentType = image.contentType || 'application/octet-stream';
return {src: `data:${contentType};base64,${buf.toString('base64')}`};
}),
},
);
let html: string = result.value || '';
html = applyAlignmentToHtml(html, aligns);
return html;
};