123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803 |
- /*-----------------------------------------------------------------------------
- | Copyright (c) Jupyter Development Team.
- | Distributed under the terms of the Modified BSD License.
- |----------------------------------------------------------------------------*/
- import {
- default as AnsiUp
- } from 'ansi_up';
- import * as marked
- from 'marked';
- import {
- ISanitizer
- } from '@jupyterlab/apputils';
- import {
- Mode, CodeMirrorEditor
- } from '@jupyterlab/codemirror';
- import {
- URLExt
- } from '@jupyterlab/coreutils';
- import {
- IRenderMime
- } from '@jupyterlab/rendermime-interfaces';
- import {
- removeMath, replaceMath
- } from './latex';
- /**
- * Render HTML into a host node.
- *
- * @params options - The options for rendering.
- *
- * @returns A promise which resolves when rendering is complete.
- */
- export
- function renderHTML(options: renderHTML.IOptions): Promise<void> {
- // Unpack the options.
- let {
- host, source, trusted, sanitizer, resolver, linkHandler,
- shouldTypeset, latexTypesetter
- } = options;
- // Bail early if the source is empty.
- if (!source) {
- host.textContent = '';
- return Promise.resolve(undefined);
- }
- // Sanitize the source if it is not trusted. This removes all
- // `<script>` tags as well as other potentially harmful HTML.
- if (!trusted) {
- source = sanitizer.sanitize(source);
- }
- // Set the inner HTML of the host.
- host.innerHTML = source;
- if (host.getElementsByTagName('script').length > 0) {
- console.warn('JupyterLab does not execute inline JavaScript in HTML output');
- }
- // TODO - arbitrary script execution is disabled for now.
- // Eval any script tags contained in the HTML. This is not done
- // automatically by the browser when script tags are created by
- // setting `innerHTML`. The santizer should have removed all of
- // the script tags for untrusted source, but this extra trusted
- // check is just extra insurance.
- // if (trusted) {
- // // TODO do we really want to run scripts? Because if so, there
- // // is really no difference between this and a JS mime renderer.
- // Private.evalInnerHTMLScriptTags(host);
- // }
- // Handle default behavior of nodes.
- Private.handleDefaults(host);
- // Patch the urls if a resolver is available.
- let promise: Promise<void>;
- if (resolver) {
- promise = Private.handleUrls(host, resolver, linkHandler);
- } else {
- promise = Promise.resolve(undefined);
- }
- // Return the final rendered promise.
- return promise.then(() => {
- if (shouldTypeset && latexTypesetter ) { latexTypesetter.typeset(host); }
- });
- }
- /**
- * The namespace for the `renderHTML` function statics.
- */
- export
- namespace renderHTML {
- /**
- * The options for the `renderHTML` function.
- */
- export
- interface IOptions {
- /**
- * The host node for the rendered HTML.
- */
- host: HTMLElement;
- /**
- * The HTML source to render.
- */
- source: string;
- /**
- * Whether the source is trusted.
- */
- trusted: boolean;
- /**
- * The html sanitizer for untrusted source.
- */
- sanitizer: ISanitizer;
- /**
- * An optional url resolver.
- */
- resolver: IRenderMime.IResolver | null;
- /**
- * An optional link handler.
- */
- linkHandler: IRenderMime.ILinkHandler | null;
- /**
- * Whether the node should be typeset.
- */
- shouldTypeset: boolean;
- /**
- * The LaTeX typesetter for the application.
- */
- latexTypesetter: IRenderMime.ILatexTypesetter | null;
- }
- }
- /**
- * Render an image into a host node.
- *
- * @params options - The options for rendering.
- *
- * @returns A promise which resolves when rendering is complete.
- */
- export
- function renderImage(options: renderImage.IRenderOptions): Promise<void> {
- // Unpack the options.
- let { host, mimeType, source, width, height, unconfined } = options;
- // Clear the content in the host.
- host.textContent = '';
- // Create the image element.
- let img = document.createElement('img');
- // Set the source of the image.
- img.src = `data:${mimeType};base64,${source}`;
- // Set the size of the image if provided.
- if (typeof height === 'number') {
- img.height = height;
- }
- if (typeof width === 'number') {
- img.width = width;
- }
- if (unconfined === true) {
- img.classList.add('jp-mod-unconfined');
- }
- // Add the image to the host.
- host.appendChild(img);
- // Return the rendered promise.
- return Promise.resolve(undefined);
- }
- /**
- * The namespace for the `renderImage` function statics.
- */
- export
- namespace renderImage {
- /**
- * The options for the `renderImage` function.
- */
- export
- interface IRenderOptions {
- /**
- * The image node to update with the content.
- */
- host: HTMLElement;
- /**
- * The mime type for the image.
- */
- mimeType: string;
- /**
- * The base64 encoded source for the image.
- */
- source: string;
- /**
- * The optional width for the image.
- */
- width?: number;
- /**
- * The optional height for the image.
- */
- height?: number;
- /**
- * Whether the image should be unconfined.
- */
- unconfined?: boolean;
- }
- }
- /**
- * Render LaTeX into a host node.
- *
- * @params options - The options for rendering.
- *
- * @returns A promise which resolves when rendering is complete.
- */
- export
- function renderLatex(options: renderLatex.IRenderOptions): Promise<void> {
- // Unpack the options.
- let { host, source, shouldTypeset, latexTypesetter } = options;
- // Set the source on the node.
- host.textContent = source;
- // Typeset the node if needed.
- if (shouldTypeset && latexTypesetter) {
- latexTypesetter.typeset(host);
- }
- // Return the rendered promise.
- return Promise.resolve(undefined);
- }
- /**
- * The namespace for the `renderLatex` function statics.
- */
- export
- namespace renderLatex {
- /**
- * The options for the `renderLatex` function.
- */
- export
- interface IRenderOptions {
- /**
- * The host node for the rendered LaTeX.
- */
- host: HTMLElement;
- /**
- * The LaTeX source to render.
- */
- source: string;
- /**
- * Whether the node should be typeset.
- */
- shouldTypeset: boolean;
- /**
- * The LaTeX typesetter for the application.
- */
- latexTypesetter: IRenderMime.ILatexTypesetter | null;
- }
- }
- /**
- * Render Markdown into a host node.
- *
- * @params options - The options for rendering.
- *
- * @returns A promise which resolves when rendering is complete.
- */
- export
- function renderMarkdown(options: renderMarkdown.IRenderOptions): Promise<void> {
- // Unpack the options.
- let {
- host, source, trusted, sanitizer, resolver, linkHandler,
- latexTypesetter, shouldTypeset
- } = options;
- // Clear the content if there is no source.
- if (!source) {
- host.textContent = '';
- return Promise.resolve(undefined);
- }
- // Separate math from normal markdown text.
- let parts = removeMath(source);
- // Render the markdown and handle sanitization.
- return Private.renderMarked(parts['text']).then(content => {
- // Restore the math content in the rendered markdown.
- content = replaceMath(content, parts['math']);
- // Santize the content it is not trusted.
- if (!trusted) {
- content = sanitizer.sanitize(content);
- }
- // Set the inner HTML of the host.
- host.innerHTML = content;
- if (host.getElementsByTagName('script').length > 0) {
- console.warn('JupyterLab does not execute inline JavaScript in HTML output');
- }
- // TODO arbitrary script execution is disabled for now.
- // Eval any script tags contained in the HTML. This is not done
- // automatically by the browser when script tags are created by
- // setting `innerHTML`. The santizer should have removed all of
- // the script tags for untrusted source, but this extra trusted
- // check is just extra insurance.
- // if (trusted) {
- // // TODO really want to run scripts?
- // Private.evalInnerHTMLScriptTags(host);
- // }
- // Handle default behavior of nodes.
- Private.handleDefaults(host);
- // Apply ids to the header nodes.
- Private.headerAnchors(host);
- // Patch the urls if a resolver is available.
- let promise: Promise<void>;
- if (resolver) {
- promise = Private.handleUrls(host, resolver, linkHandler);
- } else {
- promise = Promise.resolve(undefined);
- }
- // Return the rendered promise.
- return promise;
- }).then(() => {
- if (shouldTypeset && latexTypesetter) {
- latexTypesetter.typeset(host);
- }
- });
- }
- /**
- * The namespace for the `renderMarkdown` function statics.
- */
- export
- namespace renderMarkdown {
- /**
- * The options for the `renderMarkdown` function.
- */
- export
- interface IRenderOptions {
- /**
- * The host node for the rendered Markdown.
- */
- host: HTMLElement;
- /**
- * The Markdown source to render.
- */
- source: string;
- /**
- * Whether the source is trusted.
- */
- trusted: boolean;
- /**
- * The html sanitizer for untrusted source.
- */
- sanitizer: ISanitizer;
- /**
- * An optional url resolver.
- */
- resolver: IRenderMime.IResolver | null;
- /**
- * An optional link handler.
- */
- linkHandler: IRenderMime.ILinkHandler | null;
- /**
- * Whether the node should be typeset.
- */
- shouldTypeset: boolean;
- /**
- * The LaTeX typesetter for the application.
- */
- latexTypesetter: IRenderMime.ILatexTypesetter | null;
- }
- }
- /**
- * Render SVG into a host node.
- *
- * @params options - The options for rendering.
- *
- * @returns A promise which resolves when rendering is complete.
- */
- export
- function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
- // Unpack the options.
- let {
- host, source, trusted, unconfined
- } = options;
- // Clear the content if there is no source.
- if (!source) {
- host.textContent = '';
- return Promise.resolve(undefined);
- }
- // Display a message if the source is not trusted.
- if (!trusted) {
- host.textContent = 'Cannot display an untrusted SVG. Maybe you need to run the cell?';
- return Promise.resolve(undefined);
- }
- // Render in img so that user can save it easily
- const img = new Image();
- img.src = `data:image/svg+xml,${source}`;
- host.appendChild(img);
- if (unconfined === true) {
- host.classList.add('jp-mod-unconfined');
- }
- return Promise.resolve();
- }
- /**
- * The namespace for the `renderSVG` function statics.
- */
- export
- namespace renderSVG {
- /**
- * The options for the `renderSVG` function.
- */
- export
- interface IRenderOptions {
- /**
- * The host node for the rendered SVG.
- */
- host: HTMLElement;
- /**
- * The SVG source.
- */
- source: string;
- /**
- * Whether the source is trusted.
- */
- trusted: boolean;
- /**
- * Whether the svg should be unconfined.
- */
- unconfined?: boolean;
- }
- }
- /**
- * Render text into a host node.
- *
- * @params options - The options for rendering.
- *
- * @returns A promise which resolves when rendering is complete.
- */
- export
- function renderText(options: renderText.IRenderOptions): Promise<void> {
- // Unpack the options.
- let { host, source } = options;
- const ansiUp = new AnsiUp();
- ansiUp.escape_for_html = true;
- ansiUp.use_classes = true;
- // Create the HTML content.
- let content = ansiUp.ansi_to_html(source);
- // Set the inner HTML for the host node.
- host.innerHTML = `<pre>${content}</pre>`;
- // Return the rendered promise.
- return Promise.resolve(undefined);
- }
- /**
- * The namespace for the `renderText` function statics.
- */
- export
- namespace renderText {
- /**
- * The options for the `renderText` function.
- */
- export
- interface IRenderOptions {
- /**
- * The host node for the text content.
- */
- host: HTMLElement;
- /**
- * The source text to render.
- */
- source: string;
- }
- }
- /**
- * The namespace for module implementation details.
- */
- namespace Private {
- // This is disabled for now until we decide we actually really
- // truly want to allow arbitrary script execution.
- /**
- * Eval the script tags contained in a host populated by `innerHTML`.
- *
- * When script tags are created via `innerHTML`, the browser does not
- * evaluate them when they are added to the page. This function works
- * around that by creating new equivalent script nodes manually, and
- * replacing the originals.
- */
- // export
- // function evalInnerHTMLScriptTags(host: HTMLElement): void {
- // // Create a snapshot of the current script nodes.
- // let scripts = toArray(host.getElementsByTagName('script'));
- // // Loop over each script node.
- // for (let script of scripts) {
- // // Skip any scripts which no longer have a parent.
- // if (!script.parentNode) {
- // continue;
- // }
- // // Create a new script node which will be clone.
- // let clone = document.createElement('script');
- // // Copy the attributes into the clone.
- // let attrs = script.attributes;
- // for (let i = 0, n = attrs.length; i < n; ++i) {
- // let { name, value } = attrs[i];
- // clone.setAttribute(name, value);
- // }
- // // Copy the text content into the clone.
- // clone.textContent = script.textContent;
- // // Replace the old script in the parent.
- // script.parentNode.replaceChild(clone, script);
- // }
- // }
- /**
- * Render markdown for the specified content.
- *
- * @param content - The string of markdown to render.
- *
- * @return A promise which resolves with the rendered content.
- */
- export
- function renderMarked(content: string): Promise<string> {
- initializeMarked();
- return new Promise<string>((resolve, reject) => {
- marked(content, (err: any, content: string) => {
- if (err) {
- reject(err);
- } else {
- resolve(content);
- }
- });
- });
- }
- /**
- * Handle the default behavior of nodes.
- */
- export
- function handleDefaults(node: HTMLElement): void {
- // Handle anchor elements.
- let anchors = node.getElementsByTagName('a');
- for (let i = 0; i < anchors.length; i++) {
- let path = anchors[i].href;
- if (URLExt.isLocal(path)) {
- anchors[i].target = '_self';
- } else {
- anchors[i].target = '_blank';
- }
- }
- // Handle image elements.
- let imgs = node.getElementsByTagName('img');
- for (let i = 0; i < imgs.length; i++) {
- if (!imgs[i].alt) {
- imgs[i].alt = 'Image';
- }
- }
- }
- /**
- * Resolve the relative urls in element `src` and `href` attributes.
- *
- * @param node - The head html element.
- *
- * @param resolver - A url resolver.
- *
- * @param linkHandler - An optional link handler for nodes.
- *
- * @returns a promise fulfilled when the relative urls have been resolved.
- */
- export
- function handleUrls(node: HTMLElement, resolver: IRenderMime.IResolver, linkHandler: IRenderMime.ILinkHandler | null): Promise<void> {
- // Set up an array to collect promises.
- let promises: Promise<void>[] = [];
- // Handle HTML Elements with src attributes.
- let nodes = node.querySelectorAll('*[src]');
- for (let i = 0; i < nodes.length; i++) {
- promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
- }
- // Handle anchor elements.
- let anchors = node.getElementsByTagName('a');
- for (let i = 0; i < anchors.length; i++) {
- promises.push(handleAnchor(anchors[i], resolver, linkHandler));
- }
- // Handle link elements.
- let links = node.getElementsByTagName('link');
- for (let i = 0; i < links.length; i++) {
- promises.push(handleAttr(links[i], 'href', resolver));
- }
- // Wait on all promises.
- return Promise.all(promises).then(() => undefined);
- }
- /**
- * Apply ids to headers.
- */
- export
- function headerAnchors(node: HTMLElement): void {
- let headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
- for (let headerType of headerNames) {
- let headers = node.getElementsByTagName(headerType);
- for (let i=0; i < headers.length; i++) {
- let header = headers[i];
- header.id = header.innerHTML.replace(/ /g, '-');
- let anchor = document.createElement('a');
- anchor.target = '_self';
- anchor.textContent = '¶';
- anchor.href = '#' + header.id;
- anchor.classList.add('jp-InternalAnchorLink');
- header.appendChild(anchor);
- }
- }
- }
- /**
- * Handle a node with a `src` or `href` attribute.
- */
- function handleAttr(node: HTMLElement, name: 'src' | 'href', resolver: IRenderMime.IResolver): Promise<void> {
- let source = node.getAttribute(name);
- if (!source || URLExt.parse(source).protocol === 'data:') {
- return Promise.resolve(undefined);
- }
- node.setAttribute(name, '');
- return resolver.resolveUrl(source).then(path => {
- return resolver.getDownloadUrl(path);
- }).then(url => {
- // Bust caching for local src attrs.
- // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
- url += ((/\?/).test(url) ? '&' : '?') + (new Date()).getTime();
- node.setAttribute(name, url);
- }).catch(err => {
- // If there was an error getting the url,
- // just make it an empty link.
- node.setAttribute(name, '');
- });
- }
- /**
- * Handle an anchor node.
- */
- function handleAnchor(anchor: HTMLAnchorElement, resolver: IRenderMime.IResolver, linkHandler: IRenderMime.ILinkHandler | null): Promise<void> {
- // Get the link path without the location prepended.
- // (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
- let href = anchor.getAttribute('href');
- // Bail if it is not a file-like url.
- if (!href || href.indexOf('://') !== -1 && href.indexOf('//') === 0) {
- return Promise.resolve(undefined);
- }
- // Remove the hash until we can handle it.
- let hash = anchor.hash;
- if (hash) {
- // Handle internal link in the file.
- if (hash === href) {
- anchor.target = '_self';
- return Promise.resolve(undefined);
- }
- // For external links, remove the hash until we have hash handling.
- href = href.replace(hash, '');
- }
- // Get the appropriate file path.
- return resolver.resolveUrl(href).then(path => {
- // Handle the click override.
- if (linkHandler && URLExt.isLocal(path)) {
- linkHandler.handleLink(anchor, path);
- }
- // Get the appropriate file download path.
- return resolver.getDownloadUrl(path);
- }).then(url => {
- // Set the visible anchor.
- anchor.href = url + hash;
- }).catch(err => {
- // If there was an error getting the url,
- // just make it an empty link.
- anchor.href = '';
- });
- }
- let markedInitialized = false;
- /**
- * Support GitHub flavored Markdown, leave sanitizing to external library.
- */
- function initializeMarked(): void {
- if (markedInitialized) {
- return;
- }
- markedInitialized = true;
- marked.setOptions({
- gfm: true,
- sanitize: false,
- tables: true,
- // breaks: true; We can't use GFM breaks as it causes problems with tables
- langPrefix: `cm-s-${CodeMirrorEditor.defaultConfig.theme} language-`,
- highlight: (code, lang, callback) => {
- let cb = (err: Error | null, code: string) => {
- if (callback) {
- callback(err, code);
- }
- return code;
- };
- if (!lang) {
- // no language, no highlight
- return cb(null, code);
- }
- Mode.ensure(lang).then(spec => {
- let el = document.createElement('div');
- if (!spec) {
- console.log(`No CodeMirror mode: ${lang}`);
- return cb(null, code);
- }
- try {
- Mode.run(code, spec.mime, el);
- return cb(null, el.innerHTML);
- } catch (err) {
- console.log(`Failed to highlight ${lang} code`, err);
- return cb(err, code);
- }
- }).catch(err => {
- console.log(`No CodeMirror mode: ${lang}`);
- console.log(`Require CodeMirror mode error: ${err}`);
- return cb(null, code);
- });
- return code;
- }
- });
- }
- }
|