@file-viewer/doc 2.2.6 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/msdoc/html-word.d.ts +3 -0
- package/dist/msdoc/html-word.js +178 -0
- package/dist/msdoc/parser.js +6 -0
- package/package.json +3 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { charPropsToState, paraPropsToState } from './properties.js';
|
|
2
|
+
const HTML_PREFIX_RE = /^(?:<!doctype\s+html\b|<html\b|<body\b)/i;
|
|
3
|
+
const BLOCK_RE = /<(div|p|h[1-6]|li|pre|blockquote|tr)\b([^>]*)>([\s\S]*?)<\/\1\s*>/gi;
|
|
4
|
+
const UNSAFE_CONTENT_RE = /<(script|style|noscript|iframe|object|embed|svg)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
5
|
+
function asciiPreview(bytes, length = 1024) {
|
|
6
|
+
return Array.from(bytes.subarray(0, Math.min(length, bytes.length)), byte => byte === 0 ? '' : String.fromCharCode(byte)).join('');
|
|
7
|
+
}
|
|
8
|
+
export function isHtmlWordDocumentStream(bytes) {
|
|
9
|
+
const preview = asciiPreview(bytes)
|
|
10
|
+
.replace(/^\uFEFF/, '')
|
|
11
|
+
.trimStart();
|
|
12
|
+
return HTML_PREFIX_RE.test(preview);
|
|
13
|
+
}
|
|
14
|
+
function decodeWith(bytes, label, fatal = false) {
|
|
15
|
+
try {
|
|
16
|
+
return new TextDecoder(label, { fatal }).decode(bytes);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function decodeHtmlWordDocument(bytes) {
|
|
23
|
+
let end = bytes.length;
|
|
24
|
+
while (end > 0 && bytes[end - 1] === 0)
|
|
25
|
+
end -= 1;
|
|
26
|
+
const content = bytes.subarray(0, end);
|
|
27
|
+
const preview = asciiPreview(content, 4096);
|
|
28
|
+
const declared = /charset\s*=\s*["']?([a-z0-9._-]+)/i.exec(preview)?.[1]?.toLowerCase();
|
|
29
|
+
const declaredEncoding = declared === 'gb2312' || declared === 'gbk' ? 'gb18030' : declared;
|
|
30
|
+
if (declaredEncoding) {
|
|
31
|
+
const decoded = decodeWith(content, declaredEncoding);
|
|
32
|
+
if (decoded != null)
|
|
33
|
+
return decoded;
|
|
34
|
+
}
|
|
35
|
+
const utf8 = decodeWith(content, 'utf-8', true);
|
|
36
|
+
if (utf8 != null)
|
|
37
|
+
return utf8;
|
|
38
|
+
return decodeWith(content, 'gb18030') ?? decodeWith(content, 'utf-8') ?? '';
|
|
39
|
+
}
|
|
40
|
+
function decodeEntities(value) {
|
|
41
|
+
const named = {
|
|
42
|
+
amp: '&',
|
|
43
|
+
apos: "'",
|
|
44
|
+
gt: '>',
|
|
45
|
+
lt: '<',
|
|
46
|
+
nbsp: '\u00a0',
|
|
47
|
+
quot: '"',
|
|
48
|
+
};
|
|
49
|
+
return value.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z][a-z0-9]+);/gi, (match, entity) => {
|
|
50
|
+
if (entity[0] === '#') {
|
|
51
|
+
const hexadecimal = entity[1]?.toLowerCase() === 'x';
|
|
52
|
+
const parsed = Number.parseInt(entity.slice(hexadecimal ? 2 : 1), hexadecimal ? 16 : 10);
|
|
53
|
+
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 0x10ffff) {
|
|
54
|
+
try {
|
|
55
|
+
return String.fromCodePoint(parsed);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return match;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return match;
|
|
62
|
+
}
|
|
63
|
+
return named[entity.toLowerCase()] ?? match;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function fragmentToText(fragment) {
|
|
67
|
+
return decodeEntities(fragment
|
|
68
|
+
.replace(UNSAFE_CONTENT_RE, '')
|
|
69
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
70
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
71
|
+
.replace(/<\/(?:div|p|h[1-6]|li|pre|blockquote|tr|table)>/gi, '\n')
|
|
72
|
+
.replace(/<\/?(?:td|th)\b[^>]*>/gi, '\t')
|
|
73
|
+
.replace(/<[^>]+>/g, '')
|
|
74
|
+
.replace(/\u0000/g, ''))
|
|
75
|
+
.replace(/\r\n?/g, '\n')
|
|
76
|
+
.replace(/[\t\f\v ]+/g, ' ')
|
|
77
|
+
.replace(/ *\n */g, '\n')
|
|
78
|
+
.trim();
|
|
79
|
+
}
|
|
80
|
+
function extractParagraphs(html) {
|
|
81
|
+
const safe = html.replace(UNSAFE_CONTENT_RE, '').replace(/<!--[\s\S]*?-->/g, '');
|
|
82
|
+
const paragraphs = [];
|
|
83
|
+
for (const match of safe.matchAll(BLOCK_RE)) {
|
|
84
|
+
const content = match[3] || '';
|
|
85
|
+
const text = fragmentToText(content);
|
|
86
|
+
if (!text)
|
|
87
|
+
continue;
|
|
88
|
+
paragraphs.push({ text, attributes: match[2] || '', content });
|
|
89
|
+
}
|
|
90
|
+
if (paragraphs.length)
|
|
91
|
+
return paragraphs;
|
|
92
|
+
const text = fragmentToText(safe);
|
|
93
|
+
return text ? [{ text, attributes: '', content: safe }] : [];
|
|
94
|
+
}
|
|
95
|
+
function readStyle(attributes) {
|
|
96
|
+
return /\bstyle\s*=\s*(["'])([\s\S]*?)\1/i.exec(attributes)?.[2] || '';
|
|
97
|
+
}
|
|
98
|
+
function createParagraph(source, index) {
|
|
99
|
+
const paraState = paraPropsToState([]);
|
|
100
|
+
const charState = charPropsToState([]);
|
|
101
|
+
const style = readStyle(source.attributes);
|
|
102
|
+
const alignment = /text-align\s*:\s*(center|right|justify)/i.exec(style)?.[1]?.toLowerCase();
|
|
103
|
+
if (alignment === 'center')
|
|
104
|
+
paraState.alignment = 1;
|
|
105
|
+
if (alignment === 'right')
|
|
106
|
+
paraState.alignment = 2;
|
|
107
|
+
if (alignment === 'justify')
|
|
108
|
+
paraState.alignment = 3;
|
|
109
|
+
const indentPoints = Number.parseFloat(/text-indent\s*:\s*([\d.]+)pt/i.exec(style)?.[1] || '');
|
|
110
|
+
if (Number.isFinite(indentPoints))
|
|
111
|
+
paraState.firstLineIndent = Math.round(indentPoints * 20);
|
|
112
|
+
const fontSizePoints = Number.parseFloat(/font-size\s*:\s*([\d.]+)pt/i.exec(style)?.[1] || '');
|
|
113
|
+
if (Number.isFinite(fontSizePoints)) {
|
|
114
|
+
charState.fontSizeHalfPoints = Math.round(Math.min(96, Math.max(8, fontSizePoints)) * 2);
|
|
115
|
+
}
|
|
116
|
+
if (/font-weight\s*:\s*(?:bold|[6-9]00)/i.test(style) ||
|
|
117
|
+
/<(?:b|strong)\b/i.test(source.content)) {
|
|
118
|
+
charState.bold = true;
|
|
119
|
+
}
|
|
120
|
+
if (/font-style\s*:\s*italic/i.test(style) || /<(?:i|em)\b/i.test(source.content)) {
|
|
121
|
+
charState.italic = true;
|
|
122
|
+
}
|
|
123
|
+
const inlines = source.text
|
|
124
|
+
.split('\n')
|
|
125
|
+
.flatMap((line, lineIndex) => [
|
|
126
|
+
...(lineIndex ? [{ type: 'lineBreak' }] : []),
|
|
127
|
+
...(line ? [{ type: 'text', text: line, style: { ...charState } }] : []),
|
|
128
|
+
]);
|
|
129
|
+
return {
|
|
130
|
+
type: 'paragraph',
|
|
131
|
+
id: `html-word-paragraph-${index + 1}`,
|
|
132
|
+
styleId: 0,
|
|
133
|
+
styleName: 'Normal',
|
|
134
|
+
paraState,
|
|
135
|
+
inlines,
|
|
136
|
+
text: source.text,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function parseHtmlWordDocumentStream(bytes) {
|
|
140
|
+
if (!isHtmlWordDocumentStream(bytes))
|
|
141
|
+
return null;
|
|
142
|
+
const paragraphs = extractParagraphs(decodeHtmlWordDocument(bytes));
|
|
143
|
+
if (!paragraphs.length)
|
|
144
|
+
return null;
|
|
145
|
+
const blocks = paragraphs.map(createParagraph);
|
|
146
|
+
const textLength = paragraphs.reduce((total, paragraph) => total + paragraph.text.length, 0);
|
|
147
|
+
return {
|
|
148
|
+
kind: 'msdoc',
|
|
149
|
+
version: 1,
|
|
150
|
+
warnings: [
|
|
151
|
+
{
|
|
152
|
+
message: 'Recovered an HTML document stored inside the legacy WordDocument stream.',
|
|
153
|
+
code: 'MSDOC_HTML_WORDDOCUMENT_RECOVERED',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
meta: {
|
|
157
|
+
fib: {
|
|
158
|
+
wIdent: 0,
|
|
159
|
+
nFib: 0,
|
|
160
|
+
fWhichTblStm: 0,
|
|
161
|
+
fComplex: false,
|
|
162
|
+
fEncrypted: false,
|
|
163
|
+
ccpText: textLength,
|
|
164
|
+
},
|
|
165
|
+
counts: {
|
|
166
|
+
paragraphs: blocks.length,
|
|
167
|
+
blocks: blocks.length,
|
|
168
|
+
assets: 0,
|
|
169
|
+
styles: 0,
|
|
170
|
+
fonts: 0,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
fonts: [],
|
|
174
|
+
styles: [],
|
|
175
|
+
blocks,
|
|
176
|
+
assets: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/msdoc/parser.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DOC_CONTROL } from './constants.js';
|
|
|
5
5
|
import { parseFib } from './fib.js';
|
|
6
6
|
import { readChpxRuns, readPapxRuns } from './fkp.js';
|
|
7
7
|
import { parseFonts } from './fonts.js';
|
|
8
|
+
import { parseHtmlWordDocumentStream } from './html-word.js';
|
|
8
9
|
import { extractObjectPool, extractPictureAsset } from './objects.js';
|
|
9
10
|
import { applyTableStateToCells, charPropsToState, getTableDepth, paraPropsToState, tablePropsToState, } from './properties.js';
|
|
10
11
|
import { mergePropertyArrays, parseStyles, splitPropertiesByKind } from './styles.js';
|
|
@@ -646,6 +647,11 @@ export function parseMsDoc(input, options = {}) {
|
|
|
646
647
|
const wordBytes = cfb.getStream('/WordDocument');
|
|
647
648
|
if (!wordBytes)
|
|
648
649
|
throw new Error('Missing WordDocument stream');
|
|
650
|
+
const htmlDocument = parseHtmlWordDocumentStream(wordBytes);
|
|
651
|
+
if (htmlDocument) {
|
|
652
|
+
htmlDocument.warnings.unshift(...(cfb.warnings || []));
|
|
653
|
+
return htmlDocument;
|
|
654
|
+
}
|
|
649
655
|
const fib = parseFib(wordBytes);
|
|
650
656
|
if (fib.base.wIdent !== 0xA5EC) {
|
|
651
657
|
pushWarning(warnings, `Unexpected FIB identifier: 0x${fib.base.wIdent.toString(16)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@file-viewer/doc",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser-native legacy MS-DOC (.doc) parser and HTML renderer extracted from the public msdoc-viewer 0.2.0 line.",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"verify:table-spec": "pnpm build && node scripts/verify-table-spec.mjs",
|
|
72
72
|
"verify:github-34": "pnpm build && node scripts/verify-github-34.mjs",
|
|
73
73
|
"verify:github-87": "pnpm build && node scripts/verify-github-87.mjs",
|
|
74
|
-
"verify:github-171": "pnpm build && node scripts/verify-github-171.mjs"
|
|
74
|
+
"verify:github-171": "pnpm build && node scripts/verify-github-171.mjs",
|
|
75
|
+
"verify:github-192": "pnpm build && node scripts/verify-github-192.mjs"
|
|
75
76
|
}
|
|
76
77
|
}
|