@difizen/libro-rendermime 0.0.2-alpha.0

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/es/components/html-render.d.ts +6 -0
  4. package/es/components/html-render.d.ts.map +1 -0
  5. package/es/components/html-render.js +39 -0
  6. package/es/components/image-render.d.ts +6 -0
  7. package/es/components/image-render.d.ts.map +1 -0
  8. package/es/components/image-render.js +36 -0
  9. package/es/components/index.d.ts +7 -0
  10. package/es/components/index.d.ts.map +1 -0
  11. package/es/components/index.js +6 -0
  12. package/es/components/latex-render.d.ts +6 -0
  13. package/es/components/latex-render.d.ts.map +1 -0
  14. package/es/components/latex-render.js +23 -0
  15. package/es/components/markdown-render.d.ts +6 -0
  16. package/es/components/markdown-render.d.ts.map +1 -0
  17. package/es/components/markdown-render.js +38 -0
  18. package/es/components/svg-render.d.ts +6 -0
  19. package/es/components/svg-render.d.ts.map +1 -0
  20. package/es/components/svg-render.js +33 -0
  21. package/es/components/text-render.d.ts +9 -0
  22. package/es/components/text-render.d.ts.map +1 -0
  23. package/es/components/text-render.js +44 -0
  24. package/es/index.d.ts +7 -0
  25. package/es/index.d.ts.map +1 -0
  26. package/es/index.js +6 -0
  27. package/es/renderers.d.ts +42 -0
  28. package/es/renderers.d.ts.map +1 -0
  29. package/es/renderers.js +327 -0
  30. package/es/rendermime-factory.d.ts +30 -0
  31. package/es/rendermime-factory.d.ts.map +1 -0
  32. package/es/rendermime-factory.js +73 -0
  33. package/es/rendermime-module.d.ts +3 -0
  34. package/es/rendermime-module.d.ts.map +1 -0
  35. package/es/rendermime-module.js +11 -0
  36. package/es/rendermime-protocol.d.ts +405 -0
  37. package/es/rendermime-protocol.d.ts.map +1 -0
  38. package/es/rendermime-protocol.js +60 -0
  39. package/es/rendermime-registry.d.ts +135 -0
  40. package/es/rendermime-registry.d.ts.map +1 -0
  41. package/es/rendermime-registry.js +392 -0
  42. package/es/rendermime-utils.d.ts +87 -0
  43. package/es/rendermime-utils.d.ts.map +1 -0
  44. package/es/rendermime-utils.js +603 -0
  45. package/package.json +61 -0
  46. package/src/components/html-render.tsx +42 -0
  47. package/src/components/image-render.tsx +46 -0
  48. package/src/components/index.ts +6 -0
  49. package/src/components/latex-render.tsx +30 -0
  50. package/src/components/markdown-render.tsx +42 -0
  51. package/src/components/svg-render.tsx +38 -0
  52. package/src/components/text-render.tsx +51 -0
  53. package/src/index.ts +6 -0
  54. package/src/renderers.ts +325 -0
  55. package/src/rendermime-factory.ts +92 -0
  56. package/src/rendermime-module.ts +19 -0
  57. package/src/rendermime-protocol.ts +516 -0
  58. package/src/rendermime-registry.ts +301 -0
  59. package/src/rendermime-utils.ts +665 -0
