@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.
package/dist/chm.js ADDED
@@ -0,0 +1,593 @@
1
+ import { assertChmSvgSourceSafety, decodeChmText, MAX_CHM_CSS_TEXT_LENGTH, MAX_CHM_CSS_RESOURCE_PATHS, MAX_CHM_SVG_TEXT_LENGTH, MAX_CHM_TOPIC_RESOURCE_PATHS, normalizeChmPath, sanitizeChmCss, sanitizeChmHtmlDocument, } from './security.js';
2
+ import { chmViewerStyle } from './style.js';
3
+ import { ChmWorkerClient } from './workerClient.js';
4
+ const MAX_RENDERED_NAVIGATION_ITEMS = 12000;
5
+ const MAX_LOADED_RESOURCE_PATHS = 4096;
6
+ const HTML_EXTENSIONS = new Set(['htm', 'html', 'xhtml', 'shtml']);
7
+ const messages = {
8
+ 'zh-CN': {
9
+ contents: '目录', index: '索引', search: '搜索', searchPlaceholder: '搜索主题和正文',
10
+ loading: '正在打开 CHM…', wasm: '正在加载本地 WASM…', directory: '正在读取 CHM 目录…',
11
+ manifest: '正在构建帮助目录…', searching: '正在搜索…', noContents: '没有可显示的目录。',
12
+ noIndex: '没有可显示的关键词索引。', searchHint: '输入至少两个字符搜索当前 CHM。',
13
+ noResults: '没有找到匹配内容。', truncated: '结果已按安全与性能上限截断。',
14
+ error: 'CHM 预览失败', topics: '个主题', ready: '离线安全预览', binaryToc: '二进制目录',
15
+ fullText: '全文索引', menu: '目录', opening: '正在打开主题…', unavailable: '没有找到可打开的主题。',
16
+ },
17
+ 'en-US': {
18
+ contents: 'Contents', index: 'Index', search: 'Search', searchPlaceholder: 'Search topics and text',
19
+ loading: 'Opening CHM…', wasm: 'Loading local WASM…', directory: 'Reading the CHM directory…',
20
+ manifest: 'Building help navigation…', searching: 'Searching…', noContents: 'No table of contents is available.',
21
+ noIndex: 'No keyword index is available.', searchHint: 'Enter at least two characters to search this CHM.',
22
+ noResults: 'No matching content was found.', truncated: 'Results were truncated at the configured safety limit.',
23
+ error: 'CHM preview failed', topics: 'topics', ready: 'Safe offline preview', binaryToc: 'Binary TOC',
24
+ fullText: 'Full-text index', menu: 'Menu', opening: 'Opening topic…', unavailable: 'No readable topic was found.',
25
+ },
26
+ };
27
+ const resolveLocale = (context) => {
28
+ var _a;
29
+ const configured = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.locale;
30
+ const candidate = configured && configured !== 'auto'
31
+ ? configured
32
+ : typeof navigator === 'undefined' ? 'en-US' : navigator.language;
33
+ return candidate.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US';
34
+ };
35
+ const createElement = (tag, className, text) => {
36
+ const element = document.createElement(tag);
37
+ if (className)
38
+ element.className = className;
39
+ if (text != null)
40
+ element.textContent = text;
41
+ return element;
42
+ };
43
+ const extensionOf = (path) => {
44
+ const filename = path.split('/').pop() || '';
45
+ const index = filename.lastIndexOf('.');
46
+ return index >= 0 ? filename.slice(index + 1).toLowerCase() : '';
47
+ };
48
+ const mimeTypeForPath = (path) => {
49
+ const extension = extensionOf(path);
50
+ const types = {
51
+ css: 'text/css;charset=utf-8', gif: 'image/gif', jpeg: 'image/jpeg', jpg: 'image/jpeg',
52
+ png: 'image/png', webp: 'image/webp', avif: 'image/avif', bmp: 'image/bmp', ico: 'image/x-icon',
53
+ svg: 'image/svg+xml', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', mp4: 'video/mp4',
54
+ webm: 'video/webm', woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', otf: 'font/otf',
55
+ };
56
+ return types[extension] || 'application/octet-stream';
57
+ };
58
+ const sanitizeEmbeddedSvgCss = (value) => sanitizeChmCss(value, '/', 0).css;
59
+ const sanitizeSvg = (source) => {
60
+ assertChmSvgSourceSafety(source);
61
+ const document = new DOMParser().parseFromString(source, 'image/svg+xml');
62
+ if (document.querySelector('parsererror'))
63
+ return '';
64
+ document.querySelectorAll('script,foreignObject,iframe,object,embed').forEach(element => element.remove());
65
+ document.querySelectorAll('style').forEach(style => {
66
+ style.textContent = sanitizeEmbeddedSvgCss(style.textContent || '');
67
+ });
68
+ document.querySelectorAll('*').forEach(element => {
69
+ for (const attribute of Array.from(element.attributes)) {
70
+ const name = attribute.name.toLowerCase();
71
+ const value = attribute.value.trim();
72
+ if (name.startsWith('on')) {
73
+ element.removeAttribute(attribute.name);
74
+ continue;
75
+ }
76
+ if ((name === 'href' || name === 'xlink:href')
77
+ && !value.startsWith('#')
78
+ && !/^data:image\/(?:avif|bmp|gif|jpeg|png|webp);/i.test(value)) {
79
+ element.removeAttribute(attribute.name);
80
+ continue;
81
+ }
82
+ if (name !== 'href' && name !== 'xlink:href'
83
+ && /(?:url\s*\(|@import\b|expression\s*\(|behavior\s*:|-moz-binding|\\|\/\*)/i.test(value)) {
84
+ element.setAttribute(attribute.name, sanitizeEmbeddedSvgCss(value));
85
+ }
86
+ }
87
+ });
88
+ return new XMLSerializer().serializeToString(document.documentElement);
89
+ };
90
+ const INTERNAL_CSS_RESOURCE_PATTERN = /url\("chm-internal:([^"]+)"\)/g;
91
+ const loadResourceMap = async (paths, loadResource, ancestry, maxResourcePaths = MAX_CHM_CSS_RESOURCE_PATHS) => {
92
+ const uniquePaths = Array.from(new Set(paths)).slice(0, maxResourcePaths);
93
+ const replacements = new Map();
94
+ let cursor = 0;
95
+ const runners = Array.from({ length: Math.min(8, uniquePaths.length) }, async () => {
96
+ while (cursor < uniquePaths.length) {
97
+ const path = uniquePaths[cursor++];
98
+ const normalized = normalizeChmPath(path);
99
+ const url = normalized && normalized !== '/'
100
+ ? await loadResource(normalized, ancestry)
101
+ : null;
102
+ if (normalized)
103
+ replacements.set(normalized.toLocaleLowerCase(), url || '');
104
+ }
105
+ });
106
+ await Promise.all(runners);
107
+ return replacements;
108
+ };
109
+ const replaceInternalCssResourceUrls = (css, replacements) => css.replace(INTERNAL_CSS_RESOURCE_PATTERN, (_match, encoded) => {
110
+ let path = '';
111
+ try {
112
+ path = decodeURIComponent(encoded);
113
+ }
114
+ catch {
115
+ return 'url("")';
116
+ }
117
+ const normalized = normalizeChmPath(path);
118
+ return `url("${normalized ? replacements.get(normalized.toLocaleLowerCase()) || '' : ''}")`;
119
+ });
120
+ const replaceCssResourceUrls = async (css, resourcePaths, loadResource, ancestry) => replaceInternalCssResourceUrls(css, await loadResourceMap(resourcePaths, loadResource, ancestry));
121
+ const flatNavigation = (nodes) => {
122
+ const output = [];
123
+ const stack = nodes.slice().reverse().map(node => ({ node, depth: 0 }));
124
+ while (stack.length && output.length < MAX_RENDERED_NAVIGATION_ITEMS) {
125
+ const current = stack.pop();
126
+ if (!current)
127
+ continue;
128
+ output.push(current);
129
+ for (let index = current.node.children.length - 1; index >= 0; index -= 1) {
130
+ stack.push({ node: current.node.children[index], depth: current.depth + 1 });
131
+ }
132
+ }
133
+ return output;
134
+ };
135
+ const highlightSnippet = (target, text, query) => {
136
+ const index = text.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
137
+ if (index < 0) {
138
+ target.textContent = text;
139
+ return;
140
+ }
141
+ target.append(document.createTextNode(text.slice(0, index)), createElement('mark', undefined, text.slice(index, index + query.length)), document.createTextNode(text.slice(index + query.length)));
142
+ };
143
+ export default async function renderChm(buffer, target, _type, context) {
144
+ var _a, _b, _c, _d;
145
+ const locale = resolveLocale(context);
146
+ const t = (key) => messages[locale][key];
147
+ const options = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.chm;
148
+ const abortController = new AbortController();
149
+ const objectUrls = new Set();
150
+ const resourceUrls = new Map();
151
+ const listeners = [];
152
+ let client;
153
+ let destroyed = false;
154
+ let manifest;
155
+ let entries = [];
156
+ let entryLookup = new Map();
157
+ let currentHtml = '';
158
+ let topicGeneration = 0;
159
+ let searchGeneration = 0;
160
+ let searchTimer;
161
+ const style = createElement('style');
162
+ style.textContent = chmViewerStyle;
163
+ const root = createElement('section', 'chm-viewer');
164
+ root.dataset.chmReady = 'false';
165
+ const theme = ((_b = context === null || context === void 0 ? void 0 : context.options) === null || _b === void 0 ? void 0 : _b.theme) || 'light';
166
+ target.dataset.viewerTheme = theme;
167
+ const header = createElement('header', 'chm-header');
168
+ const sidebarToggle = createElement('button', 'chm-sidebar-toggle', '☰');
169
+ sidebarToggle.type = 'button';
170
+ sidebarToggle.title = t('menu');
171
+ sidebarToggle.setAttribute('aria-label', t('menu'));
172
+ const heading = createElement('div', 'chm-heading');
173
+ const title = createElement('h2', undefined, (context === null || context === void 0 ? void 0 : context.filename) || 'CHM');
174
+ const subtitle = createElement('p', undefined, t('loading'));
175
+ heading.append(title, subtitle);
176
+ const badges = createElement('div', 'chm-badges');
177
+ header.append(sidebarToggle, heading, badges);
178
+ const body = createElement('div', 'chm-body');
179
+ const sidebar = createElement('aside', 'chm-sidebar');
180
+ const tabs = createElement('div', 'chm-tabs');
181
+ const panels = createElement('div', 'chm-sidebar-panels');
182
+ const contentsPanel = createElement('section', 'chm-panel');
183
+ contentsPanel.dataset.chmPanel = 'contents';
184
+ const indexPanel = createElement('section', 'chm-panel');
185
+ indexPanel.dataset.chmPanel = 'index';
186
+ indexPanel.hidden = true;
187
+ const searchPanel = createElement('section', 'chm-panel chm-search');
188
+ searchPanel.dataset.chmPanel = 'search';
189
+ searchPanel.hidden = true;
190
+ const searchBox = createElement('div', 'chm-search-box');
191
+ const searchInput = createElement('input');
192
+ searchInput.type = 'search';
193
+ searchInput.placeholder = t('searchPlaceholder');
194
+ searchInput.setAttribute('aria-label', t('search'));
195
+ const searchMeta = createElement('div', 'chm-search-meta', t('searchHint'));
196
+ const searchResults = createElement('div', 'chm-search-results');
197
+ searchBox.append(searchInput);
198
+ searchPanel.append(searchBox, searchMeta, searchResults);
199
+ panels.append(contentsPanel, indexPanel, searchPanel);
200
+ sidebar.append(tabs, panels);
201
+ const topic = createElement('main', 'chm-topic');
202
+ const topicBar = createElement('div', 'chm-topic-bar');
203
+ const topicTitle = createElement('strong', undefined, t('loading'));
204
+ const topicPath = createElement('span', 'chm-topic-path');
205
+ topicBar.append(topicTitle, topicPath);
206
+ const frame = createElement('iframe', 'chm-topic-frame');
207
+ frame.setAttribute('sandbox', 'allow-same-origin');
208
+ frame.setAttribute('referrerpolicy', 'no-referrer');
209
+ frame.title = (context === null || context === void 0 ? void 0 : context.filename) || 'CHM topic';
210
+ topic.append(topicBar, frame);
211
+ body.append(sidebar, topic);
212
+ const state = createElement('div', 'chm-state');
213
+ const spinner = createElement('span', 'chm-spinner');
214
+ const stateText = createElement('p', undefined, t('loading'));
215
+ state.append(spinner, stateText);
216
+ const errorPanel = createElement('div', 'chm-error');
217
+ errorPanel.hidden = true;
218
+ errorPanel.append(createElement('strong', undefined, t('error')), createElement('p'));
219
+ root.append(header, body, state);
220
+ target.replaceChildren(style, root);
221
+ const listen = (node, type, handler) => {
222
+ node.addEventListener(type, handler);
223
+ listeners.push(() => node.removeEventListener(type, handler));
224
+ };
225
+ const showState = (text, visible = true) => {
226
+ stateText.textContent = text;
227
+ state.hidden = !visible;
228
+ };
229
+ const showError = (error) => {
230
+ showState('', false);
231
+ const paragraph = errorPanel.querySelector('p');
232
+ if (paragraph)
233
+ paragraph.textContent = error instanceof Error ? error.message : String(error);
234
+ errorPanel.hidden = false;
235
+ if (!errorPanel.isConnected)
236
+ root.append(errorPanel);
237
+ };
238
+ const onProgress = (progress) => {
239
+ if (root.dataset.chmReady === 'true') {
240
+ if (progress.phase === 'search')
241
+ searchMeta.textContent = t('searching');
242
+ return;
243
+ }
244
+ if (progress.phase === 'wasm')
245
+ showState(t('wasm'));
246
+ else if (progress.phase === 'directory')
247
+ showState(t('directory'));
248
+ else if (progress.phase === 'manifest')
249
+ showState(t('manifest'));
250
+ };
251
+ const resolveEntryPath = (path) => {
252
+ const normalized = normalizeChmPath(path);
253
+ if (!normalized)
254
+ return null;
255
+ return entryLookup.get(normalized.toLocaleLowerCase()) || null;
256
+ };
257
+ const loadResource = async (path, ancestry = new Set()) => {
258
+ const actualPath = resolveEntryPath(path);
259
+ if (!actualPath || !client || destroyed)
260
+ return null;
261
+ const key = actualPath.toLocaleLowerCase();
262
+ if (ancestry.has(key))
263
+ return null;
264
+ const cached = resourceUrls.get(key);
265
+ if (cached)
266
+ return cached;
267
+ if (resourceUrls.size >= MAX_LOADED_RESOURCE_PATHS)
268
+ return null;
269
+ const promise = (async () => {
270
+ try {
271
+ const bytes = await (client === null || client === void 0 ? void 0 : client.read(actualPath, abortController.signal));
272
+ if (!bytes || bytes.byteLength > ((client === null || client === void 0 ? void 0 : client.options.maxEntryBytes) || 0))
273
+ return null;
274
+ const extension = extensionOf(actualPath);
275
+ let blob;
276
+ if (extension === 'css') {
277
+ // @import is removed by the sanitizer. A CSS file referenced from a
278
+ // CSS url() is a non-renderable resource and must not start a graph.
279
+ if (ancestry.size > 0)
280
+ return null;
281
+ if (bytes.byteLength > MAX_CHM_CSS_TEXT_LENGTH)
282
+ return null;
283
+ const nextAncestry = new Set(ancestry);
284
+ nextAncestry.add(key);
285
+ const sanitizedCss = sanitizeChmCss(decodeChmText(bytes, manifest === null || manifest === void 0 ? void 0 : manifest.encoding), actualPath);
286
+ const css = await replaceCssResourceUrls(sanitizedCss.css, sanitizedCss.resourcePaths, loadResource, nextAncestry);
287
+ blob = new Blob([css], { type: 'text/css;charset=utf-8' });
288
+ }
289
+ else if (extension === 'svg') {
290
+ if (bytes.byteLength > MAX_CHM_SVG_TEXT_LENGTH)
291
+ return null;
292
+ const svg = sanitizeSvg(decodeChmText(bytes, 'utf-8'));
293
+ if (!svg)
294
+ return null;
295
+ blob = new Blob([svg], { type: 'image/svg+xml' });
296
+ }
297
+ else {
298
+ const copy = new Uint8Array(bytes.byteLength);
299
+ copy.set(bytes);
300
+ blob = new Blob([copy.buffer], { type: mimeTypeForPath(actualPath) });
301
+ }
302
+ const url = URL.createObjectURL(blob);
303
+ objectUrls.add(url);
304
+ return url;
305
+ }
306
+ catch {
307
+ return null;
308
+ }
309
+ })();
310
+ resourceUrls.set(key, promise);
311
+ return promise;
312
+ };
313
+ const hydrateTopic = async (html, basePath) => {
314
+ const sanitized = sanitizeChmHtmlDocument(html, basePath, false);
315
+ const { document } = sanitized;
316
+ const replacements = await loadResourceMap(sanitized.resourcePaths, loadResource, new Set(), MAX_CHM_TOPIC_RESOURCE_PATHS);
317
+ const resourceAttributes = ['src', 'poster', 'href', 'xlink-href', 'background', 'lowsrc', 'dynsrc'];
318
+ for (const attribute of resourceAttributes) {
319
+ document.querySelectorAll(`[data-chm-resource-${attribute}]`).forEach(element => {
320
+ const dataAttribute = `data-chm-resource-${attribute}`;
321
+ const path = normalizeChmPath(element.getAttribute(dataAttribute) || '');
322
+ const url = path ? replacements.get(path.toLocaleLowerCase()) : '';
323
+ element.removeAttribute(dataAttribute);
324
+ if (!url)
325
+ return;
326
+ if (attribute === 'xlink-href')
327
+ element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', url);
328
+ else
329
+ element.setAttribute(attribute, url);
330
+ });
331
+ }
332
+ for (const style of Array.from(document.querySelectorAll('style'))) {
333
+ style.textContent = replaceInternalCssResourceUrls(style.textContent || '', replacements);
334
+ }
335
+ for (const element of Array.from(document.querySelectorAll('[style]'))) {
336
+ element.setAttribute('style', replaceInternalCssResourceUrls(element.getAttribute('style') || '', replacements));
337
+ }
338
+ return {
339
+ html: `<!doctype html>${document.documentElement.outerHTML}`,
340
+ title: sanitized.title,
341
+ };
342
+ };
343
+ const syncActiveNavigation = (path) => {
344
+ root.querySelectorAll('[data-chm-path].is-active').forEach(element => element.classList.remove('is-active'));
345
+ const folded = path.toLocaleLowerCase();
346
+ root.querySelectorAll('[data-chm-path]').forEach(element => {
347
+ if ((element.dataset.chmPath || '').toLocaleLowerCase() === folded)
348
+ element.classList.add('is-active');
349
+ });
350
+ };
351
+ const scrollToFragment = (fragment) => {
352
+ if (!fragment)
353
+ return;
354
+ const document = frame.contentDocument;
355
+ const target = (document === null || document === void 0 ? void 0 : document.getElementById(fragment)) || (document === null || document === void 0 ? void 0 : document.getElementsByName(fragment)[0]);
356
+ target === null || target === void 0 ? void 0 : target.scrollIntoView({ block: 'start' });
357
+ };
358
+ const openTopic = async (path, fragment) => {
359
+ var _a, _b;
360
+ const generation = ++topicGeneration;
361
+ const actualPath = resolveEntryPath(path);
362
+ if (!actualPath || !client)
363
+ throw new Error(`CHM_ENTRY_NOT_FOUND: ${path}`);
364
+ showState(t('opening'));
365
+ try {
366
+ const bytes = await client.read(actualPath, abortController.signal);
367
+ if (bytes.byteLength > client.options.maxHtmlBytes) {
368
+ throw new Error(`CHM_LIMIT_EXCEEDED: topic ${actualPath} is too large to render safely.`);
369
+ }
370
+ const hydrated = await hydrateTopic(decodeChmText(bytes, manifest === null || manifest === void 0 ? void 0 : manifest.encoding), actualPath);
371
+ if (destroyed || generation !== topicGeneration)
372
+ return;
373
+ currentHtml = hydrated.html;
374
+ const canonicalPath = normalizeChmPath(actualPath) || actualPath;
375
+ topicTitle.textContent = hydrated.title || ((_a = manifest === null || manifest === void 0 ? void 0 : manifest.topics.find(item => { var _a; return ((_a = normalizeChmPath(item.path)) === null || _a === void 0 ? void 0 : _a.toLocaleLowerCase()) === canonicalPath.toLocaleLowerCase(); })) === null || _a === void 0 ? void 0 : _a.title) || actualPath;
376
+ topicPath.textContent = canonicalPath;
377
+ frame.srcdoc = hydrated.html;
378
+ frame.onload = () => {
379
+ if (destroyed || generation !== topicGeneration)
380
+ return;
381
+ const frameDocument = frame.contentDocument;
382
+ if (frameDocument) {
383
+ frameDocument.addEventListener('click', event => {
384
+ const eventTarget = event.target;
385
+ const anchor = eventTarget instanceof Element ? eventTarget.closest('a[data-chm-link-kind],area[data-chm-link-kind]') : null;
386
+ if (!anchor)
387
+ return;
388
+ event.preventDefault();
389
+ const kind = anchor.dataset.chmLinkKind;
390
+ const nextPath = anchor.dataset.chmPath;
391
+ const nextFragment = anchor.dataset.chmFragment;
392
+ if (kind === 'fragment')
393
+ scrollToFragment(nextFragment);
394
+ else if (kind === 'internal' && nextPath)
395
+ void openTopic(nextPath, nextFragment).catch(showError);
396
+ });
397
+ }
398
+ scrollToFragment(fragment);
399
+ };
400
+ syncActiveNavigation(canonicalPath);
401
+ root.classList.remove('is-sidebar-open');
402
+ errorPanel.remove();
403
+ errorPanel.hidden = true;
404
+ showState('', false);
405
+ (_b = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _b === void 0 ? void 0 : _b.call(context);
406
+ }
407
+ catch (error) {
408
+ if (generation === topicGeneration && !destroyed)
409
+ showError(error);
410
+ throw error;
411
+ }
412
+ };
413
+ const appendNavigation = (panel, nodes, emptyText) => {
414
+ panel.replaceChildren();
415
+ const flattened = flatNavigation(nodes);
416
+ if (!flattened.length) {
417
+ panel.append(createElement('p', 'chm-panel-note', emptyText));
418
+ return;
419
+ }
420
+ const list = createElement('ul', 'chm-navigation');
421
+ const parentByDepth = new Map([[0, list]]);
422
+ for (const { node, depth } of flattened) {
423
+ const parent = parentByDepth.get(depth) || list;
424
+ const item = createElement('li');
425
+ const normalizedPath = node.path ? normalizeChmPath(node.path) : null;
426
+ const label = normalizedPath
427
+ ? createElement('button', 'chm-nav-button', node.title || normalizedPath)
428
+ : createElement('div', 'chm-nav-label', node.title);
429
+ if (label instanceof HTMLButtonElement && normalizedPath) {
430
+ label.type = 'button';
431
+ label.dataset.chmPath = normalizedPath;
432
+ }
433
+ item.append(label);
434
+ if (node.children.length) {
435
+ const children = createElement('ul');
436
+ item.append(children);
437
+ parentByDepth.set(depth + 1, children);
438
+ }
439
+ else {
440
+ parentByDepth.delete(depth + 1);
441
+ }
442
+ parent.append(item);
443
+ }
444
+ panel.append(list);
445
+ if (flattened.length >= MAX_RENDERED_NAVIGATION_ITEMS) {
446
+ panel.append(createElement('p', 'chm-panel-note', t('truncated')));
447
+ }
448
+ };
449
+ const selectTab = (name) => {
450
+ tabs.querySelectorAll('.chm-tab').forEach(button => button.classList.toggle('is-active', button.dataset.chmTab === name));
451
+ panels.querySelectorAll('.chm-panel').forEach(panel => { panel.hidden = panel.dataset.chmPanel !== name; });
452
+ if (name === 'search')
453
+ searchInput.focus();
454
+ };
455
+ ['contents', 'index', 'search'].forEach((name, index) => {
456
+ const button = createElement('button', `chm-tab${index === 0 ? ' is-active' : ''}`, t(name));
457
+ button.type = 'button';
458
+ button.dataset.chmTab = name;
459
+ button.setAttribute('aria-label', t(name));
460
+ tabs.append(button);
461
+ });
462
+ const renderSearchHits = (hits, query, truncated) => {
463
+ searchResults.replaceChildren();
464
+ if (!hits.length) {
465
+ searchMeta.textContent = t('noResults');
466
+ return;
467
+ }
468
+ searchMeta.textContent = truncated ? `${hits.length} · ${t('truncated')}` : String(hits.length);
469
+ for (const hit of hits) {
470
+ const button = createElement('button', 'chm-search-result');
471
+ button.type = 'button';
472
+ button.dataset.chmSearchResult = 'true';
473
+ button.dataset.chmPath = hit.path;
474
+ const heading = createElement('strong');
475
+ highlightSnippet(heading, hit.title, query);
476
+ const snippet = createElement('span');
477
+ highlightSnippet(snippet, hit.snippet || hit.path, query);
478
+ button.append(heading, snippet);
479
+ searchResults.append(button);
480
+ }
481
+ };
482
+ const runSearch = async () => {
483
+ const query = searchInput.value.trim();
484
+ const generation = ++searchGeneration;
485
+ if (query.length < 2 || !client) {
486
+ searchResults.replaceChildren();
487
+ searchMeta.textContent = t('searchHint');
488
+ return;
489
+ }
490
+ searchMeta.textContent = t('searching');
491
+ try {
492
+ const result = await client.search(query, abortController.signal);
493
+ if (!destroyed && generation === searchGeneration)
494
+ renderSearchHits(result.hits, query, result.truncated);
495
+ }
496
+ catch (error) {
497
+ if (!destroyed && generation === searchGeneration)
498
+ searchMeta.textContent = error instanceof Error ? error.message : String(error);
499
+ }
500
+ };
501
+ listen(sidebarToggle, 'click', () => root.classList.toggle('is-sidebar-open'));
502
+ listen(tabs, 'click', event => {
503
+ const button = event.target instanceof Element ? event.target.closest('[data-chm-tab]') : null;
504
+ const name = button === null || button === void 0 ? void 0 : button.dataset.chmTab;
505
+ if (name === 'contents' || name === 'index' || name === 'search')
506
+ selectTab(name);
507
+ });
508
+ listen(panels, 'click', event => {
509
+ const button = event.target instanceof Element ? event.target.closest('[data-chm-path]') : null;
510
+ if (button === null || button === void 0 ? void 0 : button.dataset.chmPath)
511
+ void openTopic(button.dataset.chmPath).catch(() => undefined);
512
+ });
513
+ listen(searchInput, 'input', () => {
514
+ if (searchTimer)
515
+ clearTimeout(searchTimer);
516
+ searchTimer = setTimeout(() => void runSearch(), 350);
517
+ });
518
+ const destroy = () => {
519
+ var _a;
520
+ if (destroyed)
521
+ return;
522
+ destroyed = true;
523
+ topicGeneration += 1;
524
+ searchGeneration += 1;
525
+ if (searchTimer)
526
+ clearTimeout(searchTimer);
527
+ abortController.abort();
528
+ frame.onload = null;
529
+ listeners.splice(0).forEach(dispose => dispose());
530
+ client === null || client === void 0 ? void 0 : client.destroy();
531
+ client = undefined;
532
+ for (const url of objectUrls)
533
+ URL.revokeObjectURL(url);
534
+ objectUrls.clear();
535
+ resourceUrls.clear();
536
+ (_a = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
537
+ root.remove();
538
+ style.remove();
539
+ };
540
+ const abortFromContext = () => destroy();
541
+ (_c = context === null || context === void 0 ? void 0 : context.signal) === null || _c === void 0 ? void 0 : _c.addEventListener('abort', abortFromContext, { once: true });
542
+ listeners.push(() => { var _a; return (_a = context === null || context === void 0 ? void 0 : context.signal) === null || _a === void 0 ? void 0 : _a.removeEventListener('abort', abortFromContext); });
543
+ (_d = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _d === void 0 ? void 0 : _d.call(context, {
544
+ print: true,
545
+ exportHtml: true,
546
+ includeDocumentStyles: true,
547
+ toHtml: () => currentHtml || root.outerHTML,
548
+ printStyle: '.chm-sidebar,.chm-header,.chm-topic-bar{display:none!important}.chm-topic-frame{height:auto!important}',
549
+ });
550
+ try {
551
+ client = new ChmWorkerClient(options, onProgress);
552
+ const result = await client.open(buffer, abortController.signal);
553
+ if (destroyed)
554
+ return { $el: root, destroy };
555
+ manifest = result.manifest;
556
+ entries = result.entries;
557
+ entryLookup = new Map(entries.flatMap(entry => {
558
+ const normalized = normalizeChmPath(entry.path);
559
+ return normalized ? [[normalized.toLocaleLowerCase(), entry.path]] : [];
560
+ }));
561
+ title.textContent = manifest.title || (context === null || context === void 0 ? void 0 : context.filename) || 'CHM';
562
+ subtitle.textContent = `${manifest.topics.length} ${t('topics')}`;
563
+ badges.replaceChildren(createElement('span', 'chm-badge', t('ready')));
564
+ if (manifest.hasBinaryToc)
565
+ badges.append(createElement('span', 'chm-badge is-muted', t('binaryToc')));
566
+ if (manifest.hasFullTextIndex)
567
+ badges.append(createElement('span', 'chm-badge is-muted', t('fullText')));
568
+ const fallbackContents = manifest.topics.map(item => ({ title: item.title, path: item.path, children: [] }));
569
+ appendNavigation(contentsPanel, manifest.contents.length ? manifest.contents : fallbackContents, t('noContents'));
570
+ appendNavigation(indexPanel, manifest.index, t('noIndex'));
571
+ const candidates = [manifest.homePath, ...manifest.topics.map(item => item.path), ...entries
572
+ .filter(entry => HTML_EXTENSIONS.has(extensionOf(entry.path))).map(entry => entry.path)];
573
+ const homePath = candidates.find(candidate => Boolean(candidate && resolveEntryPath(candidate)));
574
+ if (!homePath)
575
+ throw new Error(`CHM_NO_TOPIC: ${t('unavailable')}`);
576
+ await openTopic(homePath);
577
+ if (destroyed)
578
+ return { $el: root, destroy };
579
+ root.dataset.chmReady = 'true';
580
+ errorPanel.remove();
581
+ errorPanel.hidden = true;
582
+ root.dispatchEvent(new CustomEvent('file-viewer:chm-ready', {
583
+ bubbles: true,
584
+ composed: true,
585
+ detail: { title: manifest.title, homePath: normalizeChmPath(homePath), topicCount: manifest.topics.length },
586
+ }));
587
+ }
588
+ catch (error) {
589
+ if (!destroyed)
590
+ showError(error);
591
+ }
592
+ return { $el: root, destroy };
593
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ var H=/[\u0000-\u001f\u007f]/;var re=16*1024*1024;var ie=8*1024*1024;var oe=4*1024*1024,W=4*1024*1024,X=(e=Number.POSITIVE_INFINITY)=>{let t=[],n="",r=0;return{append:o=>{if(!o||r>=e)return;let a=e-r,s=o.length>a?o.slice(0,a):o;if(r+=s.length,n.length+s.length<=16384){n+=s;return}n&&t.push(n),s.length>16384?(t.push(s),n=""):n=s},get length(){return r},finish:()=>(n&&t.push(n),t.join(""))}};var j=e=>{try{return decodeURIComponent(e)}catch{return e}},A=e=>{if(typeof e!="string"||H.test(e))return null;let t=e.trim();if(!t||t.length>4096)return null;let n=j(t);if(/^(?:[a-z]:[\\/]|[\\/]{2})/i.test(n))return null;let r=n.replace(/\\/g,"/"),i=[];for(let o of r.split("/"))if(!(!o||o===".")){if(o===".."){if(!i.length)return null;i.pop();continue}if(H.test(o))return null;i.push(o)}return`/${i.join("/")}`};var L=e=>{let t=(e||"").trim().toLowerCase().replace(/_/g,"-");return t?t==="utf8"?"utf-8":t==="gbk"||t==="cp936"?"gb18030":t==="big5-hkscs"?"big5":t==="shift-jis"||t==="sjis"||t==="cp932"?"shift_jis":t==="euc-kr"||t==="cp949"?"euc-kr":/^windows-?\d+$/.test(t)?t.replace(/^windows-?/,"windows-"):/^cp\d+$/.test(t)?t.replace(/^cp/,"windows-"):t:""},F=e=>{let t=e.subarray(0,Math.min(e.byteLength,8192)),n="";for(let i=0;i<t.byteLength;i+=1){let o=t[i];n+=o>=32&&o<=126?String.fromCharCode(o):" "}let r=n.match(/<meta\b[^>]*\bcharset\s*=\s*["']?\s*([^\s"'/>;]+)/i)||n.match(/<meta\b[^>]*\bcontent\s*=\s*["'][^"']*charset\s*=\s*([^\s"'/>;]+)/i);return L(r?.[1])},b=(e,t)=>{try{return new TextDecoder(t,{fatal:!1}).decode(e)}catch{return""}},v=(e,t="windows-1252")=>{if(e.byteLength>=3&&e[0]===239&&e[1]===187&&e[2]===191)return b(e.subarray(3),"utf-8");if(e.byteLength>=2&&e[0]===255&&e[1]===254)return b(e.subarray(2),"utf-16le");if(e.byteLength>=2&&e[0]===254&&e[1]===255){let r=new Uint8Array(e.byteLength-2);for(let i=2;i+1<e.byteLength;i+=2)r[i-2]=e[i+1],r[i-1]=e[i];return b(r,"utf-16le")}let n=[F(e),L(t),"utf-8","windows-1252"];for(let r of n){if(!r)continue;let i=b(e,r);if(i)return i.replace(/^\ufeff/,"")}return""},E=e=>e===" "||e===" "||e===`
2
+ `||e==="\f"||e==="\r",I=e=>{if(!e)return!1;let t=e.charCodeAt(0);return t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122||e===":"||e==="_"||e==="-"},q=(e,t,n)=>{if(t+n.length>e.length)return!1;for(let r=0;r<n.length;r+=1){let i=e.charCodeAt(t+r),o=n.charCodeAt(r);if((i>=65&&i<=90?i+32:i)!==o)return!1}return!0},B=(e,t)=>{let n=t,r="";for(;n<e.length;){let i=e[n];if(r)i===r&&(r="");else if(i==='"'||i==="'")r=i;else if(i===">")return n+1;n+=1}return e.length},V=(e,t,n)=>{let r=t;for(;r<e.length;){if(e[r]==="<"&&e[r+1]==="/"&&q(e,r+2,n)&&!I(e[r+2+n.length]))return B(e,r+2+n.length);r+=1}return e.length},G=(e,t)=>{let n=t+1,r=Math.min(e.length,t+13);for(;n<r&&e[n]!==";";){if(e[n]==="&"||e[n]==="<"||E(e[n]))return null;n+=1}if(n>=r||e[n]!==";")return null;let i=e.slice(t+1,n).toLowerCase(),o={amp:"&",apos:"'",gt:">",lt:"<",nbsp:" ",quot:'"'};if(o[i]!=null)return{end:n+1,value:o[i]};let a=i.startsWith("#x")?Number.parseInt(i.slice(2),16):i.startsWith("#")?Number.parseInt(i.slice(1),10):Number.NaN;return!Number.isFinite(a)||a<=0||a>1114111||a>=55296&&a<=57343?null:{end:n+1,value:String.fromCodePoint(a)}},N=(e,t=W)=>{let n=X(Math.max(0,t)),r=0,i=!1,o=a=>{if(a){if(E(a)){i=n.length>0;return}i&&n.length<t&&n.append(" "),i=!1,n.append(a)}};for(;r<e.length;){let a=e[r];if(a==="<"){if(i=n.length>0,e.startsWith("<!--",r)){let m=e.indexOf("-->",r+4);r=m<0?e.length:m+3;continue}let s=r+1,l=e[s]==="/";for(l&&(s+=1);E(e[s]);)s+=1;let c=s;for(;I(e[s]);)s+=1;let u=e.slice(c,s).toLowerCase(),g=B(e,s);r=!l&&(u==="script"||u==="style")?V(e,g,u):g;continue}if(a==="&"){let s=G(e,r);if(s){o(s.value),r=s.end;continue}}o(a),r+=1}return n.finish().trim()};var ae=Object.freeze({workerTimeoutMs:6e4,maxArchiveBytes:335544320,maxEntries:5e4,maxEntryBytes:33554432,maxTotalDecompressedBytes:536870912,maxHtmlBytes:16777216,maxSearchTopics:1e4,maxSearchResults:200}),C=1024*1024,ce=Object.freeze({maxArchiveBytes:1024*C,maxEntries:25e4,maxEntryBytes:512*C,maxTotalDecompressedBytes:8*1024*C,maxHtmlBytes:64*C});var f=e=>typeof e=="string"?e:"",_=e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,k=e=>e===!0,K=e=>{if(!e||typeof e!="object")return null;let t=e,n=f(t.path||t.local||t.url);return n?{title:f(t.title||t.name)||n,path:n,contextId:_(t.contextId??t.context_id)}:null},R=e=>{if(!Array.isArray(e))return[];let t=[],n=[];for(let r=e.length-1;r>=0;r-=1)n.push({source:e[r],target:t});for(;n.length;){let r=n.pop();if(!r?.source||typeof r.source!="object")continue;let i=r.source,o=[],a={title:f(i.title||i.name||i.keyword)||f(i.path||i.local),path:f(i.path||i.local||i.url)||void 0,keyword:f(i.keyword)||void 0,children:o};r.target.push(a);let s=Array.isArray(i.locals)?i.locals.filter(c=>typeof c=="string"&&!!c):[];!a.path&&s.length&&(a.path=s[0]);for(let c=a.path?1:0;c<s.length;c+=1)o.push({title:`${a.title} \xB7 ${c+1}`,path:s[c],children:[]});let l=Array.isArray(i.children)?i.children:[];for(let c=l.length-1;c>=0;c-=1)n.push({source:l[c],target:o})}return t},D=e=>{let t=e&&typeof e=="object"?e:{},n=Array.isArray(t.topics)?t.topics.map(K).filter(r=>!!r):[];return{title:f(t.title),homePath:f(t.homePath??t.home_path),encoding:f(t.encoding)||"windows-1252",language:f(t.language)||void 0,topics:n,contents:R(t.contents??t.toc),index:R(t.index??t.keywords),hasBinaryToc:k(t.hasBinaryToc??t.has_binary_toc),hasBinaryIndex:k(t.hasBinaryIndex??t.has_binary_index),hasFullTextIndex:k(t.hasFullTextIndex??t.has_full_text_index)||!!(t.fullTextIndex&&typeof t.fullTextIndex=="object"&&t.fullTextIndex.available)}},O=e=>Array.isArray(e)?e.flatMap(t=>{if(!t||typeof t!="object")return[];let n=t,r=f(n.path||n.name);return r?[{path:r,size:_(n.size??n.length??n.byteLength??n.byte_length)??0,section:_(n.section)}]:[]}):[];var $=self,h,y,w,M=0,T=new Set,p=(e,t=[])=>{$.postMessage(e,t)},d=(e,t,n)=>{p({id:0,ok:!0,type:"progress",phase:e,current:t,total:n})},S=()=>{if(h)try{h.dispose?.()}finally{try{h.free?.()}catch{}h=void 0,y=void 0,w=void 0,M=0,T.clear()}},Y=e=>e.match(/^([A-Z][A-Z0-9_]+):/)?.[1],Z=e=>{let t=e instanceof Error?e.message:String(e);return{name:e instanceof Error?e.name:"Error",message:t,code:Y(t)}},J=async(e,t)=>{d("wasm",0,1);let n=await import(e);if(typeof n.default=="function"&&await n.default({module_or_path:t}),typeof n.ChmArchive!="function")throw new Error("CHM_WASM_API_MISMATCH: ChmArchive is not exported by the WASM module.");return d("wasm",1,1),n},Q=async e=>{if(S(),e.buffer.byteLength>e.limits.maxArchiveBytes)throw new Error(`CHM_LIMIT_EXCEEDED: source is ${e.buffer.byteLength} bytes; limit is ${e.limits.maxArchiveBytes}.`);let t=await J(e.moduleUrl,e.wasmUrl);d("directory",0,1),h=new t.ChmArchive(new Uint8Array(e.buffer),e.limits),w=e.limits;let n=O(h.entries());if(n.length>e.limits.maxEntries)throw new Error(`CHM_LIMIT_EXCEEDED: ${n.length} entries exceed limit ${e.limits.maxEntries}.`);return d("directory",1,1),d("manifest",0,1),y=D(h.manifest()),d("manifest",1,1),{manifest:y,entries:n}},z=()=>{if(!h||!y||!w)throw new Error("CHM_NOT_OPEN: no CHM archive is active in this Worker.");return{archive:h,manifest:y,limits:w}},P=e=>{let t=z(),n=t.archive.read(e);if(n.byteLength>t.limits.maxEntryBytes)throw new Error(`CHM_LIMIT_EXCEEDED: entry ${e} exceeds ${t.limits.maxEntryBytes} bytes.`);let r=e.toLocaleLowerCase();if(!T.has(r)){let i=M+n.byteLength;if(i>t.limits.maxTotalDecompressedBytes)throw new Error(`CHM_LIMIT_EXCEEDED: unique decoded content exceeds ${t.limits.maxTotalDecompressedBytes} bytes.`);T.add(r),M=i}return n},ee=e=>{let t=A(e);if(!t)throw new Error("CHM_BAD_PATH: the requested entry path is invalid.");let n=P(t);return n.byteOffset===0&&n.byteLength===n.buffer.byteLength?n:n.slice()},te=(e,t)=>{let r=e.toLocaleLowerCase().indexOf(t);if(r<0)return"";let i=Math.max(0,r-72),o=Math.min(e.length,r+t.length+128);return`${i?"\u2026":""}${e.slice(i,o).trim()}${o<e.length?"\u2026":""}`},ne=(e,t,n)=>{let r=z(),i=e.trim().toLocaleLowerCase();if(!i)return{hits:[],inspected:0,truncated:!1};let o=[],a=new Set,s=r.manifest.topics.slice(0,Math.max(1,n)),l=0;d("search",0,s.length);for(let c of s){if(o.length>=t)break;let u=A(c.path);if(!u||a.has(u.toLocaleLowerCase()))continue;a.add(u.toLocaleLowerCase()),l+=1;let g=c.title.toLocaleLowerCase().includes(i),m="";try{let x=P(u),U=N(v(x,r.manifest.encoding));m=te(U,i)}catch(x){if(x instanceof Error&&x.message.startsWith("CHM_LIMIT_EXCEEDED:"))throw x}(g||m)&&o.push({title:c.title||u,path:u,snippet:m,titleMatch:g}),l%64===0&&d("search",l,s.length)}return d("search",l,s.length),{hits:o,inspected:l,truncated:s.length<r.manifest.topics.length||o.length>=t}};$.addEventListener("message",async e=>{let t=e.data;try{if(t.type==="open"){let n=await Q(t);p({id:t.id,ok:!0,type:"open",...n});return}if(t.type==="read"){let n=ee(t.path);p({id:t.id,ok:!0,type:"read",data:n},[n.buffer]);return}if(t.type==="search"){let n=ne(t.query,Math.max(1,t.limit),Math.max(1,t.maxTopics));p({id:t.id,ok:!0,type:"search",...n});return}S(),p({id:t.id,ok:!0,type:"close"})}catch(n){t.type==="open"&&S(),p({id:t.id,ok:!1,type:t.type,error:Z(n)})}});