widgets.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ansi_to_html, escape_for_html
  5. } from 'ansi_up';
  6. import {
  7. Mode, CodeMirrorEditor
  8. } from '@jupyterlab/codemirror';
  9. import * as marked
  10. from 'marked';
  11. import {
  12. Message
  13. } from '@phosphor/messaging';
  14. import {
  15. Widget
  16. } from '@phosphor/widgets';
  17. import {
  18. JSONObject
  19. } from '@phosphor/coreutils';
  20. import {
  21. RenderMime, typeset, removeMath, replaceMath
  22. } from '.';
  23. /*
  24. * The class name added to common rendered HTML.
  25. */
  26. const HTML_COMMON_CLASS = 'jp-RenderedHTMLCommon';
  27. /*
  28. * The class name added to rendered HTML.
  29. */
  30. const HTML_CLASS = 'jp-RenderedHTML';
  31. /*
  32. * The class name added to rendered markdown.
  33. */
  34. const MARKDOWN_CLASS = 'jp-RenderedMarkdown';
  35. /*
  36. * The class name added to rendered Latex.
  37. */
  38. const LATEX_CLASS = 'jp-RenderedLatex';
  39. /*
  40. * The class name added to rendered images.
  41. */
  42. const IMAGE_CLASS = 'jp-RenderedImage';
  43. /*
  44. * The class name added to rendered text.
  45. */
  46. const TEXT_CLASS = 'jp-RenderedText';
  47. /**
  48. * The class name added to an error output.
  49. */
  50. const ERROR_CLASS = 'jp-mod-error';
  51. /*
  52. * The class name added to rendered javascript.
  53. */
  54. const JAVASCRIPT_CLASS = 'jp-RenderedJavascript';
  55. /*
  56. * The class name added to rendered SVG.
  57. */
  58. const SVG_CLASS = 'jp-RenderedSVG';
  59. /*
  60. * The class name added to rendered PDF.
  61. */
  62. const PDF_CLASS = 'jp-RenderedPDF';
  63. /*
  64. * A widget for displaying any widget whoes representation is rendered HTML
  65. * */
  66. export
  67. class RenderedHTMLCommon extends Widget {
  68. /* Construct a new rendered HTML common widget.*/
  69. constructor(options: RenderMime.IRenderOptions) {
  70. super();
  71. this.addClass(HTML_COMMON_CLASS);
  72. }
  73. }
  74. /**
  75. * A widget for displaying HTML and rendering math.
  76. */
  77. export
  78. class RenderedHTML extends RenderedHTMLCommon {
  79. /**
  80. * Construct a new html widget.
  81. */
  82. constructor(options: RenderMime.IRenderOptions) {
  83. super(options);
  84. this.addClass(HTML_CLASS);
  85. let source = Private.getSource(options);
  86. if (!options.model.trusted) {
  87. source = options.sanitizer.sanitize(source);
  88. }
  89. Private.appendHtml(this.node, source);
  90. if (options.resolver) {
  91. this._urlResolved = Private.handleUrls(this.node, options.resolver,
  92. options.linkHandler);
  93. }
  94. }
  95. /**
  96. * A message handler invoked on an `'after-attach'` message.
  97. */
  98. onAfterAttach(msg: Message): void {
  99. if (this._urlResolved) {
  100. this._urlResolved.then( () => { typeset(this.node); });
  101. } else {
  102. typeset(this.node);
  103. }
  104. }
  105. private _urlResolved: Promise<void> = null;
  106. }
  107. /**
  108. * A widget for displaying Markdown with embeded latex.
  109. */
  110. export
  111. class RenderedMarkdown extends RenderedHTMLCommon {
  112. /**
  113. * Construct a new markdown widget.
  114. */
  115. constructor(options: RenderMime.IRenderOptions) {
  116. super(options);
  117. this.addClass(MARKDOWN_CLASS);
  118. // Initialize the marked library if necessary.
  119. Private.initializeMarked();
  120. let source = Private.getSource(options);
  121. let parts = removeMath(source);
  122. // Add the markdown content asynchronously.
  123. marked(parts['text'], (err: any, content: string) => {
  124. if (err) {
  125. console.error(err);
  126. return;
  127. }
  128. content = replaceMath(content, parts['math']);
  129. if (!options.model.trusted) {
  130. content = options.sanitizer.sanitize(content);
  131. }
  132. Private.appendHtml(this.node, content);
  133. if (options.resolver) {
  134. this._urlResolved = Private.handleUrls(this.node, options.resolver,
  135. options.linkHandler);
  136. }
  137. Private.headerAnchors(this.node);
  138. this.fit();
  139. this._rendered = true;
  140. if (this.isAttached) {
  141. if (this._urlResolved) {
  142. this._urlResolved.then(() => { typeset(this.node); });
  143. } else {
  144. typeset(this.node);
  145. }
  146. }
  147. });
  148. }
  149. /**
  150. * A message handler invoked on an `'after-attach'` message.
  151. */
  152. onAfterAttach(msg: Message): void {
  153. if (this._rendered) {
  154. typeset(this.node);
  155. }
  156. }
  157. private _rendered = false;
  158. private _urlResolved : Promise<void> = null;
  159. }
  160. /**
  161. * A widget for displaying LaTeX output.
  162. */
  163. export
  164. class RenderedLatex extends Widget {
  165. /**
  166. * Construct a new latex widget.
  167. */
  168. constructor(options: RenderMime.IRenderOptions) {
  169. super();
  170. let source = Private.getSource(options);
  171. this.node.textContent = source;
  172. this.addClass(LATEX_CLASS);
  173. }
  174. /**
  175. * A message handler invoked on an `'after-attach'` message.
  176. */
  177. onAfterAttach(msg: Message): void {
  178. typeset(this.node);
  179. }
  180. }
  181. /**
  182. * A widget for displaying rendered images.
  183. */
  184. export
  185. class RenderedImage extends Widget {
  186. /**
  187. * Construct a new rendered image widget.
  188. */
  189. constructor(options: RenderMime.IRenderOptions) {
  190. super();
  191. let img = document.createElement('img');
  192. let source = Private.getSource(options);
  193. img.src = `data:${options.mimeType};base64,${source}`;
  194. let metadata = options.model.metadata.get(options.mimeType) as JSONObject;
  195. if (metadata) {
  196. let metaJSON = metadata as JSONObject;
  197. if (typeof metaJSON['height'] === 'number') {
  198. img.height = metaJSON['height'] as number;
  199. }
  200. if (typeof metaJSON['width'] === 'number') {
  201. img.width = metaJSON['width'] as number;
  202. }
  203. }
  204. this.node.appendChild(img);
  205. this.addClass(IMAGE_CLASS);
  206. }
  207. }
  208. /**
  209. * A widget for displaying rendered text.
  210. */
  211. export
  212. class RenderedText extends Widget {
  213. /**
  214. * Construct a new rendered text widget.
  215. */
  216. constructor(options: RenderMime.IRenderOptions) {
  217. super();
  218. let source = Private.getSource(options);
  219. let data = escape_for_html(source);
  220. let pre = document.createElement('pre');
  221. pre.innerHTML = ansi_to_html(data);
  222. this.node.appendChild(pre);
  223. this.addClass(TEXT_CLASS);
  224. if (options.mimeType === 'application/vnd.jupyter.stderr') {
  225. this.addClass(ERROR_CLASS);
  226. }
  227. }
  228. }
  229. /**
  230. * A widget for displaying rendered JavaScript.
  231. */
  232. export
  233. class RenderedJavaScript extends Widget {
  234. /**
  235. * Construct a new rendered JavaScript widget.
  236. */
  237. constructor(options: RenderMime.IRenderOptions) {
  238. super();
  239. let s = document.createElement('script');
  240. s.type = options.mimeType;
  241. let source = Private.getSource(options);
  242. s.textContent = source;
  243. this.node.appendChild(s);
  244. this.addClass(JAVASCRIPT_CLASS);
  245. }
  246. }
  247. /**
  248. * A widget for displaying rendered SVG content.
  249. */
  250. export
  251. class RenderedSVG extends Widget {
  252. /**
  253. * Construct a new rendered SVG widget.
  254. */
  255. constructor(options: RenderMime.IRenderOptions) {
  256. super();
  257. let source = Private.getSource(options);
  258. this.node.innerHTML = source;
  259. let svgElement = this.node.getElementsByTagName('svg')[0];
  260. if (!svgElement) {
  261. throw new Error('SVGRender: Error: Failed to create <svg> element');
  262. }
  263. if (options.resolver) {
  264. this._urlResolved = Private.handleUrls(this.node, options.resolver,
  265. options.linkHandler);
  266. }
  267. this.addClass(SVG_CLASS);
  268. }
  269. private _urlResolved: Promise<void> = null;
  270. }
  271. /**
  272. * A widget for displaying rendered PDF content.
  273. */
  274. export
  275. class RenderedPDF extends Widget {
  276. /**
  277. * Construct a new rendered PDF widget.
  278. */
  279. constructor(options: RenderMime.IRenderOptions) {
  280. super();
  281. let source = Private.getSource(options);
  282. let a = document.createElement('a');
  283. a.target = '_blank';
  284. a.textContent = 'View PDF';
  285. a.href = `data:application/pdf;base64,${source}`;
  286. this.node.appendChild(a);
  287. this.addClass(PDF_CLASS);
  288. }
  289. }
  290. /**
  291. * The namespace for module private data.
  292. */
  293. namespace Private {
  294. /**
  295. * Extract the source text from render options.
  296. */
  297. export
  298. function getSource(options: RenderMime.IRenderOptions): string {
  299. return String(options.model.data.get(options.mimeType));
  300. }
  301. /**
  302. * Append trusted html to a node.
  303. */
  304. export
  305. function appendHtml(node: HTMLElement, html: string): void {
  306. try {
  307. let range = document.createRange();
  308. node.appendChild(range.createContextualFragment(html));
  309. } catch (error) {
  310. console.warn('Environment does not support Range ' +
  311. 'createContextualFragment, falling back on innerHTML');
  312. node.innerHTML = html;
  313. }
  314. }
  315. /**
  316. * Resolve the relative urls in element `src` and `href` attributes.
  317. *
  318. * @param node - The head html element.
  319. *
  320. * @param resolver - A url resolver.
  321. *
  322. * @param linkHandler - An optional link handler for nodes.
  323. *
  324. * @returns a promise fulfilled when the relative urls have been resolved.
  325. */
  326. export
  327. function handleUrls(node: HTMLElement, resolver: RenderMime.IResolver, linkHandler?: RenderMime.ILinkHandler): Promise<void> {
  328. let promises: Promise<void>[] = [];
  329. // Handle HTML Elements with src attributes.
  330. let nodes = node.querySelectorAll('*[src]');
  331. for (let i = 0; i < nodes.length; i++) {
  332. promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
  333. }
  334. let anchors = node.getElementsByTagName('a');
  335. for (let i = 0; i < anchors.length; i++) {
  336. promises.push(handleAnchor(anchors[i], resolver, linkHandler || null));
  337. }
  338. let links = node.getElementsByTagName('link');
  339. for (let i = 0; i < links.length; i++) {
  340. promises.push(handleAttr(links[i], 'href', resolver));
  341. }
  342. return Promise.all(promises).then(() => { return void 0; });
  343. }
  344. /**
  345. * Handle a node with a `src` or `href` attribute.
  346. */
  347. function handleAttr(node: HTMLElement, name: 'src' | 'href', resolver: RenderMime.IResolver): Promise<void> {
  348. let source = node.getAttribute(name);
  349. if (!source) {
  350. return Promise.resolve(void 0);
  351. }
  352. node.setAttribute(name, '');
  353. return resolver.resolveUrl(source).then(path => {
  354. return resolver.getDownloadUrl(path);
  355. }).then(url => {
  356. node.setAttribute(name, url);
  357. });
  358. }
  359. /**
  360. * Apply ids to headers.
  361. */
  362. export
  363. function headerAnchors(node: HTMLElement): void {
  364. let headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
  365. for (let headerType of headerNames){
  366. let headers = node.getElementsByTagName(headerType);
  367. for (let i=0; i < headers.length; i++) {
  368. let header = headers[i];
  369. header.id = header.innerHTML.replace(/ /g, '-');
  370. let anchor = document.createElement('a');
  371. anchor.target = '_self';
  372. anchor.textContent = '¶';
  373. anchor.href = '#' + header.id;
  374. anchor.classList.add('jp-InternalAnchorLink');
  375. header.appendChild(anchor);
  376. }
  377. }
  378. }
  379. /**
  380. * Handle an anchor node.
  381. */
  382. function handleAnchor(anchor: HTMLAnchorElement, resolver: RenderMime.IResolver, linkHandler: RenderMime.ILinkHandler | null): Promise<void> {
  383. anchor.target = '_blank';
  384. let href = anchor.getAttribute('href');
  385. if (!href) {
  386. return Promise.resolve(void 0);
  387. }
  388. return resolver.resolveUrl(href).then(path => {
  389. if (linkHandler) {
  390. linkHandler.handleLink(anchor, path);
  391. }
  392. return resolver.getDownloadUrl(path);
  393. }).then(url => {
  394. anchor.href = url;
  395. });
  396. }
  397. }
  398. /**
  399. * A namespace for private module data.
  400. */
  401. namespace Private {
  402. let initialized = false;
  403. /**
  404. * Support GitHub flavored Markdown, leave sanitizing to external library.
  405. */
  406. export
  407. function initializeMarked(): void {
  408. if (initialized) {
  409. return;
  410. }
  411. initialized = true;
  412. marked.setOptions({
  413. gfm: true,
  414. sanitize: false,
  415. tables: true,
  416. // breaks: true; We can't use GFM breaks as it causes problems with tables
  417. langPrefix: `cm-s-${CodeMirrorEditor.DEFAULT_THEME} language-`,
  418. highlight: (code, lang, callback) => {
  419. if (!lang) {
  420. // no language, no highlight
  421. if (callback) {
  422. callback(null, code);
  423. return;
  424. } else {
  425. return code;
  426. }
  427. }
  428. Mode.ensure(lang).then(spec => {
  429. let el = document.createElement('div');
  430. if (!spec) {
  431. console.log(`No CodeMirror mode: ${lang}`);
  432. callback(null, code);
  433. return;
  434. }
  435. try {
  436. Mode.run(code, spec.mime, el);
  437. callback(null, el.innerHTML);
  438. } catch (err) {
  439. console.log(`Failed to highlight ${lang} code`, err);
  440. callback(err, code);
  441. }
  442. }).catch(err => {
  443. console.log(`No CodeMirror mode: ${lang}`);
  444. console.log(`Require CodeMirror mode error: ${err}`);
  445. callback(null, code);
  446. });
  447. }
  448. });
  449. }
  450. }