renderers.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. /*-----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import {
  6. default as AnsiUp
  7. } from 'ansi_up';
  8. import * as marked
  9. from 'marked';
  10. import {
  11. ISanitizer
  12. } from '@jupyterlab/apputils';
  13. import {
  14. Mode, CodeMirrorEditor
  15. } from '@jupyterlab/codemirror';
  16. import {
  17. URLExt
  18. } from '@jupyterlab/coreutils';
  19. import {
  20. IRenderMime
  21. } from '@jupyterlab/rendermime-interfaces';
  22. import {
  23. removeMath, replaceMath
  24. } from './latex';
  25. /**
  26. * Render HTML into a host node.
  27. *
  28. * @params options - The options for rendering.
  29. *
  30. * @returns A promise which resolves when rendering is complete.
  31. */
  32. export
  33. function renderHTML(options: renderHTML.IOptions): Promise<void> {
  34. // Unpack the options.
  35. let {
  36. host, source, trusted, sanitizer, resolver, linkHandler,
  37. shouldTypeset, latexTypesetter
  38. } = options;
  39. // Bail early if the source is empty.
  40. if (!source) {
  41. host.textContent = '';
  42. return Promise.resolve(undefined);
  43. }
  44. // Sanitize the source if it is not trusted. This removes all
  45. // `<script>` tags as well as other potentially harmful HTML.
  46. if (!trusted) {
  47. source = sanitizer.sanitize(source);
  48. }
  49. // Set the inner HTML of the host.
  50. host.innerHTML = source;
  51. if (host.getElementsByTagName('script').length > 0) {
  52. console.warn('JupyterLab does not execute inline JavaScript in HTML output');
  53. }
  54. // TODO - arbitrary script execution is disabled for now.
  55. // Eval any script tags contained in the HTML. This is not done
  56. // automatically by the browser when script tags are created by
  57. // setting `innerHTML`. The santizer should have removed all of
  58. // the script tags for untrusted source, but this extra trusted
  59. // check is just extra insurance.
  60. // if (trusted) {
  61. // // TODO do we really want to run scripts? Because if so, there
  62. // // is really no difference between this and a JS mime renderer.
  63. // Private.evalInnerHTMLScriptTags(host);
  64. // }
  65. // Handle default behavior of nodes.
  66. Private.handleDefaults(host);
  67. // Patch the urls if a resolver is available.
  68. let promise: Promise<void>;
  69. if (resolver) {
  70. promise = Private.handleUrls(host, resolver, linkHandler);
  71. } else {
  72. promise = Promise.resolve(undefined);
  73. }
  74. // Return the final rendered promise.
  75. return promise.then(() => {
  76. if (shouldTypeset && latexTypesetter ) { latexTypesetter.typeset(host); }
  77. });
  78. }
  79. /**
  80. * The namespace for the `renderHTML` function statics.
  81. */
  82. export
  83. namespace renderHTML {
  84. /**
  85. * The options for the `renderHTML` function.
  86. */
  87. export
  88. interface IOptions {
  89. /**
  90. * The host node for the rendered HTML.
  91. */
  92. host: HTMLElement;
  93. /**
  94. * The HTML source to render.
  95. */
  96. source: string;
  97. /**
  98. * Whether the source is trusted.
  99. */
  100. trusted: boolean;
  101. /**
  102. * The html sanitizer for untrusted source.
  103. */
  104. sanitizer: ISanitizer;
  105. /**
  106. * An optional url resolver.
  107. */
  108. resolver: IRenderMime.IResolver | null;
  109. /**
  110. * An optional link handler.
  111. */
  112. linkHandler: IRenderMime.ILinkHandler | null;
  113. /**
  114. * Whether the node should be typeset.
  115. */
  116. shouldTypeset: boolean;
  117. /**
  118. * The LaTeX typesetter for the application.
  119. */
  120. latexTypesetter: IRenderMime.ILatexTypesetter | null;
  121. }
  122. }
  123. /**
  124. * Render an image into a host node.
  125. *
  126. * @params options - The options for rendering.
  127. *
  128. * @returns A promise which resolves when rendering is complete.
  129. */
  130. export
  131. function renderImage(options: renderImage.IRenderOptions): Promise<void> {
  132. // Unpack the options.
  133. let { host, mimeType, source, width, height, unconfined } = options;
  134. // Clear the content in the host.
  135. host.textContent = '';
  136. // Create the image element.
  137. let img = document.createElement('img');
  138. // Set the source of the image.
  139. img.src = `data:${mimeType};base64,${source}`;
  140. // Set the size of the image if provided.
  141. if (typeof height === 'number') {
  142. img.height = height;
  143. }
  144. if (typeof width === 'number') {
  145. img.width = width;
  146. }
  147. if (unconfined === true) {
  148. img.classList.add('jp-mod-unconfined');
  149. }
  150. // Add the image to the host.
  151. host.appendChild(img);
  152. // Return the rendered promise.
  153. return Promise.resolve(undefined);
  154. }
  155. /**
  156. * The namespace for the `renderImage` function statics.
  157. */
  158. export
  159. namespace renderImage {
  160. /**
  161. * The options for the `renderImage` function.
  162. */
  163. export
  164. interface IRenderOptions {
  165. /**
  166. * The image node to update with the content.
  167. */
  168. host: HTMLElement;
  169. /**
  170. * The mime type for the image.
  171. */
  172. mimeType: string;
  173. /**
  174. * The base64 encoded source for the image.
  175. */
  176. source: string;
  177. /**
  178. * The optional width for the image.
  179. */
  180. width?: number;
  181. /**
  182. * The optional height for the image.
  183. */
  184. height?: number;
  185. /**
  186. * Whether the image should be unconfined.
  187. */
  188. unconfined?: boolean;
  189. }
  190. }
  191. /**
  192. * Render LaTeX into a host node.
  193. *
  194. * @params options - The options for rendering.
  195. *
  196. * @returns A promise which resolves when rendering is complete.
  197. */
  198. export
  199. function renderLatex(options: renderLatex.IRenderOptions): Promise<void> {
  200. // Unpack the options.
  201. let { host, source, shouldTypeset, latexTypesetter } = options;
  202. // Set the source on the node.
  203. host.textContent = source;
  204. // Typeset the node if needed.
  205. if (shouldTypeset && latexTypesetter) {
  206. latexTypesetter.typeset(host);
  207. }
  208. // Return the rendered promise.
  209. return Promise.resolve(undefined);
  210. }
  211. /**
  212. * The namespace for the `renderLatex` function statics.
  213. */
  214. export
  215. namespace renderLatex {
  216. /**
  217. * The options for the `renderLatex` function.
  218. */
  219. export
  220. interface IRenderOptions {
  221. /**
  222. * The host node for the rendered LaTeX.
  223. */
  224. host: HTMLElement;
  225. /**
  226. * The LaTeX source to render.
  227. */
  228. source: string;
  229. /**
  230. * Whether the node should be typeset.
  231. */
  232. shouldTypeset: boolean;
  233. /**
  234. * The LaTeX typesetter for the application.
  235. */
  236. latexTypesetter: IRenderMime.ILatexTypesetter | null;
  237. }
  238. }
  239. /**
  240. * Render Markdown into a host node.
  241. *
  242. * @params options - The options for rendering.
  243. *
  244. * @returns A promise which resolves when rendering is complete.
  245. */
  246. export
  247. function renderMarkdown(options: renderMarkdown.IRenderOptions): Promise<void> {
  248. // Unpack the options.
  249. let {
  250. host, source, trusted, sanitizer, resolver, linkHandler,
  251. latexTypesetter, shouldTypeset
  252. } = options;
  253. // Clear the content if there is no source.
  254. if (!source) {
  255. host.textContent = '';
  256. return Promise.resolve(undefined);
  257. }
  258. // Separate math from normal markdown text.
  259. let parts = removeMath(source);
  260. // Render the markdown and handle sanitization.
  261. return Private.renderMarked(parts['text']).then(content => {
  262. // Restore the math content in the rendered markdown.
  263. content = replaceMath(content, parts['math']);
  264. // Santize the content it is not trusted.
  265. if (!trusted) {
  266. content = sanitizer.sanitize(content);
  267. }
  268. // Set the inner HTML of the host.
  269. host.innerHTML = content;
  270. if (host.getElementsByTagName('script').length > 0) {
  271. console.warn('JupyterLab does not execute inline JavaScript in HTML output');
  272. }
  273. // TODO arbitrary script execution is disabled for now.
  274. // Eval any script tags contained in the HTML. This is not done
  275. // automatically by the browser when script tags are created by
  276. // setting `innerHTML`. The santizer should have removed all of
  277. // the script tags for untrusted source, but this extra trusted
  278. // check is just extra insurance.
  279. // if (trusted) {
  280. // // TODO really want to run scripts?
  281. // Private.evalInnerHTMLScriptTags(host);
  282. // }
  283. // Handle default behavior of nodes.
  284. Private.handleDefaults(host);
  285. // Apply ids to the header nodes.
  286. Private.headerAnchors(host);
  287. // Patch the urls if a resolver is available.
  288. let promise: Promise<void>;
  289. if (resolver) {
  290. promise = Private.handleUrls(host, resolver, linkHandler);
  291. } else {
  292. promise = Promise.resolve(undefined);
  293. }
  294. // Return the rendered promise.
  295. return promise;
  296. }).then(() => {
  297. if (shouldTypeset && latexTypesetter) {
  298. latexTypesetter.typeset(host);
  299. }
  300. });
  301. }
  302. /**
  303. * The namespace for the `renderMarkdown` function statics.
  304. */
  305. export
  306. namespace renderMarkdown {
  307. /**
  308. * The options for the `renderMarkdown` function.
  309. */
  310. export
  311. interface IRenderOptions {
  312. /**
  313. * The host node for the rendered Markdown.
  314. */
  315. host: HTMLElement;
  316. /**
  317. * The Markdown source to render.
  318. */
  319. source: string;
  320. /**
  321. * Whether the source is trusted.
  322. */
  323. trusted: boolean;
  324. /**
  325. * The html sanitizer for untrusted source.
  326. */
  327. sanitizer: ISanitizer;
  328. /**
  329. * An optional url resolver.
  330. */
  331. resolver: IRenderMime.IResolver | null;
  332. /**
  333. * An optional link handler.
  334. */
  335. linkHandler: IRenderMime.ILinkHandler | null;
  336. /**
  337. * Whether the node should be typeset.
  338. */
  339. shouldTypeset: boolean;
  340. /**
  341. * The LaTeX typesetter for the application.
  342. */
  343. latexTypesetter: IRenderMime.ILatexTypesetter | null;
  344. }
  345. }
  346. /**
  347. * Render SVG into a host node.
  348. *
  349. * @params options - The options for rendering.
  350. *
  351. * @returns A promise which resolves when rendering is complete.
  352. */
  353. export
  354. function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
  355. // Unpack the options.
  356. let {
  357. host, source, trusted, unconfined
  358. } = options;
  359. // Clear the content if there is no source.
  360. if (!source) {
  361. host.textContent = '';
  362. return Promise.resolve(undefined);
  363. }
  364. // Display a message if the source is not trusted.
  365. if (!trusted) {
  366. host.textContent = 'Cannot display an untrusted SVG. Maybe you need to run the cell?';
  367. return Promise.resolve(undefined);
  368. }
  369. // Render in img so that user can save it easily
  370. const img = new Image();
  371. img.src = `data:image/svg+xml,${source}`;
  372. host.appendChild(img);
  373. if (unconfined === true) {
  374. host.classList.add('jp-mod-unconfined');
  375. }
  376. return Promise.resolve();
  377. }
  378. /**
  379. * The namespace for the `renderSVG` function statics.
  380. */
  381. export
  382. namespace renderSVG {
  383. /**
  384. * The options for the `renderSVG` function.
  385. */
  386. export
  387. interface IRenderOptions {
  388. /**
  389. * The host node for the rendered SVG.
  390. */
  391. host: HTMLElement;
  392. /**
  393. * The SVG source.
  394. */
  395. source: string;
  396. /**
  397. * Whether the source is trusted.
  398. */
  399. trusted: boolean;
  400. /**
  401. * Whether the svg should be unconfined.
  402. */
  403. unconfined?: boolean;
  404. }
  405. }
  406. /**
  407. * Render text into a host node.
  408. *
  409. * @params options - The options for rendering.
  410. *
  411. * @returns A promise which resolves when rendering is complete.
  412. */
  413. export
  414. function renderText(options: renderText.IRenderOptions): Promise<void> {
  415. // Unpack the options.
  416. let { host, source } = options;
  417. const ansiUp = new AnsiUp();
  418. ansiUp.escape_for_html = true;
  419. ansiUp.use_classes = true;
  420. // Create the HTML content.
  421. let content = ansiUp.ansi_to_html(source);
  422. // Set the inner HTML for the host node.
  423. host.innerHTML = `<pre>${content}</pre>`;
  424. // Return the rendered promise.
  425. return Promise.resolve(undefined);
  426. }
  427. /**
  428. * The namespace for the `renderText` function statics.
  429. */
  430. export
  431. namespace renderText {
  432. /**
  433. * The options for the `renderText` function.
  434. */
  435. export
  436. interface IRenderOptions {
  437. /**
  438. * The host node for the text content.
  439. */
  440. host: HTMLElement;
  441. /**
  442. * The source text to render.
  443. */
  444. source: string;
  445. }
  446. }
  447. /**
  448. * The namespace for module implementation details.
  449. */
  450. namespace Private {
  451. // This is disabled for now until we decide we actually really
  452. // truly want to allow arbitrary script execution.
  453. /**
  454. * Eval the script tags contained in a host populated by `innerHTML`.
  455. *
  456. * When script tags are created via `innerHTML`, the browser does not
  457. * evaluate them when they are added to the page. This function works
  458. * around that by creating new equivalent script nodes manually, and
  459. * replacing the originals.
  460. */
  461. // export
  462. // function evalInnerHTMLScriptTags(host: HTMLElement): void {
  463. // // Create a snapshot of the current script nodes.
  464. // let scripts = toArray(host.getElementsByTagName('script'));
  465. // // Loop over each script node.
  466. // for (let script of scripts) {
  467. // // Skip any scripts which no longer have a parent.
  468. // if (!script.parentNode) {
  469. // continue;
  470. // }
  471. // // Create a new script node which will be clone.
  472. // let clone = document.createElement('script');
  473. // // Copy the attributes into the clone.
  474. // let attrs = script.attributes;
  475. // for (let i = 0, n = attrs.length; i < n; ++i) {
  476. // let { name, value } = attrs[i];
  477. // clone.setAttribute(name, value);
  478. // }
  479. // // Copy the text content into the clone.
  480. // clone.textContent = script.textContent;
  481. // // Replace the old script in the parent.
  482. // script.parentNode.replaceChild(clone, script);
  483. // }
  484. // }
  485. /**
  486. * Render markdown for the specified content.
  487. *
  488. * @param content - The string of markdown to render.
  489. *
  490. * @return A promise which resolves with the rendered content.
  491. */
  492. export
  493. function renderMarked(content: string): Promise<string> {
  494. initializeMarked();
  495. return new Promise<string>((resolve, reject) => {
  496. marked(content, (err: any, content: string) => {
  497. if (err) {
  498. reject(err);
  499. } else {
  500. resolve(content);
  501. }
  502. });
  503. });
  504. }
  505. /**
  506. * Handle the default behavior of nodes.
  507. */
  508. export
  509. function handleDefaults(node: HTMLElement): void {
  510. // Handle anchor elements.
  511. let anchors = node.getElementsByTagName('a');
  512. for (let i = 0; i < anchors.length; i++) {
  513. let path = anchors[i].href;
  514. if (URLExt.isLocal(path)) {
  515. anchors[i].target = '_self';
  516. } else {
  517. anchors[i].target = '_blank';
  518. }
  519. }
  520. // Handle image elements.
  521. let imgs = node.getElementsByTagName('img');
  522. for (let i = 0; i < imgs.length; i++) {
  523. if (!imgs[i].alt) {
  524. imgs[i].alt = 'Image';
  525. }
  526. }
  527. }
  528. /**
  529. * Resolve the relative urls in element `src` and `href` attributes.
  530. *
  531. * @param node - The head html element.
  532. *
  533. * @param resolver - A url resolver.
  534. *
  535. * @param linkHandler - An optional link handler for nodes.
  536. *
  537. * @returns a promise fulfilled when the relative urls have been resolved.
  538. */
  539. export
  540. function handleUrls(node: HTMLElement, resolver: IRenderMime.IResolver, linkHandler: IRenderMime.ILinkHandler | null): Promise<void> {
  541. // Set up an array to collect promises.
  542. let promises: Promise<void>[] = [];
  543. // Handle HTML Elements with src attributes.
  544. let nodes = node.querySelectorAll('*[src]');
  545. for (let i = 0; i < nodes.length; i++) {
  546. promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
  547. }
  548. // Handle anchor elements.
  549. let anchors = node.getElementsByTagName('a');
  550. for (let i = 0; i < anchors.length; i++) {
  551. promises.push(handleAnchor(anchors[i], resolver, linkHandler));
  552. }
  553. // Handle link elements.
  554. let links = node.getElementsByTagName('link');
  555. for (let i = 0; i < links.length; i++) {
  556. promises.push(handleAttr(links[i], 'href', resolver));
  557. }
  558. // Wait on all promises.
  559. return Promise.all(promises).then(() => undefined);
  560. }
  561. /**
  562. * Apply ids to headers.
  563. */
  564. export
  565. function headerAnchors(node: HTMLElement): void {
  566. let headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
  567. for (let headerType of headerNames) {
  568. let headers = node.getElementsByTagName(headerType);
  569. for (let i=0; i < headers.length; i++) {
  570. let header = headers[i];
  571. header.id = header.innerHTML.replace(/ /g, '-');
  572. let anchor = document.createElement('a');
  573. anchor.target = '_self';
  574. anchor.textContent = '¶';
  575. anchor.href = '#' + header.id;
  576. anchor.classList.add('jp-InternalAnchorLink');
  577. header.appendChild(anchor);
  578. }
  579. }
  580. }
  581. /**
  582. * Handle a node with a `src` or `href` attribute.
  583. */
  584. function handleAttr(node: HTMLElement, name: 'src' | 'href', resolver: IRenderMime.IResolver): Promise<void> {
  585. let source = node.getAttribute(name);
  586. if (!source || URLExt.parse(source).protocol === 'data:') {
  587. return Promise.resolve(undefined);
  588. }
  589. node.setAttribute(name, '');
  590. return resolver.resolveUrl(source).then(path => {
  591. return resolver.getDownloadUrl(path);
  592. }).then(url => {
  593. // Bust caching for local src attrs.
  594. // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
  595. url += ((/\?/).test(url) ? '&' : '?') + (new Date()).getTime();
  596. node.setAttribute(name, url);
  597. }).catch(err => {
  598. // If there was an error getting the url,
  599. // just make it an empty link.
  600. node.setAttribute(name, '');
  601. });
  602. }
  603. /**
  604. * Handle an anchor node.
  605. */
  606. function handleAnchor(anchor: HTMLAnchorElement, resolver: IRenderMime.IResolver, linkHandler: IRenderMime.ILinkHandler | null): Promise<void> {
  607. // Get the link path without the location prepended.
  608. // (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
  609. let href = anchor.getAttribute('href');
  610. // Bail if it is not a file-like url.
  611. if (!href || href.indexOf('://') !== -1 && href.indexOf('//') === 0) {
  612. return Promise.resolve(undefined);
  613. }
  614. // Remove the hash until we can handle it.
  615. let hash = anchor.hash;
  616. if (hash) {
  617. // Handle internal link in the file.
  618. if (hash === href) {
  619. anchor.target = '_self';
  620. return Promise.resolve(undefined);
  621. }
  622. // For external links, remove the hash until we have hash handling.
  623. href = href.replace(hash, '');
  624. }
  625. // Get the appropriate file path.
  626. return resolver.resolveUrl(href).then(path => {
  627. // Handle the click override.
  628. if (linkHandler && URLExt.isLocal(path)) {
  629. linkHandler.handleLink(anchor, path);
  630. }
  631. // Get the appropriate file download path.
  632. return resolver.getDownloadUrl(path);
  633. }).then(url => {
  634. // Set the visible anchor.
  635. anchor.href = url + hash;
  636. }).catch(err => {
  637. // If there was an error getting the url,
  638. // just make it an empty link.
  639. anchor.href = '';
  640. });
  641. }
  642. let markedInitialized = false;
  643. /**
  644. * Support GitHub flavored Markdown, leave sanitizing to external library.
  645. */
  646. function initializeMarked(): void {
  647. if (markedInitialized) {
  648. return;
  649. }
  650. markedInitialized = true;
  651. marked.setOptions({
  652. gfm: true,
  653. sanitize: false,
  654. tables: true,
  655. // breaks: true; We can't use GFM breaks as it causes problems with tables
  656. langPrefix: `cm-s-${CodeMirrorEditor.defaultConfig.theme} language-`,
  657. highlight: (code, lang, callback) => {
  658. let cb = (err: Error | null, code: string) => {
  659. if (callback) {
  660. callback(err, code);
  661. }
  662. return code;
  663. };
  664. if (!lang) {
  665. // no language, no highlight
  666. return cb(null, code);
  667. }
  668. Mode.ensure(lang).then(spec => {
  669. let el = document.createElement('div');
  670. if (!spec) {
  671. console.log(`No CodeMirror mode: ${lang}`);
  672. return cb(null, code);
  673. }
  674. try {
  675. Mode.run(code, spec.mime, el);
  676. return cb(null, el.innerHTML);
  677. } catch (err) {
  678. console.log(`Failed to highlight ${lang} code`, err);
  679. return cb(err, code);
  680. }
  681. }).catch(err => {
  682. console.log(`No CodeMirror mode: ${lang}`);
  683. console.log(`Require CodeMirror mode error: ${err}`);
  684. return cb(null, code);
  685. });
  686. return code;
  687. }
  688. });
  689. }
  690. }