UNPKG

40.2 kBJavaScriptView Raw
1/* -----------------------------------------------------------------------------
2| Copyright (c) Jupyter Development Team.
3| Distributed under the terms of the Modified BSD License.
4|----------------------------------------------------------------------------*/
5import { URLExt } from '@jupyterlab/coreutils';
6import { nullTranslator } from '@jupyterlab/translation';
7import escape from 'lodash.escape';
8import { removeMath, replaceMath } from './latex';
9/**
10 * Render HTML into a host node.
11 *
12 * @param options - The options for rendering.
13 *
14 * @returns A promise which resolves when rendering is complete.
15 */
16export function renderHTML(options) {
17 // Unpack the options.
18 let { host, source, trusted, sanitizer, resolver, linkHandler, shouldTypeset, latexTypesetter, translator } = options;
19 translator = translator || nullTranslator;
20 const trans = translator === null || translator === void 0 ? void 0 : translator.load('jupyterlab');
21 let originalSource = source;
22 // Bail early if the source is empty.
23 if (!source) {
24 host.textContent = '';
25 return Promise.resolve(undefined);
26 }
27 // Sanitize the source if it is not trusted. This removes all
28 // `<script>` tags as well as other potentially harmful HTML.
29 if (!trusted) {
30 originalSource = `${source}`;
31 source = sanitizer.sanitize(source);
32 }
33 // Set the inner HTML of the host.
34 host.innerHTML = source;
35 if (host.getElementsByTagName('script').length > 0) {
36 // If output it trusted, eval any script tags contained in the HTML.
37 // This is not done automatically by the browser when script tags are
38 // created by setting `innerHTML`.
39 if (trusted) {
40 Private.evalInnerHTMLScriptTags(host);
41 }
42 else {
43 const container = document.createElement('div');
44 const warning = document.createElement('pre');
45 warning.textContent = trans.__('This HTML output contains inline scripts. Are you sure that you want to run arbitrary Javascript within your JupyterLab session?');
46 const runButton = document.createElement('button');
47 runButton.textContent = trans.__('Run');
48 runButton.onclick = event => {
49 host.innerHTML = originalSource;
50 Private.evalInnerHTMLScriptTags(host);
51 if (host.firstChild) {
52 host.removeChild(host.firstChild);
53 }
54 };
55 container.appendChild(warning);
56 container.appendChild(runButton);
57 host.insertBefore(container, host.firstChild);
58 }
59 }
60 // Handle default behavior of nodes.
61 Private.handleDefaults(host, resolver);
62 // Patch the urls if a resolver is available.
63 let promise;
64 if (resolver) {
65 promise = Private.handleUrls(host, resolver, linkHandler);
66 }
67 else {
68 promise = Promise.resolve(undefined);
69 }
70 // Return the final rendered promise.
71 return promise.then(() => {
72 if (shouldTypeset && latexTypesetter) {
73 latexTypesetter.typeset(host);
74 }
75 });
76}
77/**
78 * Render an image into a host node.
79 *
80 * @param options - The options for rendering.
81 *
82 * @returns A promise which resolves when rendering is complete.
83 */
84export function renderImage(options) {
85 // Unpack the options.
86 const { host, mimeType, source, width, height, needsBackground, unconfined } = options;
87 // Clear the content in the host.
88 host.textContent = '';
89 // Create the image element.
90 const img = document.createElement('img');
91 // Set the source of the image.
92 img.src = `data:${mimeType};base64,${source}`;
93 // Set the size of the image if provided.
94 if (typeof height === 'number') {
95 img.height = height;
96 }
97 if (typeof width === 'number') {
98 img.width = width;
99 }
100 if (needsBackground === 'light') {
101 img.classList.add('jp-needs-light-background');
102 }
103 else if (needsBackground === 'dark') {
104 img.classList.add('jp-needs-dark-background');
105 }
106 if (unconfined === true) {
107 img.classList.add('jp-mod-unconfined');
108 }
109 // Add the image to the host.
110 host.appendChild(img);
111 // Return the rendered promise.
112 return Promise.resolve(undefined);
113}
114/**
115 * Render LaTeX into a host node.
116 *
117 * @param options - The options for rendering.
118 *
119 * @returns A promise which resolves when rendering is complete.
120 */
121export function renderLatex(options) {
122 // Unpack the options.
123 const { host, source, shouldTypeset, latexTypesetter } = options;
124 // Set the source on the node.
125 host.textContent = source;
126 // Typeset the node if needed.
127 if (shouldTypeset && latexTypesetter) {
128 latexTypesetter.typeset(host);
129 }
130 // Return the rendered promise.
131 return Promise.resolve(undefined);
132}
133/**
134 * Render Markdown into a host node.
135 *
136 * @param options - The options for rendering.
137 *
138 * @returns A promise which resolves when rendering is complete.
139 */
140export async function renderMarkdown(options) {
141 // Unpack the options.
142 const { host, source, markdownParser, ...others } = options;
143 // Clear the content if there is no source.
144 if (!source) {
145 host.textContent = '';
146 return;
147 }
148 let html = '';
149 if (markdownParser) {
150 // Separate math from normal markdown text.
151 const parts = removeMath(source);
152 // Convert the markdown to HTML.
153 html = await markdownParser.render(parts['text']);
154 // Replace math.
155 html = replaceMath(html, parts['math']);
156 }
157 else {
158 // Fallback if the application does not have any markdown parser.
159 html = `<pre>${source}</pre>`;
160 }
161 // Render HTML.
162 await renderHTML({
163 host,
164 source: html,
165 ...others
166 });
167 // Apply ids to the header nodes.
168 Private.headerAnchors(host);
169}
170/**
171 * The namespace for the `renderMarkdown` function statics.
172 */
173(function (renderMarkdown) {
174 /**
175 * Create a normalized id for a header element.
176 *
177 * @param header Header element
178 * @returns Normalized id
179 */
180 function createHeaderId(header) {
181 var _a;
182 return ((_a = header.textContent) !== null && _a !== void 0 ? _a : '').replace(/ /g, '-');
183 }
184 renderMarkdown.createHeaderId = createHeaderId;
185})(renderMarkdown || (renderMarkdown = {}));
186/**
187 * Render SVG into a host node.
188 *
189 * @param options - The options for rendering.
190 *
191 * @returns A promise which resolves when rendering is complete.
192 */
193export function renderSVG(options) {
194 // Unpack the options.
195 let { host, source, trusted, unconfined } = options;
196 // Clear the content if there is no source.
197 if (!source) {
198 host.textContent = '';
199 return Promise.resolve(undefined);
200 }
201 // Display a message if the source is not trusted.
202 if (!trusted) {
203 host.textContent =
204 'Cannot display an untrusted SVG. Maybe you need to run the cell?';
205 return Promise.resolve(undefined);
206 }
207 // Add missing SVG namespace (if actually missing)
208 const patt = '<svg[^>]+xmlns=[^>]+svg';
209 if (source.search(patt) < 0) {
210 source = source.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
211 }
212 // Render in img so that user can save it easily
213 const img = new Image();
214 img.src = `data:image/svg+xml,${encodeURIComponent(source)}`;
215 host.appendChild(img);
216 if (unconfined === true) {
217 host.classList.add('jp-mod-unconfined');
218 }
219 return Promise.resolve();
220}
221var ILinker;
222(function (ILinker) {
223 // Taken from Visual Studio Code:
224 // https://github.com/microsoft/vscode/blob/9f709d170b06e991502153f281ec3c012add2e42/src/vs/workbench/contrib/debug/browser/linkDetector.ts#L17-L18
225 const controlCodes = '\\u0000-\\u0020\\u007f-\\u009f';
226 ILinker.webLinkRegex = new RegExp('(?<path>(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' +
227 controlCodes +
228 '"]{2,}[^\\s' +
229 controlCodes +
230 '"\'(){}\\[\\],:;.!?])', 'ug');
231 // Taken from Visual Studio Code:
232 // https://github.com/microsoft/vscode/blob/3e407526a1e2ff22cacb69c7e353e81a12f41029/extensions/notebook-renderers/src/linkify.ts#L9
233 const winAbsPathRegex = /(?:[a-zA-Z]:(?:(?:\\|\/)[\w\.-]*)+)/;
234 const winRelPathRegex = /(?:(?:\~|\.)(?:(?:\\|\/)[\w\.-]*)+)/;
235 const winPathRegex = new RegExp(`(${winAbsPathRegex.source}|${winRelPathRegex.source})`);
236 const posixPathRegex = /((?:\~|\.)?(?:\/[\w\.-]*)+)/;
237 const lineColumnRegex = /(?:(?:\:|", line )(?<line>[\d]+))?(?:\:(?<column>[\d]+))?/;
238 // TODO: this ought to come from kernel (browser may be on a different OS).
239 const isWindows = navigator.userAgent.indexOf('Windows') >= 0;
240 ILinker.pathLinkRegex = new RegExp(`(?<path>${isWindows ? winPathRegex.source : posixPathRegex.source})${lineColumnRegex.source}`, 'g');
241})(ILinker || (ILinker = {}));
242/**
243 * Linker for web URLs.
244 */
245class WebLinker {
246 constructor() {
247 this.regex = ILinker.webLinkRegex;
248 }
249 createAnchor(url, label) {
250 const anchor = document.createElement('a');
251 anchor.href = url.startsWith('www.') ? 'https://' + url : url;
252 anchor.rel = 'noopener';
253 anchor.target = '_blank';
254 anchor.appendChild(document.createTextNode(label));
255 return anchor;
256 }
257 processPath(url) {
258 // Special case when the URL ends with ">" or "<"
259 const lastChars = url.slice(-1);
260 const endsWithGtLt = ['>', '<'].indexOf(lastChars) !== -1;
261 const len = endsWithGtLt ? url.length - 1 : url.length;
262 url = url.slice(0, len);
263 return url;
264 }
265 processLabel(url) {
266 return this.processPath(url);
267 }
268}
269/**
270 * Linker for path URIs.
271 */
272class PathLinker {
273 constructor() {
274 this.regex = ILinker.pathLinkRegex;
275 }
276 createAnchor(path, label, locators) {
277 const anchor = document.createElement('a');
278 // Store the path in dataset.
279 // Do not set `href` - at this point we do not know if the path is valid and
280 // accessible for application (and we want rendering those as links).
281 anchor.dataset.path = path;
282 // Store line using RFC 5147 fragment locator for text/plain files.
283 // It could be expanded to other formats, e.g. based on file extension.
284 const line = parseInt(locators['line'], 10);
285 let locator = !isNaN(line) ? `line=${line - 1}` : '';
286 anchor.dataset.locator = locator;
287 anchor.appendChild(document.createTextNode(label));
288 return anchor;
289 }
290}
291function autolink(content, options) {
292 const linkers = [];
293 if (options.checkWeb) {
294 linkers.push(new WebLinker());
295 }
296 if (options.checkPaths) {
297 linkers.push(new PathLinker());
298 }
299 const nodes = [];
300 // There are two ways to implement competitive regexes:
301 // - two heads (which would need to resolve overlaps), or
302 // - (simpler) divide and recurse (implemented below)
303 const linkify = (content, regexIndex) => {
304 if (regexIndex >= linkers.length) {
305 nodes.push(document.createTextNode(content));
306 return;
307 }
308 const linker = linkers[regexIndex];
309 let match;
310 let currentIndex = 0;
311 const regex = linker.regex;
312 // Reset regex
313 regex.lastIndex = 0;
314 while (null != (match = regex.exec(content))) {
315 const stringBeforeMatch = content.substring(currentIndex, match.index);
316 if (stringBeforeMatch) {
317 linkify(stringBeforeMatch, regexIndex + 1);
318 }
319 const { path, ...locators } = match.groups;
320 const value = linker.processPath ? linker.processPath(path) : path;
321 const label = linker.processLabel
322 ? linker.processLabel(match[0])
323 : match[0];
324 nodes.push(linker.createAnchor(value, label, locators));
325 currentIndex = match.index + label.length;
326 }
327 const stringAfterMatches = content.substring(currentIndex);
328 if (stringAfterMatches) {
329 linkify(stringAfterMatches, regexIndex + 1);
330 }
331 };
332 linkify(content, 0);
333 return nodes;
334}
335/**
336 * Split a shallow node (node without nested nodes inside) at a given text content position.
337 *
338 * @param node the shallow node to be split
339 * @param at the position in textContent at which the split should occur
340 */
341function splitShallowNode(node, at) {
342 var _a, _b;
343 const pre = node.cloneNode();
344 pre.textContent = (_a = node.textContent) === null || _a === void 0 ? void 0 : _a.slice(0, at);
345 const post = node.cloneNode();
346 post.textContent = (_b = node.textContent) === null || _b === void 0 ? void 0 : _b.slice(at);
347 return {
348 pre,
349 post
350 };
351}
352/**
353 * Iterate over some nodes, while tracking cumulative start and end position.
354 */
355function* nodeIter(nodes) {
356 var _a;
357 let start = 0;
358 let end;
359 for (let node of nodes) {
360 end = start + (((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.length) || 0);
361 yield {
362 node,
363 start,
364 end,
365 isText: node.nodeType === Node.TEXT_NODE
366 };
367 start = end;
368 }
369}
370/**
371 * Align two collections of nodes.
372 *
373 * If a text node in one collections spans an element in the other, yield the spanned elements.
374 * Otherwise, split the nodes such that yielded pair start and stop on the same position.
375 */
376function* alignedNodes(a, b) {
377 var _a, _b;
378 let iterA = nodeIter(a);
379 let iterB = nodeIter(b);
380 let nA = iterA.next();
381 let nB = iterB.next();
382 while (!nA.done && !nB.done) {
383 let A = nA.value;
384 let B = nB.value;
385 if (A.isText && A.start <= B.start && A.end >= B.end) {
386 // A is a text element that spans all of B, simply yield B
387 yield [null, B.node];
388 nB = iterB.next();
389 }
390 else if (B.isText && B.start <= A.start && B.end >= A.end) {
391 // B is a text element that spans all of A, simply yield A
392 yield [A.node, null];
393 nA = iterA.next();
394 }
395 else {
396 // There is some intersection, split one, unless they match exactly
397 if (A.end === B.end && A.start === B.start) {
398 yield [A.node, B.node];
399 nA = iterA.next();
400 nB = iterB.next();
401 }
402 else if (A.end > B.end) {
403 /*
404 A |-----[======]---|
405 B |--[======]------|
406 | <- Split A here
407 | <- trim B to start from here if needed
408 */
409 let { pre, post } = splitShallowNode(A.node, B.end - A.start);
410 if (B.start < A.start) {
411 // this node should not be yielded anywhere else, so ok to modify in-place
412 B.node.textContent = (_a = B.node.textContent) === null || _a === void 0 ? void 0 : _a.slice(A.start - B.start);
413 }
414 yield [pre, B.node];
415 // Modify iteration result in-place:
416 A.node = post;
417 A.start = B.end;
418 nB = iterB.next();
419 }
420 else if (B.end > A.end) {
421 let { pre, post } = splitShallowNode(B.node, A.end - B.start);
422 if (A.start < B.start) {
423 // this node should not be yielded anywhere else, so ok to modify in-place
424 A.node.textContent = (_b = A.node.textContent) === null || _b === void 0 ? void 0 : _b.slice(B.start - A.start);
425 }
426 yield [A.node, pre];
427 // Modify iteration result in-place:
428 B.node = post;
429 B.start = A.end;
430 nA = iterA.next();
431 }
432 else {
433 throw new Error(`Unexpected intersection: ${JSON.stringify(A)} ${JSON.stringify(B)}`);
434 }
435 }
436 }
437}
438/**
439 * Render text into a host node.
440 *
441 * @param options - The options for rendering.
442 *
443 * @returns A promise which resolves when rendering is complete.
444 */
445export function renderText(options) {
446 var _a, _b;
447 // Unpack the options.
448 const { host, sanitizer, source } = options;
449 // Create the HTML content.
450 const content = sanitizer.sanitize(Private.ansiSpan(source), {
451 allowedTags: ['span']
452 });
453 // Set the sanitized content for the host node.
454 const pre = document.createElement('pre');
455 pre.innerHTML = content;
456 const preTextContent = pre.textContent;
457 let ret;
458 if (preTextContent) {
459 // Note: only text nodes and span elements should be present after sanitization in the `<pre>` element.
460 const linkedNodes = ((_b = (_a = sanitizer.getAutolink) === null || _a === void 0 ? void 0 : _a.call(sanitizer)) !== null && _b !== void 0 ? _b : true)
461 ? autolink(preTextContent, {
462 checkWeb: true,
463 checkPaths: false
464 })
465 : [document.createTextNode(content)];
466 const preNodes = Array.from(pre.childNodes);
467 ret = mergeNodes(preNodes, linkedNodes);
468 }
469 else {
470 ret = document.createElement('pre');
471 }
472 host.appendChild(ret);
473 // Return the rendered promise.
474 return Promise.resolve(undefined);
475}
476/**
477 * Render error into a host node.
478 *
479 * @param options - The options for rendering.
480 *
481 * @returns A promise which resolves when rendering is complete.
482 */
483export function renderError(options) {
484 var _a, _b;
485 // Unpack the options.
486 const { host, linkHandler, sanitizer, resolver, source } = options;
487 // Create the HTML content.
488 const content = sanitizer.sanitize(Private.ansiSpan(source), {
489 allowedTags: ['span']
490 });
491 // Set the sanitized content for the host node.
492 const pre = document.createElement('pre');
493 pre.innerHTML = content;
494 const preTextContent = pre.textContent;
495 let ret;
496 if (preTextContent) {
497 // Note: only text nodes and span elements should be present after sanitization in the `<pre>` element.
498 const linkedNodes = ((_b = (_a = sanitizer.getAutolink) === null || _a === void 0 ? void 0 : _a.call(sanitizer)) !== null && _b !== void 0 ? _b : true)
499 ? autolink(preTextContent, {
500 checkWeb: true,
501 checkPaths: true
502 })
503 : [document.createTextNode(content)];
504 const preNodes = Array.from(pre.childNodes);
505 ret = mergeNodes(preNodes, linkedNodes);
506 }
507 else {
508 ret = document.createElement('pre');
509 }
510 host.appendChild(ret);
511 // Patch the paths if a resolver is available.
512 let promise;
513 if (resolver) {
514 promise = Private.handlePaths(host, resolver, linkHandler);
515 }
516 else {
517 promise = Promise.resolve(undefined);
518 }
519 // Return the rendered promise.
520 return promise;
521}
522/**
523 * Merge `<span>` nodes from a `<pre>` element with `<a>` nodes from linker.
524 */
525function mergeNodes(preNodes, linkedNodes) {
526 const ret = document.createElement('pre');
527 let inAnchorElement = false;
528 const combinedNodes = [];
529 for (let nodes of alignedNodes(preNodes, linkedNodes)) {
530 if (!nodes[0]) {
531 combinedNodes.push(nodes[1]);
532 inAnchorElement = nodes[1].nodeType !== Node.TEXT_NODE;
533 continue;
534 }
535 else if (!nodes[1]) {
536 combinedNodes.push(nodes[0]);
537 inAnchorElement = false;
538 continue;
539 }
540 let [preNode, linkNode] = nodes;
541 const lastCombined = combinedNodes[combinedNodes.length - 1];
542 // If we are already in an anchor element and the anchor element did not change,
543 // we should insert the node from <pre> which is either Text node or coloured span Element
544 // into the anchor content as a child
545 if (inAnchorElement &&
546 linkNode.href ===
547 lastCombined.href) {
548 lastCombined.appendChild(preNode);
549 }
550 else {
551 // the `linkNode` is either Text or AnchorElement;
552 const isAnchor = linkNode.nodeType !== Node.TEXT_NODE;
553 // if we are NOT about to start an anchor element, just add the pre Node
554 if (!isAnchor) {
555 combinedNodes.push(preNode);
556 inAnchorElement = false;
557 }
558 else {
559 // otherwise start a new anchor; the contents of the `linkNode` and `preNode` should be the same,
560 // so we just put the neatly formatted `preNode` inside the anchor node (`linkNode`)
561 // and append that to combined nodes.
562 linkNode.textContent = '';
563 linkNode.appendChild(preNode);
564 combinedNodes.push(linkNode);
565 inAnchorElement = true;
566 }
567 }
568 }
569 // Do not reuse `pre` element. Clearing out previous children is too slow...
570 for (const child of combinedNodes) {
571 ret.appendChild(child);
572 }
573 return ret;
574}
575/**
576 * The namespace for module implementation details.
577 */
578var Private;
579(function (Private) {
580 /**
581 * Eval the script tags contained in a host populated by `innerHTML`.
582 *
583 * When script tags are created via `innerHTML`, the browser does not
584 * evaluate them when they are added to the page. This function works
585 * around that by creating new equivalent script nodes manually, and
586 * replacing the originals.
587 */
588 function evalInnerHTMLScriptTags(host) {
589 // Create a snapshot of the current script nodes.
590 const scripts = Array.from(host.getElementsByTagName('script'));
591 // Loop over each script node.
592 for (const script of scripts) {
593 // Skip any scripts which no longer have a parent.
594 if (!script.parentNode) {
595 continue;
596 }
597 // Create a new script node which will be clone.
598 const clone = document.createElement('script');
599 // Copy the attributes into the clone.
600 const attrs = script.attributes;
601 for (let i = 0, n = attrs.length; i < n; ++i) {
602 const { name, value } = attrs[i];
603 clone.setAttribute(name, value);
604 }
605 // Copy the text content into the clone.
606 clone.textContent = script.textContent;
607 // Replace the old script in the parent.
608 script.parentNode.replaceChild(clone, script);
609 }
610 }
611 Private.evalInnerHTMLScriptTags = evalInnerHTMLScriptTags;
612 /**
613 * Handle the default behavior of nodes.
614 */
615 function handleDefaults(node, resolver) {
616 // Handle anchor elements.
617 const anchors = node.getElementsByTagName('a');
618 for (let i = 0; i < anchors.length; i++) {
619 const el = anchors[i];
620 // skip when processing a elements inside svg
621 // which are of type SVGAnimatedString
622 if (!(el instanceof HTMLAnchorElement)) {
623 continue;
624 }
625 const path = el.href;
626 const isLocal = resolver && resolver.isLocal
627 ? resolver.isLocal(path)
628 : URLExt.isLocal(path);
629 // set target attribute if not already present
630 if (!el.target) {
631 el.target = isLocal ? '_self' : '_blank';
632 }
633 // set rel as 'noopener' for non-local anchors
634 if (!isLocal) {
635 el.rel = 'noopener';
636 }
637 }
638 // Handle image elements.
639 const imgs = node.getElementsByTagName('img');
640 for (let i = 0; i < imgs.length; i++) {
641 if (!imgs[i].alt) {
642 imgs[i].alt = 'Image';
643 }
644 }
645 }
646 Private.handleDefaults = handleDefaults;
647 /**
648 * Resolve the relative urls in element `src` and `href` attributes.
649 *
650 * @param node - The head html element.
651 *
652 * @param resolver - A url resolver.
653 *
654 * @param linkHandler - An optional link handler for nodes.
655 *
656 * @returns a promise fulfilled when the relative urls have been resolved.
657 */
658 function handleUrls(node, resolver, linkHandler) {
659 // Set up an array to collect promises.
660 const promises = [];
661 // Handle HTML Elements with src attributes.
662 const nodes = node.querySelectorAll('*[src]');
663 for (let i = 0; i < nodes.length; i++) {
664 promises.push(handleAttr(nodes[i], 'src', resolver));
665 }
666 // Handle anchor elements.
667 const anchors = node.getElementsByTagName('a');
668 for (let i = 0; i < anchors.length; i++) {
669 promises.push(handleAnchor(anchors[i], resolver, linkHandler));
670 }
671 // Handle link elements.
672 const links = node.getElementsByTagName('link');
673 for (let i = 0; i < links.length; i++) {
674 promises.push(handleAttr(links[i], 'href', resolver));
675 }
676 // Wait on all promises.
677 return Promise.all(promises).then(() => undefined);
678 }
679 Private.handleUrls = handleUrls;
680 /**
681 * Resolve the paths in `<a>` elements `data` attributes.
682 *
683 * @param node - The head html element.
684 *
685 * @param resolver - A url resolver.
686 *
687 * @param linkHandler - An optional link handler for nodes.
688 *
689 * @returns a promise fulfilled when the relative urls have been resolved.
690 */
691 async function handlePaths(node, resolver, linkHandler) {
692 // Handle anchor elements.
693 const anchors = node.getElementsByTagName('a');
694 for (let i = 0; i < anchors.length; i++) {
695 await handlePathAnchor(anchors[i], resolver, linkHandler);
696 }
697 }
698 Private.handlePaths = handlePaths;
699 /**
700 * Apply ids to headers.
701 */
702 function headerAnchors(node) {
703 const headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
704 for (const headerType of headerNames) {
705 const headers = node.getElementsByTagName(headerType);
706 for (let i = 0; i < headers.length; i++) {
707 const header = headers[i];
708 header.id = renderMarkdown.createHeaderId(header);
709 const anchor = document.createElement('a');
710 anchor.target = '_self';
711 anchor.textContent = '¶';
712 anchor.href = '#' + header.id;
713 anchor.classList.add('jp-InternalAnchorLink');
714 header.appendChild(anchor);
715 }
716 }
717 }
718 Private.headerAnchors = headerAnchors;
719 /**
720 * Handle a node with a `src` or `href` attribute.
721 */
722 async function handleAttr(node, name, resolver) {
723 const source = node.getAttribute(name) || '';
724 const isLocal = resolver.isLocal
725 ? resolver.isLocal(source)
726 : URLExt.isLocal(source);
727 if (!source || !isLocal) {
728 return;
729 }
730 try {
731 const urlPath = await resolver.resolveUrl(source);
732 let url = await resolver.getDownloadUrl(urlPath);
733 if (URLExt.parse(url).protocol !== 'data:') {
734 // Bust caching for local src attrs.
735 // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
736 url += (/\?/.test(url) ? '&' : '?') + new Date().getTime();
737 }
738 node.setAttribute(name, url);
739 }
740 catch (err) {
741 // If there was an error getting the url,
742 // just make it an empty link and report the error.
743 node.setAttribute(name, '');
744 throw err;
745 }
746 }
747 /**
748 * Handle an anchor node.
749 */
750 function handleAnchor(anchor, resolver, linkHandler) {
751 // Get the link path without the location prepended.
752 // (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
753 let href = anchor.getAttribute('href') || '';
754 const isLocal = resolver.isLocal
755 ? resolver.isLocal(href)
756 : URLExt.isLocal(href);
757 // Bail if it is not a file-like url.
758 if (!href || !isLocal) {
759 return Promise.resolve(undefined);
760 }
761 // Remove the hash until we can handle it.
762 const hash = anchor.hash;
763 if (hash) {
764 // Handle internal link in the file.
765 if (hash === href) {
766 anchor.target = '_self';
767 return Promise.resolve(undefined);
768 }
769 // For external links, remove the hash until we have hash handling.
770 href = href.replace(hash, '');
771 }
772 // Get the appropriate file path.
773 return resolver
774 .resolveUrl(href)
775 .then(urlPath => {
776 // decode encoded url from url to api path
777 const path = decodeURIComponent(urlPath);
778 // Handle the click override.
779 if (linkHandler) {
780 linkHandler.handleLink(anchor, path, hash);
781 }
782 // Get the appropriate file download path.
783 return resolver.getDownloadUrl(urlPath);
784 })
785 .then(url => {
786 // Set the visible anchor.
787 anchor.href = url + hash;
788 })
789 .catch(err => {
790 // If there was an error getting the url,
791 // just make it an empty link.
792 anchor.href = '';
793 });
794 }
795 /**
796 * Handle an anchor node.
797 */
798 async function handlePathAnchor(anchor, resolver, linkHandler) {
799 let path = anchor.dataset.path || '';
800 let locator = anchor.dataset.locator ? '#' + anchor.dataset.locator : '';
801 delete anchor.dataset.path;
802 delete anchor.dataset.locator;
803 const allowRoot = true;
804 const isLocal = resolver.isLocal
805 ? resolver.isLocal(path, allowRoot)
806 : URLExt.isLocal(path, allowRoot);
807 // Bail if:
808 // - it is not a file-like url,
809 // - the resolver does not support paths
810 // - there is no link handler, or if it does not support paths
811 if (!path ||
812 !isLocal ||
813 !resolver.resolvePath ||
814 !linkHandler ||
815 !linkHandler.handlePath) {
816 anchor.replaceWith(...anchor.childNodes);
817 return Promise.resolve(undefined);
818 }
819 try {
820 // Find given path
821 const resolution = await resolver.resolvePath(path);
822 if (!resolution) {
823 // Bail if the file does not exist
824 console.log('Path resolution bailing: does not exist');
825 return Promise.resolve(undefined);
826 }
827 // Handle the click override.
828 linkHandler.handlePath(anchor, resolution.path, resolution.scope, locator);
829 // Set the visible anchor.
830 anchor.href = resolution.path + locator;
831 }
832 catch (err) {
833 // If there was an error getting the url,
834 // just make it an empty link.
835 console.warn('Path anchor error:', err);
836 anchor.href = '#linking-failed-see-console';
837 }
838 }
839 const ANSI_COLORS = [
840 'ansi-black',
841 'ansi-red',
842 'ansi-green',
843 'ansi-yellow',
844 'ansi-blue',
845 'ansi-magenta',
846 'ansi-cyan',
847 'ansi-white',
848 'ansi-black-intense',
849 'ansi-red-intense',
850 'ansi-green-intense',
851 'ansi-yellow-intense',
852 'ansi-blue-intense',
853 'ansi-magenta-intense',
854 'ansi-cyan-intense',
855 'ansi-white-intense'
856 ];
857 /**
858 * Create HTML tags for a string with given foreground, background etc. and
859 * add them to the `out` array.
860 */
861 function pushColoredChunk(chunk, fg, bg, bold, underline, inverse, out) {
862 if (chunk) {
863 const classes = [];
864 const styles = [];
865 if (bold && typeof fg === 'number' && 0 <= fg && fg < 8) {
866 fg += 8; // Bold text uses "intense" colors
867 }
868 if (inverse) {
869 [fg, bg] = [bg, fg];
870 }
871 if (typeof fg === 'number') {
872 classes.push(ANSI_COLORS[fg] + '-fg');
873 }
874 else if (fg.length) {
875 styles.push(`color: rgb(${fg})`);
876 }
877 else if (inverse) {
878 classes.push('ansi-default-inverse-fg');
879 }
880 if (typeof bg === 'number') {
881 classes.push(ANSI_COLORS[bg] + '-bg');
882 }
883 else if (bg.length) {
884 styles.push(`background-color: rgb(${bg})`);
885 }
886 else if (inverse) {
887 classes.push('ansi-default-inverse-bg');
888 }
889 if (bold) {
890 classes.push('ansi-bold');
891 }
892 if (underline) {
893 classes.push('ansi-underline');
894 }
895 if (classes.length || styles.length) {
896 out.push('<span');
897 if (classes.length) {
898 out.push(` class="${classes.join(' ')}"`);
899 }
900 if (styles.length) {
901 out.push(` style="${styles.join('; ')}"`);
902 }
903 out.push('>');
904 out.push(chunk);
905 out.push('</span>');
906 }
907 else {
908 out.push(chunk);
909 }
910 }
911 }
912 /**
913 * Convert ANSI extended colors to R/G/B triple.
914 */
915 function getExtendedColors(numbers) {
916 let r;
917 let g;
918 let b;
919 const n = numbers.shift();
920 if (n === 2 && numbers.length >= 3) {
921 // 24-bit RGB
922 r = numbers.shift();
923 g = numbers.shift();
924 b = numbers.shift();
925 if ([r, g, b].some(c => c < 0 || 255 < c)) {
926 throw new RangeError('Invalid range for RGB colors');
927 }
928 }
929 else if (n === 5 && numbers.length >= 1) {
930 // 256 colors
931 const idx = numbers.shift();
932 if (idx < 0) {
933 throw new RangeError('Color index must be >= 0');
934 }
935 else if (idx < 16) {
936 // 16 default terminal colors
937 return idx;
938 }
939 else if (idx < 232) {
940 // 6x6x6 color cube, see https://stackoverflow.com/a/27165165/500098
941 r = Math.floor((idx - 16) / 36);
942 r = r > 0 ? 55 + r * 40 : 0;
943 g = Math.floor(((idx - 16) % 36) / 6);
944 g = g > 0 ? 55 + g * 40 : 0;
945 b = (idx - 16) % 6;
946 b = b > 0 ? 55 + b * 40 : 0;
947 }
948 else if (idx < 256) {
949 // grayscale, see https://stackoverflow.com/a/27165165/500098
950 r = g = b = (idx - 232) * 10 + 8;
951 }
952 else {
953 throw new RangeError('Color index must be < 256');
954 }
955 }
956 else {
957 throw new RangeError('Invalid extended color specification');
958 }
959 return [r, g, b];
960 }
961 /**
962 * Transform ANSI color escape codes into HTML <span> tags with CSS
963 * classes such as "ansi-green-intense-fg".
964 * The actual colors used are set in the CSS file.
965 * This also removes non-color escape sequences.
966 * This is supposed to have the same behavior as nbconvert.filters.ansi2html()
967 */
968 function ansiSpan(str) {
969 const ansiRe = /\x1b\[(.*?)([@-~])/g; // eslint-disable-line no-control-regex
970 let fg = [];
971 let bg = [];
972 let bold = false;
973 let underline = false;
974 let inverse = false;
975 let match;
976 const out = [];
977 const numbers = [];
978 let start = 0;
979 str = escape(str);
980 str += '\x1b[m'; // Ensure markup for trailing text
981 // tslint:disable-next-line
982 while ((match = ansiRe.exec(str))) {
983 if (match[2] === 'm') {
984 const items = match[1].split(';');
985 for (let i = 0; i < items.length; i++) {
986 const item = items[i];
987 if (item === '') {
988 numbers.push(0);
989 }
990 else if (item.search(/^\d+$/) !== -1) {
991 numbers.push(parseInt(item, 10));
992 }
993 else {
994 // Ignored: Invalid color specification
995 numbers.length = 0;
996 break;
997 }
998 }
999 }
1000 else {
1001 // Ignored: Not a color code
1002 }
1003 const chunk = str.substring(start, match.index);
1004 pushColoredChunk(chunk, fg, bg, bold, underline, inverse, out);
1005 start = ansiRe.lastIndex;
1006 while (numbers.length) {
1007 const n = numbers.shift();
1008 switch (n) {
1009 case 0:
1010 fg = bg = [];
1011 bold = false;
1012 underline = false;
1013 inverse = false;
1014 break;
1015 case 1:
1016 case 5:
1017 bold = true;
1018 break;
1019 case 4:
1020 underline = true;
1021 break;
1022 case 7:
1023 inverse = true;
1024 break;
1025 case 21:
1026 case 22:
1027 bold = false;
1028 break;
1029 case 24:
1030 underline = false;
1031 break;
1032 case 27:
1033 inverse = false;
1034 break;
1035 case 30:
1036 case 31:
1037 case 32:
1038 case 33:
1039 case 34:
1040 case 35:
1041 case 36:
1042 case 37:
1043 fg = n - 30;
1044 break;
1045 case 38:
1046 try {
1047 fg = getExtendedColors(numbers);
1048 }
1049 catch (e) {
1050 numbers.length = 0;
1051 }
1052 break;
1053 case 39:
1054 fg = [];
1055 break;
1056 case 40:
1057 case 41:
1058 case 42:
1059 case 43:
1060 case 44:
1061 case 45:
1062 case 46:
1063 case 47:
1064 bg = n - 40;
1065 break;
1066 case 48:
1067 try {
1068 bg = getExtendedColors(numbers);
1069 }
1070 catch (e) {
1071 numbers.length = 0;
1072 }
1073 break;
1074 case 49:
1075 bg = [];
1076 break;
1077 case 90:
1078 case 91:
1079 case 92:
1080 case 93:
1081 case 94:
1082 case 95:
1083 case 96:
1084 case 97:
1085 fg = n - 90 + 8;
1086 break;
1087 case 100:
1088 case 101:
1089 case 102:
1090 case 103:
1091 case 104:
1092 case 105:
1093 case 106:
1094 case 107:
1095 bg = n - 100 + 8;
1096 break;
1097 default:
1098 // Unknown codes are ignored
1099 }
1100 }
1101 }
1102 return out.join('');
1103 }
1104 Private.ansiSpan = ansiSpan;
1105})(Private || (Private = {}));
1106//# sourceMappingURL=renderers.js.map
\No newline at end of file