@file-viewer/renderer-chm 3.0.1

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,850 @@
1
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
2
+ const MAX_CHM_PATH_LENGTH = 4096;
3
+ const ABSOLUTE_SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/i;
4
+ const EXTERNAL_SCHEMES = new Set(['http', 'https', 'mailto', 'tel']);
5
+ const BLOCKED_SCHEMES = new Set([
6
+ 'about', 'blob', 'file', 'ftp', 'javascript', 'resource', 'shell', 'vbscript',
7
+ ]);
8
+ export const MAX_CHM_HTML_TEXT_LENGTH = 16 * 1024 * 1024;
9
+ export const MAX_CHM_HTML_MARKUP_TOKENS = 100000;
10
+ export const MAX_CHM_SVG_TEXT_LENGTH = 8 * 1024 * 1024;
11
+ export const MAX_CHM_SVG_MARKUP_TOKENS = 50000;
12
+ export const MAX_CHM_CSS_TEXT_LENGTH = 4 * 1024 * 1024;
13
+ export const MAX_CHM_SEARCH_TEXT_LENGTH = 4 * 1024 * 1024;
14
+ const createStringBuilder = (maxLength = Number.POSITIVE_INFINITY) => {
15
+ const chunks = [];
16
+ let buffer = '';
17
+ let length = 0;
18
+ const append = (value) => {
19
+ if (!value || length >= maxLength)
20
+ return;
21
+ const remaining = maxLength - length;
22
+ const next = value.length > remaining ? value.slice(0, remaining) : value;
23
+ length += next.length;
24
+ if (buffer.length + next.length <= 16384) {
25
+ buffer += next;
26
+ return;
27
+ }
28
+ if (buffer)
29
+ chunks.push(buffer);
30
+ if (next.length > 16384) {
31
+ chunks.push(next);
32
+ buffer = '';
33
+ }
34
+ else {
35
+ buffer = next;
36
+ }
37
+ };
38
+ return {
39
+ append,
40
+ get length() { return length; },
41
+ finish: () => {
42
+ if (buffer)
43
+ chunks.push(buffer);
44
+ return chunks.join('');
45
+ },
46
+ };
47
+ };
48
+ export const assertChmMarkupBudget = (source, maxLength, maxTokens, kind) => {
49
+ if (source.length > maxLength) {
50
+ throw new Error(`CHM_LIMIT_EXCEEDED: ${kind} text exceeds ${maxLength} characters.`);
51
+ }
52
+ let tokens = 0;
53
+ let tagTokens = 0;
54
+ let inTag = false;
55
+ let inTagToken = false;
56
+ let quote = '';
57
+ const maxTagTokens = maxTokens * 3;
58
+ for (let index = 0; index < source.length; index += 1) {
59
+ const character = source[index];
60
+ if (!quote && character === '<') {
61
+ tokens += 1;
62
+ if (tokens > maxTokens) {
63
+ throw new Error(`CHM_LIMIT_EXCEEDED: ${kind} markup exceeds ${maxTokens} tokens.`);
64
+ }
65
+ if (!inTag) {
66
+ inTag = true;
67
+ inTagToken = false;
68
+ }
69
+ continue;
70
+ }
71
+ if (!inTag)
72
+ continue;
73
+ if (quote) {
74
+ if (character === quote)
75
+ quote = '';
76
+ continue;
77
+ }
78
+ if (character === '"' || character === "'") {
79
+ quote = character;
80
+ continue;
81
+ }
82
+ if (character === '>') {
83
+ inTag = false;
84
+ inTagToken = false;
85
+ continue;
86
+ }
87
+ if (isAsciiWhitespace(character)) {
88
+ inTagToken = false;
89
+ continue;
90
+ }
91
+ if (!inTagToken) {
92
+ inTagToken = true;
93
+ tagTokens += 1;
94
+ if (tagTokens > maxTagTokens) {
95
+ throw new Error(`CHM_LIMIT_EXCEEDED: ${kind} markup exceeds ${maxTagTokens} tag fields.`);
96
+ }
97
+ }
98
+ }
99
+ };
100
+ export const assertChmSvgSourceSafety = (source) => {
101
+ assertChmMarkupBudget(source, MAX_CHM_SVG_TEXT_LENGTH, MAX_CHM_SVG_MARKUP_TOKENS, 'SVG');
102
+ for (let index = 0; index + 2 < source.length; index += 1) {
103
+ if (source[index] !== '<' || source[index + 1] !== '!')
104
+ continue;
105
+ let cursor = index + 2;
106
+ while (isAsciiWhitespace(source[cursor]))
107
+ cursor += 1;
108
+ let end = cursor;
109
+ while (end < source.length) {
110
+ const code = source.charCodeAt(end);
111
+ if (!((code >= 65 && code <= 90) || (code >= 97 && code <= 122)))
112
+ break;
113
+ end += 1;
114
+ }
115
+ const keyword = source.slice(cursor, end).toLowerCase();
116
+ if (keyword === 'doctype' || keyword === 'entity') {
117
+ throw new Error(`CHM_SECURITY_BLOCKED: SVG ${keyword.toUpperCase()} declarations are disabled.`);
118
+ }
119
+ }
120
+ };
121
+ const safeDecodeURIComponent = (value) => {
122
+ try {
123
+ return decodeURIComponent(value);
124
+ }
125
+ catch {
126
+ return value;
127
+ }
128
+ };
129
+ export const normalizeChmPath = (input) => {
130
+ if (typeof input !== 'string' || CONTROL_CHARACTER_PATTERN.test(input))
131
+ return null;
132
+ const trimmed = input.trim();
133
+ if (!trimmed || trimmed.length > MAX_CHM_PATH_LENGTH)
134
+ return null;
135
+ const decoded = safeDecodeURIComponent(trimmed);
136
+ if (/^(?:[a-z]:[\\/]|[\\/]{2})/i.test(decoded))
137
+ return null;
138
+ const source = decoded.replace(/\\/g, '/');
139
+ const segments = [];
140
+ for (const segment of source.split('/')) {
141
+ if (!segment || segment === '.')
142
+ continue;
143
+ if (segment === '..') {
144
+ if (!segments.length)
145
+ return null;
146
+ segments.pop();
147
+ continue;
148
+ }
149
+ if (CONTROL_CHARACTER_PATTERN.test(segment))
150
+ return null;
151
+ segments.push(segment);
152
+ }
153
+ return `/${segments.join('/')}`;
154
+ };
155
+ const splitReference = (value) => {
156
+ const hashIndex = value.indexOf('#');
157
+ const fragment = hashIndex >= 0 ? safeDecodeURIComponent(value.slice(hashIndex + 1)) : '';
158
+ const withoutFragment = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
159
+ const queryIndex = withoutFragment.indexOf('?');
160
+ return {
161
+ path: queryIndex >= 0 ? withoutFragment.slice(0, queryIndex) : withoutFragment,
162
+ fragment,
163
+ };
164
+ };
165
+ const unwrapCompiledHelpUrl = (value) => {
166
+ const delimiter = value.indexOf('::');
167
+ return delimiter >= 0 ? value.slice(delimiter + 2) : '';
168
+ };
169
+ export const resolveChmReference = (basePath, rawUrl) => {
170
+ if (typeof rawUrl !== 'string')
171
+ return { kind: 'blocked' };
172
+ let value = rawUrl.trim();
173
+ if (!value || CONTROL_CHARACTER_PATTERN.test(value))
174
+ return { kind: 'blocked' };
175
+ if (value.startsWith('#')) {
176
+ return { kind: 'fragment', fragment: safeDecodeURIComponent(value.slice(1)) };
177
+ }
178
+ if (value.startsWith('//'))
179
+ return { kind: 'external', url: value };
180
+ const schemeMatch = value.match(ABSOLUTE_SCHEME_PATTERN);
181
+ if (schemeMatch) {
182
+ const scheme = schemeMatch[1].toLowerCase();
183
+ if (scheme === 'ms-its' || scheme === 'mk' || scheme === 'its') {
184
+ value = unwrapCompiledHelpUrl(value);
185
+ if (!value)
186
+ return { kind: 'blocked' };
187
+ }
188
+ else if (scheme === 'data') {
189
+ return /^data:image\/(?:avif|bmp|gif|jpeg|png|webp);/i.test(value)
190
+ ? { kind: 'data', url: value }
191
+ : { kind: 'blocked' };
192
+ }
193
+ else if (EXTERNAL_SCHEMES.has(scheme)) {
194
+ return { kind: 'external', url: value };
195
+ }
196
+ else if (BLOCKED_SCHEMES.has(scheme) || scheme) {
197
+ return { kind: 'blocked' };
198
+ }
199
+ }
200
+ const { path: rawPath, fragment } = splitReference(value);
201
+ if (!rawPath && fragment)
202
+ return { kind: 'fragment', fragment };
203
+ const normalizedBase = normalizeChmPath(basePath) || '/';
204
+ const baseDirectory = normalizedBase.includes('/')
205
+ ? normalizedBase.slice(0, normalizedBase.lastIndexOf('/') + 1)
206
+ : '';
207
+ const joined = rawPath.startsWith('/') ? rawPath : `${baseDirectory}${rawPath}`;
208
+ const path = normalizeChmPath(joined);
209
+ if (path == null || path === '/')
210
+ return { kind: 'blocked' };
211
+ return { kind: 'internal', path, fragment: fragment || undefined };
212
+ };
213
+ const normalizeEncoding = (value) => {
214
+ const normalized = (value || '').trim().toLowerCase().replace(/_/g, '-');
215
+ if (!normalized)
216
+ return '';
217
+ if (normalized === 'utf8')
218
+ return 'utf-8';
219
+ if (normalized === 'gbk' || normalized === 'cp936')
220
+ return 'gb18030';
221
+ if (normalized === 'big5-hkscs')
222
+ return 'big5';
223
+ if (normalized === 'shift-jis' || normalized === 'sjis' || normalized === 'cp932')
224
+ return 'shift_jis';
225
+ if (normalized === 'euc-kr' || normalized === 'cp949')
226
+ return 'euc-kr';
227
+ if (/^windows-?\d+$/.test(normalized))
228
+ return normalized.replace(/^windows-?/, 'windows-');
229
+ if (/^cp\d+$/.test(normalized))
230
+ return normalized.replace(/^cp/, 'windows-');
231
+ return normalized;
232
+ };
233
+ const sniffHtmlEncoding = (bytes) => {
234
+ const prefix = bytes.subarray(0, Math.min(bytes.byteLength, 8192));
235
+ let ascii = '';
236
+ for (let index = 0; index < prefix.byteLength; index += 1) {
237
+ const value = prefix[index];
238
+ ascii += value >= 0x20 && value <= 0x7e ? String.fromCharCode(value) : ' ';
239
+ }
240
+ const match = ascii.match(/<meta\b[^>]*\bcharset\s*=\s*["']?\s*([^\s"'/>;]+)/i)
241
+ || ascii.match(/<meta\b[^>]*\bcontent\s*=\s*["'][^"']*charset\s*=\s*([^\s"'/>;]+)/i);
242
+ return normalizeEncoding(match === null || match === void 0 ? void 0 : match[1]);
243
+ };
244
+ const decodeWith = (bytes, encoding) => {
245
+ try {
246
+ return new TextDecoder(encoding, { fatal: false }).decode(bytes);
247
+ }
248
+ catch {
249
+ return '';
250
+ }
251
+ };
252
+ export const decodeChmText = (bytes, fallbackEncoding = 'windows-1252') => {
253
+ if (bytes.byteLength >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
254
+ return decodeWith(bytes.subarray(3), 'utf-8');
255
+ }
256
+ if (bytes.byteLength >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
257
+ return decodeWith(bytes.subarray(2), 'utf-16le');
258
+ }
259
+ if (bytes.byteLength >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
260
+ const swapped = new Uint8Array(bytes.byteLength - 2);
261
+ for (let index = 2; index + 1 < bytes.byteLength; index += 2) {
262
+ swapped[index - 2] = bytes[index + 1];
263
+ swapped[index - 1] = bytes[index];
264
+ }
265
+ return decodeWith(swapped, 'utf-16le');
266
+ }
267
+ const candidates = [sniffHtmlEncoding(bytes), normalizeEncoding(fallbackEncoding), 'utf-8', 'windows-1252'];
268
+ for (const encoding of candidates) {
269
+ if (!encoding)
270
+ continue;
271
+ const decoded = decodeWith(bytes, encoding);
272
+ if (decoded)
273
+ return decoded.replace(/^\ufeff/, '');
274
+ }
275
+ return '';
276
+ };
277
+ const isAsciiWhitespace = (character) => character === ' '
278
+ || character === '\t'
279
+ || character === '\n'
280
+ || character === '\f'
281
+ || character === '\r';
282
+ const isHtmlNameCharacter = (character) => {
283
+ if (!character)
284
+ return false;
285
+ const code = character.charCodeAt(0);
286
+ return (code >= 48 && code <= 57)
287
+ || (code >= 65 && code <= 90)
288
+ || (code >= 97 && code <= 122)
289
+ || character === ':'
290
+ || character === '_'
291
+ || character === '-';
292
+ };
293
+ const startsWithAsciiCaseInsensitive = (source, index, expected) => {
294
+ if (index + expected.length > source.length)
295
+ return false;
296
+ for (let offset = 0; offset < expected.length; offset += 1) {
297
+ const actual = source.charCodeAt(index + offset);
298
+ const wanted = expected.charCodeAt(offset);
299
+ const folded = actual >= 65 && actual <= 90 ? actual + 32 : actual;
300
+ if (folded !== wanted)
301
+ return false;
302
+ }
303
+ return true;
304
+ };
305
+ const scanHtmlTagEnd = (source, start) => {
306
+ let index = start;
307
+ let quote = '';
308
+ while (index < source.length) {
309
+ const character = source[index];
310
+ if (quote) {
311
+ if (character === quote)
312
+ quote = '';
313
+ }
314
+ else if (character === '"' || character === "'") {
315
+ quote = character;
316
+ }
317
+ else if (character === '>') {
318
+ return index + 1;
319
+ }
320
+ index += 1;
321
+ }
322
+ return source.length;
323
+ };
324
+ const skipHtmlRawTextElement = (source, start, name) => {
325
+ let index = start;
326
+ while (index < source.length) {
327
+ if (source[index] === '<' && source[index + 1] === '/'
328
+ && startsWithAsciiCaseInsensitive(source, index + 2, name)
329
+ && !isHtmlNameCharacter(source[index + 2 + name.length])) {
330
+ return scanHtmlTagEnd(source, index + 2 + name.length);
331
+ }
332
+ index += 1;
333
+ }
334
+ return source.length;
335
+ };
336
+ const decodeSearchEntity = (source, index) => {
337
+ let end = index + 1;
338
+ const limit = Math.min(source.length, index + 13);
339
+ while (end < limit && source[end] !== ';') {
340
+ if (source[end] === '&' || source[end] === '<' || isAsciiWhitespace(source[end]))
341
+ return null;
342
+ end += 1;
343
+ }
344
+ if (end >= limit || source[end] !== ';')
345
+ return null;
346
+ const entity = source.slice(index + 1, end).toLowerCase();
347
+ const named = {
348
+ amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"',
349
+ };
350
+ if (named[entity] != null)
351
+ return { end: end + 1, value: named[entity] };
352
+ const numeric = entity.startsWith('#x')
353
+ ? Number.parseInt(entity.slice(2), 16)
354
+ : entity.startsWith('#') ? Number.parseInt(entity.slice(1), 10) : Number.NaN;
355
+ if (!Number.isFinite(numeric) || numeric <= 0 || numeric > 0x10ffff || (numeric >= 0xd800 && numeric <= 0xdfff)) {
356
+ return null;
357
+ }
358
+ return { end: end + 1, value: String.fromCodePoint(numeric) };
359
+ };
360
+ /** Extract searchable topic text without DOM APIs or backtracking regular expressions. */
361
+ export const extractChmSearchText = (html, maxOutputLength = MAX_CHM_SEARCH_TEXT_LENGTH) => {
362
+ const output = createStringBuilder(Math.max(0, maxOutputLength));
363
+ let index = 0;
364
+ let pendingSpace = false;
365
+ const appendText = (value) => {
366
+ if (!value)
367
+ return;
368
+ if (isAsciiWhitespace(value)) {
369
+ pendingSpace = output.length > 0;
370
+ return;
371
+ }
372
+ if (pendingSpace && output.length < maxOutputLength)
373
+ output.append(' ');
374
+ pendingSpace = false;
375
+ output.append(value);
376
+ };
377
+ while (index < html.length) {
378
+ const character = html[index];
379
+ if (character === '<') {
380
+ pendingSpace = output.length > 0;
381
+ if (html.startsWith('<!--', index)) {
382
+ const end = html.indexOf('-->', index + 4);
383
+ index = end < 0 ? html.length : end + 3;
384
+ continue;
385
+ }
386
+ let cursor = index + 1;
387
+ const closing = html[cursor] === '/';
388
+ if (closing)
389
+ cursor += 1;
390
+ while (isAsciiWhitespace(html[cursor]))
391
+ cursor += 1;
392
+ const nameStart = cursor;
393
+ while (isHtmlNameCharacter(html[cursor]))
394
+ cursor += 1;
395
+ const name = html.slice(nameStart, cursor).toLowerCase();
396
+ const tagEnd = scanHtmlTagEnd(html, cursor);
397
+ index = !closing && (name === 'script' || name === 'style')
398
+ ? skipHtmlRawTextElement(html, tagEnd, name)
399
+ : tagEnd;
400
+ continue;
401
+ }
402
+ if (character === '&') {
403
+ const entity = decodeSearchEntity(html, index);
404
+ if (entity) {
405
+ appendText(entity.value);
406
+ index = entity.end;
407
+ continue;
408
+ }
409
+ }
410
+ appendText(character);
411
+ index += 1;
412
+ }
413
+ return output.finish().trim();
414
+ };
415
+ const INTERNAL_RESOURCE_SCHEME = 'chm-internal:';
416
+ export const MAX_CHM_TOPIC_RESOURCE_PATHS = 2048;
417
+ export const MAX_CHM_CSS_RESOURCE_PATHS = 1024;
418
+ const isHexDigit = (character) => {
419
+ if (!character)
420
+ return false;
421
+ const code = character.charCodeAt(0);
422
+ return (code >= 48 && code <= 57)
423
+ || (code >= 65 && code <= 70)
424
+ || (code >= 97 && code <= 102);
425
+ };
426
+ const isCssIdentifierCharacter = (character) => {
427
+ if (!character)
428
+ return false;
429
+ const code = character.charCodeAt(0);
430
+ return (code >= 48 && code <= 57)
431
+ || (code >= 65 && code <= 90)
432
+ || (code >= 97 && code <= 122)
433
+ || character === '_'
434
+ || character === '-';
435
+ };
436
+ const normalizeCssSyntax = (css) => {
437
+ const output = createStringBuilder();
438
+ let index = 0;
439
+ let spanStart = 0;
440
+ while (index < css.length) {
441
+ if (css[index] === '/' && css[index + 1] === '*') {
442
+ output.append(css.slice(spanStart, index));
443
+ const end = css.indexOf('*/', index + 2);
444
+ if (end < 0) {
445
+ index = css.length;
446
+ spanStart = index;
447
+ break;
448
+ }
449
+ index = end + 2;
450
+ spanStart = index;
451
+ continue;
452
+ }
453
+ if (css[index] !== '\\') {
454
+ index += 1;
455
+ continue;
456
+ }
457
+ output.append(css.slice(spanStart, index));
458
+ const next = css[index + 1];
459
+ if (next === '\n' || next === '\f') {
460
+ index += 2;
461
+ spanStart = index;
462
+ continue;
463
+ }
464
+ if (next === '\r') {
465
+ index += css[index + 2] === '\n' ? 3 : 2;
466
+ spanStart = index;
467
+ continue;
468
+ }
469
+ let cursor = index + 1;
470
+ while (cursor < css.length && cursor < index + 7 && isHexDigit(css[cursor]))
471
+ cursor += 1;
472
+ if (cursor > index + 1) {
473
+ const hex = css.slice(index + 1, cursor);
474
+ const codePoint = Number.parseInt(hex, 16);
475
+ if (css[cursor] === '\r' && css[cursor + 1] === '\n')
476
+ cursor += 2;
477
+ else if (/[\t\n\f\r ]/.test(css[cursor] || ''))
478
+ cursor += 1;
479
+ const decoded = codePoint > 0 && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : '\ufffd';
480
+ if (/^["'();\\]$/.test(decoded))
481
+ output.append(`\\${hex} `);
482
+ else
483
+ output.append(decoded);
484
+ index = cursor;
485
+ spanStart = index;
486
+ continue;
487
+ }
488
+ if (next) {
489
+ if (/^["'();\\]$/.test(next))
490
+ output.append(`\\${next}`);
491
+ else
492
+ output.append(next);
493
+ index += 2;
494
+ }
495
+ else {
496
+ index += 1;
497
+ }
498
+ spanStart = index;
499
+ }
500
+ output.append(css.slice(spanStart));
501
+ return output.finish();
502
+ };
503
+ const stripCssImports = (css) => {
504
+ const output = createStringBuilder();
505
+ let index = 0;
506
+ let spanStart = 0;
507
+ while (index < css.length) {
508
+ const isImport = css[index] === '@'
509
+ && css.slice(index, index + 7).toLowerCase() === '@import'
510
+ && !isCssIdentifierCharacter(css[index + 7]);
511
+ if (!isImport) {
512
+ index += 1;
513
+ continue;
514
+ }
515
+ output.append(css.slice(spanStart, index));
516
+ index += 7;
517
+ let quote = '';
518
+ let parenthesisDepth = 0;
519
+ while (index < css.length) {
520
+ const character = css[index];
521
+ if (character === '\\') {
522
+ index = Math.min(css.length, index + 2);
523
+ continue;
524
+ }
525
+ if (quote) {
526
+ if (character === quote)
527
+ quote = '';
528
+ }
529
+ else if (character === '"' || character === "'") {
530
+ quote = character;
531
+ }
532
+ else if (character === '(') {
533
+ parenthesisDepth += 1;
534
+ }
535
+ else if (character === ')' && parenthesisDepth > 0) {
536
+ parenthesisDepth -= 1;
537
+ }
538
+ else if (character === ';' && parenthesisDepth === 0) {
539
+ index += 1;
540
+ break;
541
+ }
542
+ index += 1;
543
+ }
544
+ spanStart = index;
545
+ }
546
+ output.append(css.slice(spanStart));
547
+ return output.finish();
548
+ };
549
+ const matchCssFunction = (css, index, name) => {
550
+ if (isCssIdentifierCharacter(css[index - 1]) || css.slice(index, index + name.length).toLowerCase() !== name)
551
+ return -1;
552
+ let cursor = index + name.length;
553
+ if (isCssIdentifierCharacter(css[cursor]))
554
+ return -1;
555
+ while (/[\t\n\f\r ]/.test(css[cursor] || ''))
556
+ cursor += 1;
557
+ return css[cursor] === '(' ? cursor : -1;
558
+ };
559
+ const readCssFunction = (css, openParenthesis) => {
560
+ let cursor = openParenthesis + 1;
561
+ let depth = 1;
562
+ let quote = '';
563
+ while (cursor < css.length) {
564
+ const character = css[cursor];
565
+ if (character === '\\') {
566
+ cursor = Math.min(css.length, cursor + 2);
567
+ continue;
568
+ }
569
+ if (quote) {
570
+ if (character === quote)
571
+ quote = '';
572
+ }
573
+ else if (character === '"' || character === "'") {
574
+ quote = character;
575
+ }
576
+ else if (character === '(') {
577
+ depth += 1;
578
+ }
579
+ else if (character === ')') {
580
+ depth -= 1;
581
+ if (depth === 0)
582
+ return { body: css.slice(openParenthesis + 1, cursor), end: cursor + 1 };
583
+ }
584
+ cursor += 1;
585
+ }
586
+ return { body: css.slice(openParenthesis + 1), end: css.length };
587
+ };
588
+ const matchDangerousDeclaration = (css, index) => {
589
+ if (isCssIdentifierCharacter(css[index - 1]))
590
+ return -1;
591
+ for (const name of ['behavior', '-moz-binding']) {
592
+ if (css.slice(index, index + name.length).toLowerCase() !== name)
593
+ continue;
594
+ let cursor = index + name.length;
595
+ if (isCssIdentifierCharacter(css[cursor]))
596
+ continue;
597
+ while (/[\t\n\f\r ]/.test(css[cursor] || ''))
598
+ cursor += 1;
599
+ if (css[cursor] === ':')
600
+ return cursor + 1;
601
+ }
602
+ return -1;
603
+ };
604
+ const skipCssDeclarationValue = (css, start) => {
605
+ let cursor = start;
606
+ let depth = 0;
607
+ let quote = '';
608
+ while (cursor < css.length) {
609
+ const character = css[cursor];
610
+ if (character === '\\') {
611
+ cursor = Math.min(css.length, cursor + 2);
612
+ continue;
613
+ }
614
+ if (quote) {
615
+ if (character === quote)
616
+ quote = '';
617
+ }
618
+ else if (character === '"' || character === "'") {
619
+ quote = character;
620
+ }
621
+ else if (character === '(') {
622
+ depth += 1;
623
+ }
624
+ else if (character === ')' && depth > 0) {
625
+ depth -= 1;
626
+ }
627
+ else if (depth === 0 && (character === ';' || character === '}')) {
628
+ return character === ';' ? cursor + 1 : cursor;
629
+ }
630
+ cursor += 1;
631
+ }
632
+ return cursor;
633
+ };
634
+ const recordResourcePath = (resources, path, maxResourcePaths) => {
635
+ if (resources.has(path))
636
+ return true;
637
+ if (resources.size >= maxResourcePaths)
638
+ return false;
639
+ resources.add(path);
640
+ return true;
641
+ };
642
+ const sanitizeCss = (css, basePath, resources, maxResourcePaths) => {
643
+ if (css.length > MAX_CHM_CSS_TEXT_LENGTH)
644
+ return '';
645
+ const source = stripCssImports(normalizeCssSyntax(css));
646
+ const output = createStringBuilder();
647
+ let index = 0;
648
+ let spanStart = 0;
649
+ while (index < source.length) {
650
+ const character = source[index];
651
+ if (character === '"' || character === "'") {
652
+ index += 1;
653
+ while (index < source.length) {
654
+ if (source[index] === '\\')
655
+ index = Math.min(source.length, index + 2);
656
+ else if (source[index++] === character)
657
+ break;
658
+ else
659
+ index += 0;
660
+ }
661
+ continue;
662
+ }
663
+ const declarationStart = matchDangerousDeclaration(source, index);
664
+ if (declarationStart >= 0) {
665
+ output.append(source.slice(spanStart, index));
666
+ index = skipCssDeclarationValue(source, declarationStart);
667
+ spanStart = index;
668
+ continue;
669
+ }
670
+ const expressionStart = matchCssFunction(source, index, 'expression');
671
+ if (expressionStart >= 0) {
672
+ output.append(source.slice(spanStart, index));
673
+ index = readCssFunction(source, expressionStart).end;
674
+ spanStart = index;
675
+ continue;
676
+ }
677
+ const urlStart = matchCssFunction(source, index, 'url');
678
+ if (urlStart >= 0) {
679
+ output.append(source.slice(spanStart, index));
680
+ const parsed = readCssFunction(source, urlStart);
681
+ let raw = parsed.body.trim();
682
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
683
+ raw = raw.slice(1, -1).trim();
684
+ }
685
+ const resolved = resolveChmReference(basePath, raw);
686
+ if (resolved.kind === 'data' && resolved.url) {
687
+ output.append(`url("${resolved.url.replace(/"/g, '%22')}")`);
688
+ }
689
+ else if (resolved.kind === 'fragment' && resolved.fragment && /^[\w:.-]+$/.test(resolved.fragment)) {
690
+ output.append(`url("#${resolved.fragment}")`);
691
+ }
692
+ else if (resolved.kind === 'internal' && resolved.path
693
+ && recordResourcePath(resources, resolved.path, maxResourcePaths)) {
694
+ output.append(`url("${INTERNAL_RESOURCE_SCHEME}${encodeURIComponent(resolved.path)}")`);
695
+ }
696
+ else {
697
+ output.append('url("")');
698
+ }
699
+ index = parsed.end;
700
+ spanStart = index;
701
+ continue;
702
+ }
703
+ index += 1;
704
+ }
705
+ output.append(source.slice(spanStart));
706
+ return output.finish();
707
+ };
708
+ const sanitizeEmbeddedCssAttribute = (value, basePath) => sanitizeCss(value, basePath, new Set(), 0);
709
+ export const sanitizeChmCss = (css, basePath, maxResourcePaths = MAX_CHM_CSS_RESOURCE_PATHS) => {
710
+ const resources = new Set();
711
+ return {
712
+ css: sanitizeCss(css, basePath, resources, maxResourcePaths),
713
+ resourcePaths: Array.from(resources),
714
+ };
715
+ };
716
+ const RESOURCE_ATTRIBUTES = [
717
+ ['img', 'src'], ['source', 'src'], ['audio', 'src'], ['video', 'src'], ['video', 'poster'],
718
+ ['track', 'src'], ['input', 'src'], ['img', 'lowsrc'], ['img', 'dynsrc'],
719
+ ['body', 'background'], ['table', 'background'], ['td', 'background'], ['th', 'background'],
720
+ ['image', 'href'], ['image', 'xlink:href'], ['use', 'href'], ['use', 'xlink:href'],
721
+ ['feImage', 'href'], ['feImage', 'xlink:href'],
722
+ ];
723
+ const removeUnsafeElements = (document) => {
724
+ document.querySelectorAll('script,object,embed,applet,iframe,frame,frameset,form,input,button,textarea,select,option,base,noscript,template,foreignObject,animate,animateMotion,set').forEach(element => element.remove());
725
+ document.querySelectorAll('meta[http-equiv]').forEach(element => element.remove());
726
+ };
727
+ export const sanitizeChmHtmlDocument = (html, basePath, serialize = true) => {
728
+ if (typeof DOMParser === 'undefined') {
729
+ throw new Error('CHM_SECURITY_DOM_UNAVAILABLE: DOMParser is required to sanitize CHM topics.');
730
+ }
731
+ assertChmMarkupBudget(html, MAX_CHM_HTML_TEXT_LENGTH, MAX_CHM_HTML_MARKUP_TOKENS, 'HTML');
732
+ const document = new DOMParser().parseFromString(html, 'text/html');
733
+ const resources = new Set();
734
+ removeUnsafeElements(document);
735
+ document.querySelectorAll('*').forEach(element => {
736
+ for (const attribute of Array.from(element.attributes)) {
737
+ const name = attribute.name.toLowerCase();
738
+ if (name.startsWith('on') || name === 'srcdoc' || name === 'ping' || name === 'formaction') {
739
+ element.removeAttribute(attribute.name);
740
+ }
741
+ }
742
+ const inlineStyle = element.getAttribute('style');
743
+ if (inlineStyle) {
744
+ element.setAttribute('style', sanitizeCss(inlineStyle, basePath, resources, MAX_CHM_TOPIC_RESOURCE_PATHS));
745
+ }
746
+ for (const attribute of Array.from(element.attributes)) {
747
+ if (attribute.name.toLowerCase() === 'style')
748
+ continue;
749
+ if (/(?:url\s*\(|@import\b|expression\s*\(|behavior\s*:|-moz-binding|\\|\/\*)/i.test(attribute.value)) {
750
+ element.setAttribute(attribute.name, sanitizeEmbeddedCssAttribute(attribute.value, basePath));
751
+ }
752
+ }
753
+ });
754
+ document.querySelectorAll('style').forEach(style => {
755
+ style.textContent = sanitizeCss(style.textContent || '', basePath, resources, MAX_CHM_TOPIC_RESOURCE_PATHS);
756
+ });
757
+ document.querySelectorAll('a,area').forEach(element => {
758
+ const hasXlink = element.hasAttribute('xlink:href')
759
+ || element.hasAttributeNS('http://www.w3.org/1999/xlink', 'href');
760
+ if (!element.hasAttribute('href') && !hasXlink)
761
+ return;
762
+ const raw = element.getAttribute('href')
763
+ || element.getAttribute('xlink:href')
764
+ || element.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
765
+ || '';
766
+ const resolved = resolveChmReference(basePath, raw);
767
+ element.removeAttribute('href');
768
+ element.removeAttribute('xlink:href');
769
+ element.removeAttributeNS('http://www.w3.org/1999/xlink', 'href');
770
+ element.setAttribute('data-chm-link-kind', resolved.kind);
771
+ if (resolved.path)
772
+ element.setAttribute('data-chm-path', resolved.path);
773
+ if (resolved.fragment)
774
+ element.setAttribute('data-chm-fragment', resolved.fragment);
775
+ if (resolved.kind === 'internal' || resolved.kind === 'fragment') {
776
+ element.setAttribute('href', '#');
777
+ }
778
+ else {
779
+ element.setAttribute('aria-disabled', 'true');
780
+ element.setAttribute('title', 'External and active links are disabled in CHM preview.');
781
+ }
782
+ });
783
+ document.querySelectorAll('link[href]').forEach(element => {
784
+ const relation = (element.getAttribute('rel') || '').toLowerCase().split(/\s+/);
785
+ const resolved = resolveChmReference(basePath, element.getAttribute('href') || '');
786
+ element.removeAttribute('href');
787
+ if (!relation.includes('stylesheet') || resolved.kind !== 'internal' || !resolved.path) {
788
+ element.remove();
789
+ return;
790
+ }
791
+ if (!recordResourcePath(resources, resolved.path, MAX_CHM_TOPIC_RESOURCE_PATHS)) {
792
+ element.remove();
793
+ return;
794
+ }
795
+ element.setAttribute('data-chm-resource-href', resolved.path);
796
+ });
797
+ for (const [selector, attribute] of RESOURCE_ATTRIBUTES) {
798
+ document.querySelectorAll(`${selector}[${attribute.replace(':', '\\:')}]`).forEach(element => {
799
+ const raw = element.getAttribute(attribute) || '';
800
+ const resolved = resolveChmReference(basePath, raw);
801
+ element.removeAttribute(attribute);
802
+ if (resolved.kind === 'fragment' && resolved.fragment) {
803
+ element.setAttribute(attribute, `#${resolved.fragment}`);
804
+ }
805
+ else if (resolved.kind === 'data' && resolved.url) {
806
+ element.setAttribute(attribute, resolved.url);
807
+ }
808
+ else if (resolved.kind === 'internal' && resolved.path
809
+ && recordResourcePath(resources, resolved.path, MAX_CHM_TOPIC_RESOURCE_PATHS)) {
810
+ element.setAttribute(`data-chm-resource-${attribute.replace(':', '-')}`, resolved.path);
811
+ }
812
+ });
813
+ }
814
+ document.querySelectorAll('[srcset]').forEach(element => element.removeAttribute('srcset'));
815
+ const csp = document.createElement('meta');
816
+ csp.setAttribute('http-equiv', 'Content-Security-Policy');
817
+ csp.setAttribute('content', [
818
+ "default-src 'none'",
819
+ "script-src 'none'",
820
+ "style-src 'unsafe-inline' blob:",
821
+ 'img-src data: blob:',
822
+ 'font-src data: blob:',
823
+ 'media-src data: blob:',
824
+ "connect-src 'none'",
825
+ "object-src 'none'",
826
+ "frame-src 'none'",
827
+ "worker-src 'none'",
828
+ "base-uri 'none'",
829
+ "form-action 'none'",
830
+ ].join('; '));
831
+ document.head.prepend(csp);
832
+ const viewerStyle = document.createElement('style');
833
+ viewerStyle.textContent = [
834
+ ':root{color-scheme:light dark}',
835
+ 'html,body{min-height:100%;margin:0}',
836
+ 'body{padding:24px;box-sizing:border-box;background:#fff;color:#172033;font:16px/1.65 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;overflow-wrap:anywhere}',
837
+ 'img,video,svg,table{max-width:100%}',
838
+ 'pre{white-space:pre-wrap}',
839
+ 'a[aria-disabled=true]{color:inherit;text-decoration:line-through;cursor:not-allowed}',
840
+ '@media(prefers-color-scheme:dark){body{background:#111827;color:#e5eef8}a{color:#7dd3fc}}',
841
+ ].join('');
842
+ document.head.append(viewerStyle);
843
+ return {
844
+ document,
845
+ html: serialize ? `<!doctype html>${document.documentElement.outerHTML}` : '',
846
+ title: document.title || '',
847
+ resourcePaths: Array.from(resources),
848
+ };
849
+ };
850
+ export const CHM_INTERNAL_RESOURCE_SCHEME = INTERNAL_RESOURCE_SCHEME;