@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.
@@ -0,0 +1,3 @@
1
+ import type { MsDocParseResult } from '../types.js';
2
+ export declare function isHtmlWordDocumentStream(bytes: Uint8Array): boolean;
3
+ export declare function parseHtmlWordDocumentStream(bytes: Uint8Array): MsDocParseResult | null;
@@ -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
+ }
@@ -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.6",
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
  }