@@ -0,0 +1,665 @@
1
+ import type {
2
+ IExecuteResult,
3
+ IMimeBundle,
4
+ IOutput,
5
+ ISanitizer,
6
+ PartialJSONObject,
7
+ PartialJSONValue,
8
+ ReadonlyPartialJSONObject,
9
+ } from '@difizen/libro-common';
10
+ import {
11
+ isDisplayData,
12
+ isDisplayUpdate,
13
+ isError,
14
+ isExecuteResult,
15
+ isPrimitive,
16
+ isStream,
17
+ URL,
18
+ } from '@difizen/libro-common';
19
+ import { URI } from '@difizen/mana-app';
20
+ import escape from 'lodash.escape';
21
+
22
+ import type { ILinkHandler, IResolver, RankMap } from './rendermime-protocol.js';
23
+
24
+ /**
25
+ * Get the data from a notebook output.
26
+ */
27
+ export function getData(output: IOutput): PartialJSONObject {
28
+ let bundle: IMimeBundle = {};
29
+ if (isExecuteResult(output) || isDisplayData(output) || isDisplayUpdate(output)) {
30
+ bundle = (output as IExecuteResult).data;
31
+ } else if (isStream(output)) {
32
+ if (output.name === 'stderr') {
33
+ bundle['application/vnd.jupyter.stderr'] = output.text;
34
+ } else {
35
+ bundle['application/vnd.jupyter.stdout'] = output.text;
36
+ }
37
+ } else if (isError(output)) {
38
+ bundle['application/vnd.jupyter.error'] = output;
39
+ const traceback = output.traceback.join('\n');
40
+ bundle['application/vnd.jupyter.stderr'] =
41
+ traceback || `${output.ename}: ${output.evalue}`;
42
+ }
43
+ return convertBundle(bundle);
44
+ }
45
+
46
+ /**
47
+ * Extract a value from a JSONObject.
48
+ */
49
+ export function extract(
50
+ value: ReadonlyPartialJSONObject,
51
+ key: string,
52
+ ): PartialJSONValue | undefined {
53
+ const item = value[key];
54
+ if (item === undefined || isPrimitive(item)) {
55
+ return item;
56
+ }
57
+ return JSON.parse(JSON.stringify(item));
58
+ }
59
+
60
+ /**
61
+ * Convert a mime bundle to mime data.
62
+ */
63
+ function convertBundle(bundle: IMimeBundle): PartialJSONObject {
64
+ const map: PartialJSONObject = Object.create(null);
65
+ for (const mimeType in bundle) {
66
+ map[mimeType] = extract(bundle, mimeType);
67
+ }
68
+ return map;
69
+ }
70
+
71
+ const ANSI_COLORS = [
72
+ 'ansi-black',
73
+ 'ansi-red',
74
+ 'ansi-green',
75
+ 'ansi-yellow',
76
+ 'ansi-blue',
77
+ 'ansi-magenta',
78
+ 'ansi-cyan',
79
+ 'ansi-white',
80
+ 'ansi-black-intense',
81
+ 'ansi-red-intense',
82
+ 'ansi-green-intense',
83
+ 'ansi-yellow-intense',
84
+ 'ansi-blue-intense',
85
+ 'ansi-magenta-intense',
86
+ 'ansi-cyan-intense',
87
+ 'ansi-white-intense',
88
+ ];
89
+
90
+ /**
91
+ * Create HTML tags for a string with given foreground, background etc. and
92
+ * add them to the `out` array.
93
+ */
94
+ function pushColoredChunk(
95
+ chunk: string,
96
+ fg: number | number[],
97
+ bg: number | number[],
98
+ bold: boolean,
99
+ underline: boolean,
100
+ inverse: boolean,
101
+ out: string[],
102
+ ): void {
103
+ let _fg = fg;
104
+ let _bg = bg;
105
+ if (chunk) {
106
+ const classes = [];
107
+ const styles = [];
108
+
109
+ if (bold && typeof _fg === 'number' && 0 <= _fg && _fg < 8) {
110
+ _fg += 8; // Bold text uses "intense" colors
111
+ }
112
+ if (inverse) {
113
+ [_fg, _bg] = [_bg, _fg];
114
+ }
115
+
116
+ if (typeof _fg === 'number') {
117
+ classes.push(ANSI_COLORS[_fg] + '-fg');
118
+ } else if (_fg.length) {
119
+ styles.push(`color: rgb(${_fg})`);
120
+ } else if (inverse) {
121
+ classes.push('ansi-default-inverse-fg');
122
+ }
123
+
124
+ if (typeof _bg === 'number') {
125
+ classes.push(ANSI_COLORS[_bg] + '-bg');
126
+ } else if (_bg.length) {
127
+ styles.push(`background-color: rgb(${_bg})`);
128
+ } else if (inverse) {
129
+ classes.push('ansi-default-inverse-bg');
130
+ }
131
+
132
+ if (bold) {
133
+ classes.push('ansi-bold');
134
+ }
135
+
136
+ if (underline) {
137
+ classes.push('ansi-underline');
138
+ }
139
+
140
+ if (classes.length || styles.length) {
141
+ out.push('<span');
142
+ if (classes.length) {
143
+ out.push(` class="${classes.join(' ')}"`);
144
+ }
145
+ if (styles.length) {
146
+ out.push(` style="${styles.join('; ')}"`);
147
+ }
148
+ out.push('>');
149
+ out.push(chunk);
150
+ out.push('</span>');
151
+ } else {
152
+ out.push(chunk);
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Convert ANSI extended colors to R/G/B triple.
159
+ */
160
+ function getExtendedColors(numbers: number[]): number | number[] {
161
+ let r;
162
+ let g;
163
+ let b;
164
+ const n = numbers.shift();
165
+ if (n === 2 && numbers.length >= 3) {
166
+ // 24-bit RGB
167
+ r = numbers.shift()!;
168
+ g = numbers.shift()!;
169
+ b = numbers.shift()!;
170
+ if ([r, g, b].some((c) => c < 0 || 255 < c)) {
171
+ throw new RangeError('Invalid range for RGB colors');
172
+ }
173
+ } else if (n === 5 && numbers.length >= 1) {
174
+ // 256 colors
175
+ const idx = numbers.shift()!;
176
+ if (idx < 0) {
177
+ throw new RangeError('Color index must be >= 0');
178
+ } else if (idx < 16) {
179
+ // 16 default terminal colors
180
+ return idx;
181
+ } else if (idx < 232) {
182
+ // 6x6x6 color cube, see https://stackoverflow.com/a/27165165/500098
183
+ r = Math.floor((idx - 16) / 36);
184
+ r = r > 0 ? 55 + r * 40 : 0;
185
+ g = Math.floor(((idx - 16) % 36) / 6);
186
+ g = g > 0 ? 55 + g * 40 : 0;
187
+ b = (idx - 16) % 6;
188
+ b = b > 0 ? 55 + b * 40 : 0;
189
+ } else if (idx < 256) {
190
+ // grayscale, see https://stackoverflow.com/a/27165165/500098
191
+ r = g = b = (idx - 232) * 10 + 8;
192
+ } else {
193
+ throw new RangeError('Color index must be < 256');
194
+ }
195
+ } else {
196
+ throw new RangeError('Invalid extended color specification');
197
+ }
198
+ return [r, g, b];
199
+ }
200
+ /**
201
+ * Transform ANSI color escape codes into HTML <span> tags with CSS
202
+ * classes such as "ansi-green-intense-fg".
203
+ * The actual colors used are set in the CSS file.
204
+ * This also removes non-color escape sequences.
205
+ * This is supposed to have the same behavior as nbconvert.filters.ansi2html()
206
+ */
207
+ export function ansiSpan(str: string): string {
208
+ const ansiRe = /\x1b\[(.*?)([@-~])/g; // eslint-disable-line no-control-regex
209
+ let fg: number | number[] = [];
210
+ let bg: number | number[] = [];
211
+ let bold = false;
212
+ let underline = false;
213
+ let inverse = false;
214
+ let match;
215
+ const out: string[] = [];
216
+ const numbers = [];
217
+ let start = 0;
218
+
219
+ let _str = escape(str);
220
+
221
+ _str += '\x1b[m'; // Ensure markup for trailing text
222
+ // tslint:disable-next-line
223
+ while ((match = ansiRe.exec(_str))) {
224
+ if (match[2] === 'm') {
225
+ const items = match[1].split(';');
226
+ for (let i = 0; i < items.length; i++) {
227
+ const item = items[i];
228
+ if (item === '') {
229
+ numbers.push(0);
230
+ } else if (item.search(/^\d+$/) !== -1) {
231
+ numbers.push(parseInt(item, 10));
232
+ } else {
233
+ // Ignored: Invalid color specification
234
+ numbers.length = 0;
235
+ break;
236
+ }
237
+ }
238
+ } else {
239
+ // Ignored: Not a color code
240
+ }
241
+ const chunk = _str.substring(start, match.index);
242
+ pushColoredChunk(chunk, fg, bg, bold, underline, inverse, out);
243
+ start = ansiRe.lastIndex;
244
+
245
+ while (numbers.length) {
246
+ const n = numbers.shift();
247
+ switch (n) {
248
+ case 0:
249
+ fg = bg = [];
250
+ bold = false;
251
+ underline = false;
252
+ inverse = false;
253
+ break;
254
+ case 1:
255
+ case 5:
256
+ bold = true;
257
+ break;
258
+ case 4:
259
+ underline = true;
260
+ break;
261
+ case 7:
262
+ inverse = true;
263
+ break;
264
+ case 21:
265
+ case 22:
266
+ bold = false;
267
+ break;
268
+ case 24:
269
+ underline = false;
270
+ break;
271
+ case 27:
272
+ inverse = false;
273
+ break;
274
+ case 30:
275
+ case 31:
276
+ case 32:
277
+ case 33:
278
+ case 34:
279
+ case 35:
280
+ case 36:
281
+ case 37:
282
+ fg = n - 30;
283
+ break;
284
+ case 38:
285
+ try {
286
+ fg = getExtendedColors(numbers);
287
+ } catch (e) {
288
+ numbers.length = 0;
289
+ }
290
+ break;
291
+ case 39:
292
+ fg = [];
293
+ break;
294
+ case 40:
295
+ case 41:
296
+ case 42:
297
+ case 43:
298
+ case 44:
299
+ case 45:
300
+ case 46:
301
+ case 47:
302
+ bg = n - 40;
303
+ break;
304
+ case 48:
305
+ try {
306
+ bg = getExtendedColors(numbers);
307
+ } catch (e) {
308
+ numbers.length = 0;
309
+ }
310
+ break;
311
+ case 49:
312
+ bg = [];
313
+ break;
314
+ case 90:
315
+ case 91:
316
+ case 92:
317
+ case 93:
318
+ case 94:
319
+ case 95:
320
+ case 96:
321
+ case 97:
322
+ fg = n - 90 + 8;
323
+ break;
324
+ case 100:
325
+ case 101:
326
+ case 102:
327
+ case 103:
328
+ case 104:
329
+ case 105:
330
+ case 106:
331
+ case 107:
332
+ bg = n - 100 + 8;
333
+ break;
334
+ default:
335
+ // Unknown codes are ignored
336
+ }
337
+ }
338
+ }
339
+ return out.join('');
340
+ }
341
+ /**
342
+ * Replace URLs with links.
343
+ *
344
+ * @param content - The text content of a node.
345
+ *
346
+ * @returns A list of text nodes and anchor elements.
347
+ */
348
+ export function autolink(content: string): (HTMLAnchorElement | Text)[] {
349
+ // Taken from Visual Studio Code:
350
+ // https://github.com/microsoft/vscode/blob/9f709d170b06e991502153f281ec3c012add2e42/src/vs/workbench/contrib/debug/browser/linkDetector.ts#L17-L18
351
+ const controlCodes = '\\u0000-\\u0020\\u007f-\\u009f';
352
+ const webLinkRegex = new RegExp(
353
+ '(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' +
354
+ controlCodes +
355
+ '"]{2,}[^\\s' +
356
+ controlCodes +
357
+ '"\'(){}\\[\\],:;.!?]',
358
+ 'ug',
359
+ );
360
+
361
+ const nodes = [];
362
+ let lastIndex = 0;
363
+
364
+ let match: RegExpExecArray | null;
365
+ while (null !== (match = webLinkRegex.exec(content))) {
366
+ if (match.index !== lastIndex) {
367
+ nodes.push(document.createTextNode(content.slice(lastIndex, match.index)));
368
+ }
369
+ let url = match[0];
370
+ // Special case when the URL ends with ">" or "<"
371
+ const lastChars = url.slice(-1);
372
+ const endsWithGtLt = ['>', '<'].indexOf(lastChars) !== -1;
373
+ const len = endsWithGtLt ? url.length - 1 : url.length;
374
+ const anchor = document.createElement('a');
375
+ url = url.slice(0, len);
376
+ anchor.href = url.startsWith('www.') ? 'https://' + url : url;
377
+ anchor.rel = 'noopener';
378
+ anchor.target = '_blank';
379
+ anchor.appendChild(document.createTextNode(url.slice(0, len)));
380
+ nodes.push(anchor);
381
+ lastIndex = match.index + len;
382
+ }
383
+ if (lastIndex !== content.length) {
384
+ nodes.push(document.createTextNode(content.slice(lastIndex, content.length)));
385
+ }
386
+ return nodes;
387
+ }
388
+ export interface IRenderOptions {
389
+ /**
390
+ * The host node for the text content.
391
+ */
392
+ host: HTMLElement;
393
+
394
+ /**
395
+ * The html sanitizer for untrusted source.
396
+ */
397
+ sanitizer: ISanitizer;
398
+
399
+ /**
400
+ * The source text to render.
401
+ */
402
+ source: string;
403
+ }
404
+ /**
405
+ * Split a shallow node (node without nested nodes inside) at a given text content position.
406
+ *
407
+ * @param node the shallow node to be split
408
+ * @param at the position in textContent at which the split should occur
409
+ */
410
+ export function splitShallowNode<T extends Node>(
411
+ node: T,
412
+ at: number,
413
+ ): { pre: T; post: T } {
414
+ const pre = node.cloneNode() as T;
415
+ pre.textContent = node.textContent?.substr(0, at) as string;
416
+ const post = node.cloneNode() as T;
417
+ post.textContent = node.textContent?.substr(at) as string;
418
+ return {
419
+ pre: pre,
420
+ post: post,
421
+ };
422
+ }
423
+
424
+ export function sortedTypes(map: RankMap): string[] {
425
+ return Object.keys(map).sort((a, b) => {
426
+ const p1 = map[a];
427
+ const p2 = map[b];
428
+ if (p1.rank !== p2.rank) {
429
+ return p1.rank - p2.rank;
430
+ }
431
+ return p1.id - p2.id;
432
+ });
433
+ }
434
+
435
+ /**
436
+ * Eval the script tags contained in a host populated by `innerHTML`.
437
+ *
438
+ * When script tags are created via `innerHTML`, the browser does not
439
+ * evaluate them when they are added to the page. This function works
440
+ * around that by creating new equivalent script nodes manually, and
441
+ * replacing the originals.
442
+ */
443
+ export function evalInnerHTMLScriptTags(host: HTMLElement): void {
444
+ // Create a snapshot of the current script nodes.
445
+ const scripts = Array.from(host.getElementsByTagName('script'));
446
+
447
+ // Loop over each script node.
448
+ for (const script of scripts) {
449
+ // Skip any scripts which no longer have a parent.
450
+ if (!script.parentNode) {
451
+ continue;
452
+ }
453
+
454
+ // Create a new script node which will be clone.
455
+ const clone = document.createElement('script');
456
+
457
+ // Copy the attributes into the clone.
458
+ const attrs = script.attributes;
459
+ for (let i = 0, n = attrs.length; i < n; ++i) {
460
+ const { name, value } = attrs[i];
461
+ clone.setAttribute(name, value);
462
+ }
463
+
464
+ // Copy the text content into the clone.
465
+ clone.textContent = script.textContent;
466
+
467
+ // Replace the old script in the parent.
468
+ script.parentNode.replaceChild(clone, script);
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Handle the default behavior of nodes.
474
+ */
475
+ export function handleDefaults(node: HTMLElement, resolver?: IResolver | null): void {
476
+ // Handle anchor elements.
477
+ const anchors = node.getElementsByTagName('a');
478
+ for (let i = 0; i < anchors.length; i++) {
479
+ const el = anchors[i];
480
+ // skip when processing a elements inside svg
481
+ // which are of type SVGAnimatedString
482
+ if (!(el instanceof HTMLAnchorElement)) {
483
+ continue;
484
+ }
485
+ const path = el.href;
486
+ const isLocal =
487
+ resolver && resolver.isLocal ? resolver.isLocal(path) : URL.isLocal(path);
488
+ // set target attribute if not already present
489
+ if (!el.target) {
490
+ el.target = isLocal ? '_self' : '_blank';
491
+ }
492
+ // set rel as 'noopener' for non-local anchors
493
+ if (!isLocal) {
494
+ el.rel = 'noopener';
495
+ }
496
+ }
497
+
498
+ // Handle image elements.
499
+ const imgs = node.getElementsByTagName('img');
500
+ for (let i = 0; i < imgs.length; i++) {
501
+ if (!imgs[i].alt) {
502
+ imgs[i].alt = 'Image';
503
+ }
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Resolve the relative urls in element `src` and `href` attributes.
509
+ *
510
+ * @param node - The head html element.
511
+ *
512
+ * @param resolver - A url resolver.
513
+ *
514
+ * @param linkHandler - An optional link handler for nodes.
515
+ *
516
+ * @returns a promise fulfilled when the relative urls have been resolved.
517
+ */
518
+ export function handleUrls(
519
+ node: HTMLElement,
520
+ resolver: IResolver,
521
+ linkHandler: ILinkHandler | null,
522
+ ): Promise<void> {
523
+ // Set up an array to collect promises.
524
+ const promises: Promise<void>[] = [];
525
+
526
+ // Handle HTML Elements with src attributes.
527
+ const nodes = node.querySelectorAll('*[src]');
528
+ for (let i = 0; i < nodes.length; i++) {
529
+ promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
530
+ }
531
+
532
+ // Handle anchor elements.
533
+ const anchors = node.getElementsByTagName('a');
534
+ for (let i = 0; i < anchors.length; i++) {
535
+ promises.push(handleAnchor(anchors[i], resolver, linkHandler));
536
+ }
537
+
538
+ // Handle link elements.
539
+ const links = node.getElementsByTagName('link');
540
+ for (let i = 0; i < links.length; i++) {
541
+ promises.push(handleAttr(links[i], 'href', resolver));
542
+ }
543
+
544
+ // Wait on all promises.
545
+ return Promise.all(promises).then(() => undefined);
546
+ }
547
+
548
+ /**
549
+ * Handle a node with a `src` or `href` attribute.
550
+ */
551
+ async function handleAttr(
552
+ node: HTMLElement,
553
+ name: 'src' | 'href',
554
+ resolver: IResolver,
555
+ ): Promise<void> {
556
+ const source = node.getAttribute(name) || '';
557
+ const isLocal = resolver.isLocal ? resolver.isLocal(source) : URL.isLocal(source);
558
+ if (!source || !isLocal) {
559
+ return;
560
+ }
561
+ try {
562
+ const urlPath = await resolver.resolveUrl(source);
563
+ let url = await resolver.getDownloadUrl(urlPath);
564
+ if (new URI(url).scheme !== 'data:') {
565
+ // Bust caching for local src attrs.
566
+ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
567
+ url += (/\?/.test(url) ? '&' : '?') + new Date().getTime();
568
+ }
569
+ node.setAttribute(name, url);
570
+ } catch (err) {
571
+ // If there was an error getting the url,
572
+ // just make it an empty link and report the error.
573
+ node.setAttribute(name, '');
574
+ throw err;
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Handle an anchor node.
580
+ */
581
+ function handleAnchor(
582
+ anchor: HTMLAnchorElement,
583
+ resolver: IResolver,
584
+ linkHandler: ILinkHandler | null,
585
+ ): Promise<void> {
586
+ // Get the link path without the location prepended.
587
+ // (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
588
+ let href = anchor.getAttribute('href') || '';
589
+ const isLocal = resolver.isLocal ? resolver.isLocal(href) : URL.isLocal(href);
590
+ // Bail if it is not a file-like url.
591
+ if (!href || !isLocal) {
592
+ return Promise.resolve(undefined);
593
+ }
594
+ // Remove the hash until we can handle it.
595
+ const hash = anchor.hash;
596
+ if (hash) {
597
+ // Handle internal link in the file.
598
+ if (hash === href) {
599
+ anchor.target = '_self';
600
+ return Promise.resolve(undefined);
601
+ }
602
+ // For external links, remove the hash until we have hash handling.
603
+ href = href.replace(hash, '');
604
+ }
605
+ // Get the appropriate file path.
606
+ return resolver
607
+ .resolveUrl(href)
608
+ .then((urlPath) => {
609
+ // decode encoded url from url to api path
610
+ const path = decodeURIComponent(urlPath);
611
+ // Handle the click override.
612
+ if (linkHandler) {
613
+ linkHandler.handleLink(anchor, path, hash);
614
+ }
615
+ // Get the appropriate file download path.
616
+ return resolver.getDownloadUrl(urlPath);
617
+ })
618
+ .then((url) => {
619
+ // Set the visible anchor.
620
+ anchor.href = url + hash;
621
+ return;
622
+ })
623
+ .catch(() => {
624
+ // If there was an error getting the url,
625
+ // just make it an empty link.
626
+ anchor.href = '';
627
+ });
628
+ }
629
+
630
+ /**
631
+ * Create a normalized id for a header element.
632
+ *
633
+ * @param header Header element
634
+ * @returns Normalized id
635
+ */
636
+ export function createHeaderId(header: Element): string {
637
+ return (header.textContent ?? '').replace(/ /g, '-');
638
+ }
639
+
640
+ /**
641
+ * Apply ids to headers.
642
+ */
643
+ // export function headerAnchors(node: HTMLElement): void {
644
+ // const headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
645
+ // for (const headerType of headerNames) {
646
+ // const headers = node.getElementsByTagName(headerType);
647
+ // for (let i = 0; i < headers.length; i++) {
648
+ // const header = headers[i];
649
+ // header.id = createHeaderId(header);
650
+ // const anchor = document.createElement('a');
651
+ // anchor.target = '_self';
652
+ // anchor.textContent = '¶';
653
+ // anchor.href = '#' + header.id;
654
+ // anchor.classList.add('libro-InternalAnchorLink');
655
+ // header.appendChild(anchor);
656
+ // }
657
+ // }
658
+ // }
659
+ // export function sessionConnection(
660
+ // s: Session.ISessionConnection | ISessionContext,
661
+ // ): Session.ISessionConnection | null {
662
+ // return (s as any).sessionChanged
663
+ // ? (s as ISessionContext).session
664
+ // : (s as Session.ISessionConnection);
665
+ // }