瀏覽代碼

Merge pull request #538 from blink1073/colorize-code

RenderMime updates and Markdown code highlighting
Afshin Darian 8 年之前
父節點
當前提交
8b487b976c

+ 3 - 3
examples/console/src/index.ts

@@ -10,7 +10,7 @@ import {
 } from 'jupyter-js-services';
 
 import {
-  RenderMime, IRenderer, MimeMap
+  RenderMime
 } from 'jupyterlab/lib/rendermime';
 
 import {
@@ -74,7 +74,7 @@ function startApp(session: ISession) {
     new LatexRenderer(),
     new TextRenderer()
   ];
-  let renderers: MimeMap<IRenderer<Widget>> = {};
+  let renderers: RenderMime.MimeMap<RenderMime.IRenderer<Widget>> = {};
   let order: string[] = [];
   for (let t of transformers) {
     for (let m of t.mimetypes) {
@@ -82,7 +82,7 @@ function startApp(session: ISession) {
       order.push(m);
     }
   }
-  let rendermime = new RenderMime<Widget>(renderers, order);
+  let rendermime = new RenderMime<Widget>({ renderers, order });
 
   let consolePanel = new ConsolePanel({ session, rendermime });
   consolePanel.title.text = TITLE;

+ 1 - 2
examples/filebrowser/src/index.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  ContentsManager, SessionManager, IServiceManager, createServiceManager
+  IServiceManager, createServiceManager
 } from 'jupyter-js-services';
 
 import {
@@ -21,7 +21,6 @@ import {
   EditorWidgetFactory
 } from 'jupyterlab/lib/editorwidget/widget';
 
-
 import {
   showDialog, okButton
 } from 'jupyterlab/lib/dialog';

+ 3 - 3
examples/notebook/src/index.ts

@@ -19,7 +19,7 @@ import {
 } from 'jupyterlab/lib/docregistry';
 
 import {
-  RenderMime, IRenderer, MimeMap
+  RenderMime
 } from 'jupyterlab/lib/rendermime';
 
 import {
@@ -83,7 +83,7 @@ function createApp(manager: IServiceManager): void {
     new LatexRenderer(),
     new TextRenderer()
   ];
-  let renderers: MimeMap<IRenderer<Widget>> = {};
+  let renderers: RenderMime.MimeMap<RenderMime.IRenderer<Widget>> = {};
   let order: string[] = [];
   for (let t of transformers) {
     for (let m of t.mimetypes) {
@@ -91,7 +91,7 @@ function createApp(manager: IServiceManager): void {
       order.push(m);
     }
   }
-  let rendermime = new RenderMime<Widget>(renderers, order);
+  let rendermime = new RenderMime<Widget>({ renderers, order });
 
   let opener = {
     open: (widget: Widget) => {

+ 26 - 7
src/codemirror/index.ts

@@ -33,7 +33,6 @@ function loadModeByMIME(editor: CodeMirror.Editor, mimetype: string): void {
 }
 
 
-
 /**
  * Load a codemirror mode by mode name.
  */
@@ -43,6 +42,30 @@ function loadModeByName(editor: CodeMirror.Editor, mode: string): void {
 }
 
 
+/**
+ * Require a codemirror mode by name.
+ */
+export
+function requireMode(mode: string): Promise<CodeMirror.modespec> {
+  if (CodeMirror.modes.hasOwnProperty(mode)) {
+    let spec = CodeMirror.findModeByName(mode);
+    if (spec) {
+      spec.name = spec.mode;
+    }
+    return Promise.resolve(spec);
+  }
+  return new Promise<CodeMirror.modespec>((resolve, reject) => {
+    require([`codemirror/mode/${mode}/${mode}`], () => {
+      let spec = CodeMirror.findModeByName(mode);
+      if (spec) {
+        spec.name = spec.mode;
+      }
+      return Promise.resolve(spec);
+    });
+  });
+}
+
+
 /**
  * Load a CodeMirror mode based on a mode spec.
  */
@@ -51,11 +74,7 @@ function loadInfo(editor: CodeMirror.Editor, info: CodeMirror.modespec): void {
     editor.setOption('mode', 'null');
     return;
   }
-  if (CodeMirror.modes.hasOwnProperty(info.mode)) {
+  requireMode(info.mode).then(() => {
     editor.setOption('mode', info.mime);
-  } else {
-    require([`codemirror/mode/${info.mode}/${info.mode}`], () => {
-      editor.setOption('mode', info.mime);
-    });
-  }
+  });
 }

+ 4 - 0
src/default-theme/codemirror.css

@@ -0,0 +1,4 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/

+ 2 - 0
src/default-theme/index.css

@@ -11,6 +11,7 @@
 @import url('~jupyterlab/lib/running/theme.css');
 
 @import './about.css';
+@import './codemirror.css';
 @import './commandpalette.css';
 @import './completion.css';
 @import './console.css';
@@ -18,6 +19,7 @@
 @import './images.css';
 @import './inspector.css';
 @import './markdownwidget.css';
+@import './rendermime.css';
 
 @import '../landing/index.css';
 

+ 231 - 0
src/default-theme/rendermime.css

@@ -0,0 +1,231 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+.jp-Rendered-html {
+  color: #000;
+  /* any extras will just be numbers: */
+}
+
+.jp-Rendered-html em {
+  font-style: italic;
+}
+
+.jp-Rendered-html strong {
+  font-weight: bold;
+}
+
+.jp-Rendered-html u {
+  text-decoration: underline;
+}
+
+.jp-Rendered-html :link {
+  text-decoration: underline;
+}
+
+.jp-Rendered-html :visited {
+  text-decoration: underline;
+}
+
+.jp-Rendered-html h1 {
+  font-size: 185.7%;
+  margin: 1.08em 0 0 0;
+  font-weight: bold;
+  line-height: 1.0;
+}
+
+.jp-Rendered-html h2 {
+  font-size: 157.1%;
+  margin: 1.27em 0 0 0;
+  font-weight: bold;
+  line-height: 1.0;
+}
+
+.jp-Rendered-html h3 {
+  font-size: 128.6%;
+  margin: 1.55em 0 0 0;
+  font-weight: bold;
+  line-height: 1.0;
+}
+
+.jp-Rendered-html h4 {
+  font-size: 100%;
+  margin: 2em 0 0 0;
+  font-weight: bold;
+  line-height: 1.0;
+}
+
+.jp-Rendered-html h5 {
+  font-size: 100%;
+  margin: 2em 0 0 0;
+  font-weight: bold;
+  line-height: 1.0;
+  font-style: italic;
+}
+
+.jp-Rendered-html h6 {
+  font-size: 100%;
+  margin: 2em 0 0 0;
+  font-weight: bold;
+  line-height: 1.0;
+  font-style: italic;
+}
+
+.jp-Rendered-html h1:first-child {
+  margin-top: 0.538em;
+}
+
+.jp-Rendered-html h2:first-child {
+  margin-top: 0.636em;
+}
+
+.jp-Rendered-html h3:first-child {
+  margin-top: 0.777em;
+}
+
+.jp-Rendered-html h4:first-child {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html h5:first-child {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html h6:first-child {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html ul:not(.list-inline),
+.jp-Rendered-html ol:not(.list-inline) {
+  padding-left: 2em;
+}
+
+.jp-Rendered-html ul {
+  list-style: disc;
+}
+
+.jp-Rendered-html ul ul {
+  list-style: square;
+}
+
+.jp-Rendered-html ul ul ul {
+  list-style: circle;
+}
+
+.jp-Rendered-html ol {
+  list-style: decimal;
+}
+
+.jp-Rendered-html ol ol {
+  list-style: upper-alpha;
+}
+
+.jp-Rendered-html ol ol ol {
+  list-style: lower-alpha;
+}
+
+.jp-Rendered-html ol ol ol ol {
+  list-style: lower-roman;
+}
+
+.jp-Rendered-html ol ol ol ol ol {
+  list-style: decimal;
+}
+
+.jp-Rendered-html * + ul {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html * + ol {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html hr {
+  color: black;
+  background-color: black;
+}
+
+.jp-Rendered-html pre {
+  margin: 1em 2em;
+}
+
+.jp-Rendered-html pre,
+.jp-Rendered-html code {
+  border: 0;
+  background-color: #fff;
+  color: #000;
+  font-size: 100%;
+  padding: 0px;
+}
+
+.jp-Rendered-html blockquote {
+  margin: 1em 2em;
+}
+
+.jp-Rendered-html table {
+  margin-left: auto;
+  margin-right: auto;
+  border: 1px solid black;
+  border-collapse: collapse;
+}
+
+.jp-Rendered-html tr,
+.jp-Rendered-html th,
+.jp-Rendered-html td {
+  border: 1px solid black;
+  border-collapse: collapse;
+  margin: 1em 2em;
+}
+
+.jp-Rendered-html td,
+.jp-Rendered-html th {
+  text-align: left;
+  vertical-align: middle;
+  padding: 4px;
+}
+
+.jp-Rendered-html th {
+  font-weight: bold;
+}
+
+.jp-Rendered-html * + table {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html p {
+  text-align: left;
+}
+
+.jp-Rendered-html * + p {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html img {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.jp-Rendered-html * + img {
+  margin-top: 1em;
+}
+
+.jp-Rendered-html img,
+.jp-Rendered-html svg {
+  max-width: 100%;
+  height: auto;
+}
+
+.jp-Rendered-html img.unconfined,
+.jp-Rendered-html svg.unconfined {
+  max-width: none;
+}
+
+.jp-Rendered-html .alert {
+  margin-bottom: initial;
+}
+
+.jp-Rendered-html * + .alert {
+  margin-top: 1em;
+}

+ 15 - 7
src/inspector/handler.ts

@@ -26,13 +26,17 @@ import {
 } from '../notebook/cells/editor';
 
 import {
-  RenderMime, MimeMap
+  RenderMime
 } from '../rendermime';
 
 import {
   Inspector
 } from './';
 
+
+/**
+ * An object that handles code inspection.
+ */
 export
 class InspectionHandler implements IDisposable, Inspector.IInspectable {
   /**
@@ -139,9 +143,11 @@ class InspectionHandler implements IDisposable, Inspector.IInspectable {
 
     let details = content.payload.filter(i => (i as any).source === 'page')[0];
     if (details) {
-      let bundle = (details as any).data as MimeMap<string>;
-      update.content = this._rendermime.render(bundle);
-      this.inspected.emit(update);
+      let bundle = (details as any).data as RenderMime.MimeMap<string>;
+      this._rendermime.render(bundle, true).then(widget => {
+        update.content = widget;
+        this.inspected.emit(update);
+      });
       return;
     }
 
@@ -194,9 +200,11 @@ class InspectionHandler implements IDisposable, Inspector.IInspectable {
         return;
       }
 
-      let bundle = value.data as MimeMap<string>;
-      update.content = this._rendermime.render(bundle);
-      this.inspected.emit(update);
+      let bundle = value.data as RenderMime.MimeMap<string>;
+      this._rendermime.render(bundle, true).then(widget => {
+        update.content = widget;
+        this.inspected.emit(update);
+      });
     });
   }
 

+ 9 - 6
src/notebook/cells/widget.ts

@@ -10,7 +10,7 @@ import {
 } from '../../codemirror';
 
 import {
-  RenderMime, MimeMap
+  RenderMime
 } from '../../rendermime';
 
 import {
@@ -662,14 +662,17 @@ class MarkdownCellWidget extends BaseCellWidget {
       let text = model && model.source || DEFAULT_MARKDOWN_TEXT;
       // Do not re-render if the text has not changed.
       if (text !== this._prev) {
-        let bundle: MimeMap<string> = { 'text/markdown': text };
+        let bundle: RenderMime.MimeMap<string> = { 'text/markdown': text };
         this._markdownWidget.dispose();
-        this._markdownWidget = this._rendermime.render(bundle) || new Widget();
-        this._markdownWidget.addClass(MARKDOWN_CONTENT_CLASS);
-        (this.layout as PanelLayout).addChild(this._markdownWidget);
+        this._rendermime.render(bundle, this.trusted).then(widget => {
+          this._markdownWidget = widget || new Widget();
+          this._markdownWidget.addClass(MARKDOWN_CONTENT_CLASS);
+          (this.layout as PanelLayout).addChild(this._markdownWidget);
+        });
+      } else {
+        this._markdownWidget.show();
       }
       this._prev = text;
-      this._markdownWidget.show();
       this.toggleInput(false);
       this.addClass(RENDERED_CLASS);
     } else {

+ 40 - 84
src/notebook/output-area/widget.ts

@@ -30,13 +30,9 @@ import {
 } from 'phosphor-widget';
 
 import {
-  RenderMime, MimeMap
+  RenderMime
 } from '../../rendermime';
 
-import {
-  defaultSanitizer
-} from '../../sanitizer';
-
 import {
   nbformat
 } from '../notebook/nbformat';
@@ -137,19 +133,6 @@ const PROMPT_CLASS = 'jp-Output-prompt';
 const RESULT_CLASS = 'jp-Output-result';
 
 
-/**
- * A list of outputs considered safe.
- */
-const safeOutputs = ['text/plain', 'image/png', 'image/jpeg',
-                     'application/vnd.jupyter.console-text',
-                     'application/vnd.jupyter.console-stdin'];
-
-/**
- * A list of outputs that are sanitizable.
- */
-const sanitizable = ['text/svg', 'text/html', 'text/latex'];
-
-
 /**
  * An output area widget.
  *
@@ -172,6 +155,9 @@ class OutputAreaWidget extends Widget {
     this.layout = new PanelLayout();
   }
 
+  /**
+   * Create a mirrored output widget.
+   */
   mirror(): OutputAreaWidget {
     let rendermime = this._rendermime;
     let renderer = this._renderer;
@@ -716,21 +702,18 @@ class OutputWidget extends Widget {
    *
    * @param trusted - Whether the output is trusted.
    */
-  render(output: OutputAreaModel.Output, trusted?: boolean): void {
+  render(output: OutputAreaModel.Output, trusted=false): Promise<void> {
     // Handle an input request.
     if (output.output_type === 'input_request') {
       let child = new InputWidget(output as OutputAreaModel.IInputRequest);
       this.setOutput(child);
-      return;
+      return Promise.resolve(void 0);
     }
 
     // Extract the data from the output and sanitize if necessary.
     let rendermime = this._rendermime;
     let bundle = this.getBundle(output as nbformat.IOutput);
     let data = this.convertBundle(bundle);
-    if (!trusted) {
-      this.sanitize(data);
-    }
 
     // Clear the content.
     this.clear();
@@ -739,42 +722,43 @@ class OutputWidget extends Widget {
     let msg = 'Did not find renderer for output mimebundle.';
     if (!data) {
       console.log(msg);
-      return;
+      return Promise.resolve(void 0);
     }
 
     // Create the output result area.
-    let child = rendermime.render(data);
-    if (!child) {
-      console.log(msg);
-      console.log(data);
-      return;
-    }
-    this.setOutput(child);
+    return rendermime.render(data, trusted).then(child => {
+      if (!child) {
+        console.log(msg);
+        console.log(data);
+        return;
+      }
+      this.setOutput(child);
 
-    // Add classes and output prompt as necessary.
-    switch (output.output_type) {
-    case 'execute_result':
-      child.addClass(EXECUTE_CLASS);
-      let count = (output as nbformat.IExecuteResult).execution_count;
-      this.prompt.node.textContent = `Out[${count === null ? ' ' : count}]:`;
-      break;
-    case 'display_data':
-      child.addClass(DISPLAY_CLASS);
-      break;
-    case 'stream':
-      if ((output as nbformat.IStream).name === 'stdout') {
-        child.addClass(STDOUT_CLASS);
-      } else {
-        child.addClass(STDERR_CLASS);
+      // Add classes and output prompt as necessary.
+      switch (output.output_type) {
+      case 'execute_result':
+        child.addClass(EXECUTE_CLASS);
+        let count = (output as nbformat.IExecuteResult).execution_count;
+        this.prompt.node.textContent = `Out[${count === null ? ' ' : count}]:`;
+        break;
+      case 'display_data':
+        child.addClass(DISPLAY_CLASS);
+        break;
+      case 'stream':
+        if ((output as nbformat.IStream).name === 'stdout') {
+          child.addClass(STDOUT_CLASS);
+        } else {
+          child.addClass(STDERR_CLASS);
+        }
+        break;
+      case 'error':
+        child.addClass(ERROR_CLASS);
+        break;
+      default:
+        console.error(`Unrecognized output type: ${output.output_type}`);
+        data = {};
       }
-      break;
-    case 'error':
-      child.addClass(ERROR_CLASS);
-      break;
-    default:
-      console.error(`Unrecognized output type: ${output.output_type}`);
-      data = {};
-    }
+    });
   }
 
   /**
@@ -841,8 +825,8 @@ class OutputWidget extends Widget {
   /**
    * Convert a mime bundle to a mime map.
    */
-  protected convertBundle(bundle: nbformat.MimeBundle): MimeMap<string> {
-    let map: MimeMap<string> = Object.create(null);
+  protected convertBundle(bundle: nbformat.MimeBundle): RenderMime.MimeMap<string> {
+    let map: RenderMime.MimeMap<string> = Object.create(null);
     for (let mimeType in bundle) {
       let value = bundle[mimeType];
       if (Array.isArray(value)) {
@@ -854,34 +838,6 @@ class OutputWidget extends Widget {
     return map;
   }
 
-  /**
-   * Sanitize a mime map.
-   *
-   * @params map - The map to sanitize.
-   */
-  protected sanitize(map: MimeMap<string>): void {
-    let keys = Object.keys(map);
-    for (let key of keys) {
-      if (safeOutputs.indexOf(key) !== -1) {
-        continue;
-      } else if (sanitizable.indexOf(key) !== -1) {
-        let out = map[key];
-        if (typeof out === 'string') {
-          map[key] = defaultSanitizer.sanitize(out);
-        } else {
-          let message = 'Ignoring unsanitized ' + key +
-            ' output; could not sanitize because output is not a string.';
-          console.log(message);
-          delete map[key];
-        }
-      } else {
-        // Don't display if we don't know how to sanitize it.
-        console.log('Ignoring untrusted ' + key + ' output.');
-        delete map[key];
-      }
-    }
-  }
-
   private _rendermime: RenderMime<Widget> = null;
   private _placeholder: Widget = null;
 }

+ 316 - 38
src/renderers/index.ts

@@ -1,15 +1,16 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import * as CodeMirror
+  from 'codemirror';
+
+import 'codemirror/addon/runmode/runmode';
+
 import * as marked
   from 'marked';
 
 import {
-  IRenderer
-} from '../rendermime';
-
-import {
-  escape_for_html, ansi_to_html
+  ansi_to_html, escape_for_html
 } from 'ansi_up';
 
 import {
@@ -20,26 +21,80 @@ import {
   Message
 } from 'phosphor-messaging';
 
+import {
+  requireMode
+} from '../codemirror';
+
+import {
+  RenderMime
+} from '../rendermime';
+
 import {
   typeset, removeMath, replaceMath
 } from './latex';
 
-import {
-  defaultSanitizer
-} from '../sanitizer';
+
+/**
+ * The class name added to rendered widgets.
+ */
+const RENDERED_CLASS = 'jp-Rendered';
+
+/**
+ * The class name added to rendered html widgets.
+ */
+const RENDERED_HTML = 'jp-Rendered-html';
 
 
 // Support GitHub flavored Markdown, leave sanitizing to external library.
-marked.setOptions({ gfm: true, sanitize: false, breaks: true });
+marked.setOptions({
+  gfm: true,
+  sanitize: false,
+  breaks: true,
+  langPrefix: 'cm-s-default language-',
+  highlight: (code, lang, callback) => {
+    if (!lang) {
+        // no language, no highlight
+        if (callback) {
+            callback(null, code);
+            return;
+        } else {
+            return code;
+        }
+    }
+    requireMode(lang).then(spec => {
+      let el = document.createElement('div');
+      if (!spec) {
+          console.log(`No CodeMirror mode: ${lang}`);
+          callback(null, code);
+          return;
+      }
+      try {
+        CodeMirror.runMode(code, spec, el);
+        callback(null, el.innerHTML);
+      } catch (err) {
+        console.log(`Failed to highlight ${lang} code`, err);
+        callback(err, code);
+      }
+    }).catch(err => {
+      console.log(`No CodeMirror mode: ${lang}`);
+      console.log(`Require CodeMirror mode error: ${err}`);
+      callback(null, code);
+    });
+  }
+});
 
 
 /**
  * A widget for displaying HTML and rendering math.
  */
-export
 class HTMLWidget extends Widget {
+  /**
+   * Construct a new html widget.
+   */
   constructor(html: string) {
     super();
+    this.addClass(RENDERED_HTML);
+    this.addClass(RENDERED_CLASS);
     try {
       let range = document.createRange();
       this.node.appendChild(range.createContextualFragment(html));
@@ -52,43 +107,69 @@ class HTMLWidget extends Widget {
 
   /**
    * A message handler invoked on an `'after-attach'` message.
-   *
-   * ####Notes
-   * If the node is visible, it is typeset.
    */
-  onAfterAttach(msg: Message) {
+  onAfterAttach(msg: Message): void {
     typeset(this.node);
   }
 }
 
+
 /**
- * A widget for displaying text and rendering math.
+ * A widget for displaying LaTeX output.
  */
-export
 class LatexWidget extends Widget {
+  /**
+   * Construct a new latex widget.
+   */
   constructor(text: string) {
     super();
-    this.node.innerHTML = text;
+    this.node.textContent = text;
+    this.addClass(RENDERED_CLASS);
   }
 
   /**
    * A message handler invoked on an `'after-attach'` message.
-   *
-   * ####Notes
-   * If the node is visible, it is typeset.
    */
-  onAfterAttach(msg: Message) {
+  onAfterAttach(msg: Message): void {
     typeset(this.node);
   }
 }
 
+
 /**
  * A renderer for raw html.
  */
 export
-class HTMLRenderer implements IRenderer<Widget> {
+class HTMLRenderer implements RenderMime.IRenderer<Widget> {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['text/html'];
 
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return true;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): string {
+    return data;
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
   render(mimetype: string, data: string): Widget {
     return new HTMLWidget(data);
   }
@@ -99,14 +180,42 @@ class HTMLRenderer implements IRenderer<Widget> {
  * A renderer for `<img>` data.
  */
 export
-class ImageRenderer implements IRenderer<Widget> {
+class ImageRenderer implements RenderMime.IRenderer<Widget> {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['image/png', 'image/jpeg', 'image/gif'];
 
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return true;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): string {
+    return data;
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
   render(mimetype: string, data: string): Widget {
     let w = new Widget();
     let img = document.createElement('img');
     img.src = `data:${mimetype};base64,${data}`;
     w.node.appendChild(img);
+    w.addClass(RENDERED_CLASS);
     return w;
   }
 }
@@ -116,15 +225,41 @@ class ImageRenderer implements IRenderer<Widget> {
  * A renderer for plain text and Jupyter console text data.
  */
 export
-class TextRenderer implements IRenderer<Widget> {
+class TextRenderer implements RenderMime.IRenderer<Widget> {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['text/plain', 'application/vnd.jupyter.console-text'];
 
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return true;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): string {
+    data = escape_for_html(data);
+    return `<pre>${ansi_to_html(data)}</pre>`;
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
   render(mimetype: string, data: string): Widget {
     let w = new Widget();
-    let el = document.createElement('pre');
-    let esc = escape_for_html(data);
-    el.innerHTML = ansi_to_html(esc);
-    w.node.appendChild(el);
+    w.node.innerHTML = data;
+    w.addClass(RENDERED_CLASS);
     return w;
   }
 }
@@ -134,15 +269,43 @@ class TextRenderer implements IRenderer<Widget> {
  * A renderer for raw `<script>` data.
  */
 export
-class JavascriptRenderer implements IRenderer<Widget> {
+class JavascriptRenderer implements RenderMime.IRenderer<Widget> {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['text/javascript', 'application/javascript'];
 
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): string {
+    return data;
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
   render(mimetype: string, data: string): Widget {
     let w = new Widget();
     let s = document.createElement('script');
     s.type = mimetype;
     s.textContent = data;
     w.node.appendChild(s);
+    w.addClass(RENDERED_CLASS);
     return w;
   }
 }
@@ -152,9 +315,36 @@ class JavascriptRenderer implements IRenderer<Widget> {
  * A renderer for `<svg>` data.
  */
 export
-class SVGRenderer implements IRenderer<Widget> {
+class SVGRenderer implements RenderMime.IRenderer<Widget> {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['image/svg+xml'];
 
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return true;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): string {
+    return data;
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
   render(mimetype: string, data: string): Widget {
     let w = new Widget();
     w.node.innerHTML = data;
@@ -162,6 +352,7 @@ class SVGRenderer implements IRenderer<Widget> {
     if (!svgElement) {
       throw new Error('SVGRender: Error: Failed to create <svg> element');
     }
+    w.addClass(RENDERED_CLASS);
     return w;
   }
 }
@@ -171,9 +362,36 @@ class SVGRenderer implements IRenderer<Widget> {
  * A renderer for PDF data.
  */
 export
-class PDFRenderer implements IRenderer<Widget> {
+class PDFRenderer implements RenderMime.IRenderer<Widget> {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['application/pdf'];
 
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): string {
+    return data;
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
   render(mimetype: string, data: string): Widget {
     let w = new Widget();
     let a = document.createElement('a');
@@ -181,6 +399,7 @@ class PDFRenderer implements IRenderer<Widget> {
     a.textContent = 'View PDF';
     a.href = 'data:application/pdf;base64,' + data;
     w.node.appendChild(a);
+    w.addClass(RENDERED_CLASS);
     return w;
   }
 }
@@ -190,9 +409,36 @@ class PDFRenderer implements IRenderer<Widget> {
  * A renderer for LateX data.
  */
 export
-class LatexRenderer implements IRenderer<Widget> {
+class LatexRenderer implements RenderMime.IRenderer<Widget>  {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['text/latex'];
 
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return true;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): string {
+    return data;
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
   render(mimetype: string, data: string): Widget {
     return new LatexWidget(data);
   }
@@ -203,13 +449,45 @@ class LatexRenderer implements IRenderer<Widget> {
  * A renderer for Jupyter Markdown data.
  */
 export
-class MarkdownRenderer implements IRenderer<Widget> {
+class MarkdownRenderer implements RenderMime.IRenderer<Widget> {
+  /**
+   * The mimetypes this renderer accepts.
+   */
   mimetypes = ['text/markdown'];
 
-  render(mimetype: string, text: string): Widget {
-    let data = removeMath(text);
-    let html = marked(data['text']);
-    let sanitized = defaultSanitizer.sanitize(replaceMath(html, data['math']));
-    return new HTMLWidget(sanitized);
+  /**
+   * Whether the input can safely sanitized for a given mimetype.
+   */
+  sanitizable(mimetype: string): boolean {
+    return true;
+  }
+
+  /**
+   * Whether the input is safe without sanitization.
+   */
+  isSafe(mimetype: string): boolean {
+    return false;
+  }
+
+  /**
+   * Transform the input bundle.
+   */
+  transform(mimetype: string, data: string): Promise<string> {
+    let parts = removeMath(data);
+    return new Promise<string>((resolve, reject) => {
+      marked(parts['text'], (err, content) => {
+        if (err) {
+          reject(err);
+        }
+        resolve(replaceMath(content, parts['math']));
+      });
+    });
+  }
+
+  /**
+   * Render the transformed mime bundle.
+   */
+  render(mimetype: string, data: string): Widget {
+    return new HTMLWidget(data);
   }
 }

+ 1 - 1
src/renderers/latex.ts

@@ -203,7 +203,7 @@ function processMath(i: number, j: number, preProcess: (input: string) => string
     j--;
   }
   blocks[i] = '@@' + math.length + '@@'; // replace the current block text with a unique tag to find later
-  if (preProcess){
+  if (preProcess) {
     block = preProcess(block);
   }
   math.push(block);

+ 127 - 50
src/rendermime/index.ts

@@ -1,31 +1,13 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-/**
- * The interface for a renderer.
- */
-export
-interface IRenderer<T> {
-  /**
-   * The function that will render a mimebundle.
-   *
-   * @param mimetype - the mimetype for the data
-   * @param data - the data to render
-   */
-  render(mimetype: string, data: string): T;
+import {
+  Widget
+} from 'phosphor-widget';
 
-  /**
-   * The mimetypes this renderer accepts.
-   */
-  mimetypes: string[];
-}
-
-
-/**
- * A map of mimetypes to types.
- */
-export
-type MimeMap<T> = { [mimetype: string]: T };
+import {
+  ISanitizer, defaultSanitizer
+} from '../sanitizer';
 
 
 /**
@@ -40,42 +22,58 @@ type MimeMap<T> = { [mimetype: string]: T };
  * the mimetype in the `order` array.
  */
 export
-class RenderMime<T> {
+class RenderMime<T extends RenderMime.RenderedObject> {
   /**
    * Construct a renderer.
-   *
-   * @param renderers - a map of mimetypes to renderers.
-   * @param order - a list of mimetypes in order of precedence (earliest one has precedence).
    */
-  constructor(renderers: MimeMap<IRenderer<T>>, order: string[]) {
-    this._renderers = {};
-    for (let i in renderers) {
-      this._renderers[i] = renderers[i];
+  constructor(options: RenderMime.IOptions<T>) {
+    for (let mime in options.renderers) {
+      this._renderers[mime] = options.renderers[mime];
     }
-    this._order = order.slice();
+    this._order = options.order.slice();
+    this._sanitizer = options.sanitizer || defaultSanitizer;
   }
 
   /**
    * Render a mimebundle.
    *
    * @param bundle - the mimebundle to render.
+   *
+   * @param trusted - whether the bundle is trusted.
    */
-  render(bundle: MimeMap<string>): T {
-    let mimetype = this.preferredMimetype(bundle);
-    if (mimetype) {
-        return this._renderers[mimetype].render(mimetype, bundle[mimetype]);
+  render(bundle: RenderMime.MimeMap<string>, trusted=false): Promise<T> {
+    let mimetype = this.preferredMimetype(bundle, trusted);
+    if (!mimetype) {
+      return Promise.resolve(void 0);
     }
+    let renderer = this._renderers[mimetype];
+    let transform = renderer.transform(mimetype, bundle[mimetype]);
+    return Promise.resolve(transform).then(content => {
+      if (!trusted && renderer.sanitizable(mimetype)) {
+        content = this._sanitizer.sanitize(content);
+      }
+      return renderer.render(content, content);
+    });
   }
 
   /**
    * Find the preferred mimetype in a mimebundle.
    *
    * @param bundle - the mimebundle giving available mimetype content.
+   *
+   * @param trusted - whether the bundle is trusted.
+   *
+   * #### Notes
+   * If the bundle is not trusted, the highest preference
+   * mimetype that is sanitizable or safe will be chosen.
    */
-  preferredMimetype(bundle: MimeMap<string>): string {
+  preferredMimetype(bundle: RenderMime.MimeMap<string>, trusted=false): string {
     for (let m of this.order) {
       if (m in bundle) {
-        return m;
+        let renderer = this._renderers[m];
+        if (trusted || renderer.isSafe(m) || renderer.sanitizable(m)) {
+          return m;
+        }
       }
     }
   }
@@ -84,14 +82,11 @@ class RenderMime<T> {
    * Clone the rendermime instance with shallow copies of data.
    */
   clone(): RenderMime<T> {
-    return new RenderMime<T>(this._renderers, this.order);
-  }
-
-  /**
-   * Get a renderer by mimetype.
-   */
-  getRenderer(mimetype: string) {
-    return this._renderers[mimetype];
+    return new RenderMime<T>({
+      renderers: this._renderers,
+      order: this.order,
+      sanitizer: this._sanitizer
+    });
   }
 
   /**
@@ -106,9 +101,9 @@ class RenderMime<T> {
    * Use the index of `.order.length` to add to the end of the render precedence list,
    * which would make the new renderer the last choice.
    */
-  addRenderer(mimetype: string, renderer: IRenderer<T>, index = 0): void {
+  addRenderer(mimetype: string, renderer: RenderMime.IRenderer<T>, index = 0): void {
     this._renderers[mimetype] = renderer;
-    this._order.splice(index, 0, mimetype)
+    this._order.splice(index, 0, mimetype);
   }
 
   /**
@@ -140,6 +135,88 @@ class RenderMime<T> {
     this._order = value.slice();
   }
 
-  private _renderers: MimeMap<IRenderer<T>>;
+  private _renderers: RenderMime.MimeMap<RenderMime.IRenderer<T>> = Object.create(null);
   private _order: string[];
+  private _sanitizer: ISanitizer = null;
+}
+
+
+/**
+ * The namespace for RenderMime statics.
+ */
+export
+namespace RenderMime {
+  /**
+   * The options used to initialize a rendermime instance.
+   */
+  export
+  interface IOptions<T extends RenderedObject> {
+    /**
+     * A map of mimetypes to renderers.
+     */
+    renderers: MimeMap<IRenderer<T>>;
+
+    /**
+     * A list of mimetypes in order of precedence (earliest has precedence).
+     */
+    order: string[];
+
+    /**
+     * The sanitizer used to sanitize html inputs.
+     *
+     * The default is a shared
+     */
+    sanitizer?: ISanitizer;
+  }
+
+  /**
+   * Valid rendered object type.
+   */
+  export
+  type RenderedObject = HTMLElement | Widget;
+
+  /**
+   * A map of mimetypes to types.
+   */
+  export
+  type MimeMap<T> = { [mimetype: string]: T };
+
+  /**
+   * The interface for a renderer.
+   */
+  export
+  interface IRenderer<T extends RenderedObject> {
+    /**
+     * The mimetypes this renderer accepts.
+     */
+    mimetypes: string[];
+
+    /**
+     * Whether the input is safe without sanitization.
+     */
+    isSafe(mimetype: string): boolean;
+
+    /**
+     * Whether the input can safely sanitized for a given mimetype.
+     */
+    sanitizable(mimetype: string): boolean;
+
+    /**
+     * Transform the input bundle.
+     */
+    transform(mimetype: string, data: string): string | Promise<string>;
+
+    /**
+     * Render the transformed mime bundle.
+     *
+     * @param mimetype - the mimetype for the data
+     *
+     * @param data - the data to render.
+     *
+     * #### Notes
+     * It is assumed that the data has been run through [[transform]]
+     * and has been sanitized if necessary.
+     */
+    render(mimetype: string, data: string): T;
+  }
 }

+ 3 - 3
src/rendermime/plugin.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  RenderMime, MimeMap, IRenderer
+  RenderMime
 } from './index';
 
 import {
@@ -32,7 +32,7 @@ const renderMimeProvider = {
       new LatexRenderer(),
       new TextRenderer()
     ];
-    let renderers: MimeMap<IRenderer<Widget>> = {};
+    let renderers: RenderMime.MimeMap<RenderMime.IRenderer<Widget>> = {};
     let order: string[] = [];
     for (let t of transformers) {
       for (let m of t.mimetypes) {
@@ -40,6 +40,6 @@ const renderMimeProvider = {
         order.push(m);
       }
     }
-    return new RenderMime<Widget>(renderers, order);
+    return new RenderMime<Widget>({ renderers, order });
   }
 };

+ 7 - 3
src/sanitizer/index.ts

@@ -24,12 +24,16 @@ class Sanitizer implements ISanitizer {
   }
 
   private _options: sanitize.IOptions = {
-    allowedTags: sanitize.defaults.allowedTags.concat('h1', 'h2', 'img'),
+    allowedTags: sanitize.defaults.allowedTags.concat('svg', 'h1', 'h2', 'img', 'span'),
     allowedAttributes: {
       // Allow the "rel" attribute for <a> tags.
       'a': sanitize.defaults.allowedAttributes['a'].concat('rel'),
       // Allow the "src" attribute for <img> tags.
-      'img': ['src', 'height', 'width', 'alt']
+      'img': ['src', 'height', 'width', 'alt'],
+      // Allow "class" attribute for <code> tags.
+      'code': ['class'],
+      // Allow "class" attribute for <span> tags.
+      'span': ['class']
     },
     transformTags: {
       // Set the "rel" attribute for <a> tags to "nofollow".
@@ -43,4 +47,4 @@ class Sanitizer implements ISanitizer {
  * The default instance of an `ISanitizer` meant for use by user code.
  */
 export
-const defaultSanitizer: ISanitizer = new Sanitizer();
+const defaultSanitizer: ISanitizer = new Sanitizer();

+ 9 - 53
test/src/notebook/output-area/widget.spec.ts

@@ -20,7 +20,7 @@ import {
 } from '../../../../lib/notebook/output-area';
 
 import {
-  MimeMap, RenderMime
+  RenderMime
 } from '../../../../lib/rendermime';
 
 import {
@@ -74,13 +74,9 @@ class CustomOutputWidget extends OutputWidget {
     return super.getBundle(output);
   }
 
-  convertBundle(bundle: nbformat.MimeBundle): MimeMap<string> {
+  convertBundle(bundle: nbformat.MimeBundle): RenderMime.MimeMap<string> {
     return super.convertBundle(bundle);
   }
-
-  sanitize(map: MimeMap<string>): void {
-    super.sanitize(map);
-  }
 }
 
 
@@ -227,16 +223,6 @@ describe('notebook/output-area/widget', () => {
         expect(widget.trusted).to.be(true);
       });
 
-      it('should re-render the widgets', () => {
-        let widget = new OutputAreaWidget({ rendermime });
-        widget.model = new OutputAreaModel();
-        widget.model.add(DEFAULT_OUTPUTS[0]);
-        let child = widget.childAt(0);
-        let old = child.output;
-        widget.trusted = true;
-        expect(child.output).to.not.be(old);
-      });
-
     });
 
     describe('#collapsed', () => {
@@ -430,13 +416,14 @@ describe('notebook/output-area/widget', () => {
 
     describe('#clear()', () => {
 
-      it('should clear the current output', () => {
+      it('should clear the current output', (done) => {
         let widget = new OutputWidget({ rendermime });
-        widget.render(DEFAULT_OUTPUTS[0], true);
-        let output = widget.output;
-        widget.clear();
-        expect(widget.output).to.not.be(output);
-        expect(widget.output).to.be.a(Widget);
+        widget.render(DEFAULT_OUTPUTS[0], true).then(() => {
+          let output = widget.output;
+          widget.clear();
+          expect(widget.output).to.not.be(output);
+          expect(widget.output).to.be.a(Widget);
+        }).then(done, done);
       });
 
     });
@@ -513,37 +500,6 @@ describe('notebook/output-area/widget', () => {
 
     });
 
-    describe('#sanitize()', () => {
-
-      it('should sanitize html input', () => {
-        let map: MimeMap<string> = {
-          'text/html': '<div>hello, 1 < 2</div>'
-        };
-        let widget = new CustomOutputWidget({ rendermime });
-        widget.sanitize(map);
-        expect(map['text/html']).to.be('<div>hello, 1 &lt; 2</div>');
-      });
-
-      it('should allow text/plain', () => {
-        let map: MimeMap<string> = {
-          'text/plain': '<div>hello, 1 < 2</div>'
-        };
-        let widget = new CustomOutputWidget({ rendermime });
-        widget.sanitize(map);
-        expect(map['text/plain']).to.be('<div>hello, 1 < 2</div>');
-      });
-
-      it('should disallow unknown mimetype', () => {
-        let map: MimeMap<string> = {
-          'foo/bar': '<div>hello, 1 < 2</div>'
-        };
-        let widget = new CustomOutputWidget({ rendermime });
-        widget.sanitize(map);
-        expect(map['foo/bar']).to.be(void 0);
-      });
-
-    });
-
   });
 
 });

+ 414 - 114
test/src/renderers/renderers.spec.ts

@@ -9,184 +9,484 @@ import {
 } from '../../../lib/renderers';
 
 
-describe('jupyter-ui', () => {
+describe('renderers', () => {
 
   describe('TextRenderer', () => {
 
-    it('should have the text/plain and jupyter/console-text mimetype', () => {
-      let mimetypes = ['text/plain', 'application/vnd.jupyter.console-text'];
-      let t = new TextRenderer();
-      expect(t.mimetypes).to.eql(mimetypes);
+    describe('#mimetypes', () => {
+
+      it('should have the text/plain and jupyter/console-text mimetype', () => {
+        let mimetypes = ['text/plain', 'application/vnd.jupyter.console-text'];
+        let t = new TextRenderer();
+        expect(t.mimetypes).to.eql(mimetypes);
+      });
+
     });
 
-    it('should output the correct HTML', () => {
-      let consoleText = 'x = 2 ** a';
-      let t = new TextRenderer();
-      let w = t.render('application/vnd.jupyter.console-text', consoleText);
-      let el = w.node;
-      expect(el.innerHTML).to.be("<pre>x = 2 ** a</pre>");
-      expect(el.textContent).to.be(consoleText);
+    describe('#sanitizable()', () => {
+
+      it('should be `false`', () => {
+        let t = new TextRenderer();
+        expect(t.sanitizable('text/plain')).to.be(false);
+        expect(t.sanitizable('application/vnd.jupyter.console-text')).to.be(false);
+      });
+
     });
 
-    it('should output the correct HTML with ansi colors', () => {
-      let text = 'There is no text but \x1b[01;41;32mtext\x1b[00m.\nWoo.'
-      let plainText = 'There is no text but text.\nWoo.'
-      let innerHTML = '<pre>There is no text but <span style="color:rgb(0, 255, 0);background-color:rgb(187, 0, 0)">text</span>.\nWoo.</pre>'
-      let t = new TextRenderer();
-      let w = t.render('application/vnd.jupyter.console-text', text);
-      let el = w.node;
-      expect(el.innerHTML).to.be(innerHTML);
-      expect(el.textContent).to.be(plainText);
+    describe('#isSafe()', () => {
+
+      it('should be `true`', () => {
+        let t = new TextRenderer();
+        expect(t.isSafe('text/plain')).to.be(true);
+        expect(t.isSafe('application/vnd.jupyter.console-text')).to.be(true);
+      });
+
     });
 
-  });
+    describe('#transform()', () => {
+
+      it('should output the correct HTML', () => {
+        let t = new TextRenderer();
+        let text = t.transform('text/plain', 'x = 2 ** a');
+        expect(text).to.be('<pre>x = 2 ** a</pre>');
+      });
+
+      it('should output the correct HTML with ansi colors', () => {
+        let t = new TextRenderer();
+        let text = 'There is no text but \x1b[01;41;32mtext\x1b[00m.\nWoo.';
+        text = t.transform('application/vnd.jupyter.console-text', text);
+        expect(text).to.be('<pre>There is no text but <span style="color:rgb(0, 255, 0);background-color:rgb(187, 0, 0)">text</span>.\nWoo.</pre>');
+      });
+
+      it('should escape inline html', () => {
+        let t = new TextRenderer();
+        let text = 'There is no text <script>window.x=1</script> but \x1b[01;41;32mtext\x1b[00m.\nWoo.';
+        text = t.transform('application/vnd.jupyter.console-text', text);
+        expect(text).to.be('<pre>There is no text &lt;script&gt;window.x=1&lt;/script&gt; but <span style="color:rgb(0, 255, 0);background-color:rgb(187, 0, 0)">text</span>.\nWoo.</pre>');
+      });
+
+    });
+
+    describe('#render()', () => {
+
+      it('should set the inner html of the widget', () => {
+        let t = new TextRenderer();
+        let html = '<pre>Hello</pre>';
+        let widget = t.render('text/plain', html);
+        expect(widget.node.innerHTML).to.be(html);
+      });
+
+    });
 
+  });
 
   describe('LatexRenderer', () => {
 
-    it('should have the text/latex mimetype', () => {
-      let t = new LatexRenderer();
-      expect(t.mimetypes).to.eql(['text/latex']);
+    describe('#mimetypes', () => {
+
+      it('should have the text/latex mimetype', () => {
+        let t = new LatexRenderer();
+        expect(t.mimetypes).to.eql(['text/latex']);
+      });
+
+    });
+
+    describe('#sanitizable()', () => {
+
+      it('should be `false`', () => {
+        let t = new LatexRenderer();
+        expect(t.sanitizable('text/latex')).to.be(false);
+      });
+
+    });
+
+    describe('#isSafe()', () => {
+
+      it('should be `true`', () => {
+        let t = new LatexRenderer();
+        expect(t.isSafe('text/latex')).to.be(true);
+      });
+
+    });
+
+    describe('#transform()', () => {
+
+      it('should be a no-op', () => {
+        let mathJaxScript = '<script type="math/tex">\sum\limits_{i=0}^{\infty} \frac{1}{n^2}</script>';
+        let t = new LatexRenderer();
+        let text = t.transform('text/latex', mathJaxScript);
+        expect(text).to.be(mathJaxScript);
+      });
+
     });
 
-    it('should output the correct MathJax script', () => {
-      let latex = '\sum\limits_{i=0}^{\infty} \frac{1}{n^2}';
-      let mathJaxScript = '<script type="math/tex">\sum\limits_{i=0}^{\infty} \frac{1}{n^2}</script>';
-      let t = new LatexRenderer();
-      let w = t.render('text/latex', mathJaxScript);
-      expect(w.node.innerHTML).to.be(mathJaxScript);
+    describe('#render()', () => {
+
+      it('should set the textContent of the widget', () => {
+        let mathJaxScript = '\sum\limits_{i=0}^{\infty} \frac{1}{n^2}';
+        let t = new LatexRenderer();
+        let widget = t.render('text/latex', mathJaxScript);
+        expect(widget.node.textContent).to.be(mathJaxScript);
+      });
+
     });
 
   });
 
   describe('PDFRenderer', () => {
 
-    it('should have the application/pdf mimetype', () => {
-      let t = new PDFRenderer();
-      expect(t.mimetypes).to.eql(['application/pdf']);
+    describe('#mimetypes', () => {
+
+      it('should have the application/pdf mimetype', () => {
+        let t = new PDFRenderer();
+        expect(t.mimetypes).to.eql(['application/pdf']);
+      });
+
+    });
+
+    describe('#sanitizable()', () => {
+
+      it('should be `false`', () => {
+        let t = new PDFRenderer();
+        expect(t.sanitizable('application/pdf')).to.be(false);
+      });
+
+    });
+
+    describe('#isSafe()', () => {
+
+      it('should be `false`', () => {
+        let t = new PDFRenderer();
+        expect(t.isSafe('application/pdf')).to.be(false);
+      });
+
+    });
+
+    describe('#transform()', () => {
+
+      it('should be a no-op', () => {
+        let base64PDF = "I don't have a b64'd PDF";
+        let t = new PDFRenderer();
+        let text = t.transform('application/pdf', base64PDF);
+        expect(text).to.be(base64PDF);
+      });
+
     });
 
-    it('should output the correct HTML', () => {
-      let base64PDF = "I don't have a b64'd PDF";
-      let t = new PDFRenderer();
-      let w = t.render('application/pdf', base64PDF);
-      expect(w.node.innerHTML.indexOf('data:application/pdf')).to.not.be(-1);
+    describe('#render()', () => {
+
+      it('should render the correct HTML', () => {
+        let base64PDF = "I don't have a b64'd PDF";
+        let t = new PDFRenderer();
+        let w = t.render('application/pdf', base64PDF);
+        expect(w.node.innerHTML.indexOf('data:application/pdf')).to.not.be(-1);
+      });
+
     });
 
   });
 
   describe('JavascriptRenderer', () => {
 
-    it('should have the text/javascript mimetype', () => {
-      let mimetypes = ['text/javascript', 'application/javascript'];
-      let t = new JavascriptRenderer();
-      expect(t.mimetypes).to.eql(mimetypes);
+    describe('#mimetypes', () => {
+
+      it('should have the text/javascript mimetype', () => {
+        let mimetypes = ['text/javascript', 'application/javascript'];
+        let t = new JavascriptRenderer();
+        expect(t.mimetypes).to.eql(mimetypes);
+      });
+
     });
 
-    it('should create a script tag', () => {
-      let t = new JavascriptRenderer();
-      let w = t.render('text/javascript', 'window.x = 1');
-      let el = w.node.firstChild as HTMLElement;
-      expect(el.localName).to.be("script");
-      expect(el.textContent).to.be("window.x = 1");
+    describe('#sanitizable()', () => {
+
+      it('should be `false`', () => {
+        let t = new JavascriptRenderer();
+        expect(t.sanitizable('text/javascript')).to.be(false);
+      });
 
-      // Ensure script has not been run yet
-      expect((window as any).x).to.be(void 0);
-      // Put it on the DOM
-      w.attach(document.body);
-      // Should be evaluated now
-      expect((window as any).x).to.be(1);
-      w.dispose();
     });
 
-  });
+    describe('#isSafe()', () => {
+
+      it('should be `false`', () => {
+        let t = new JavascriptRenderer();
+        expect(t.isSafe('text/javascript')).to.be(false);
+      });
+
+    });
 
+    describe('#transform()', () => {
+
+      it('should be a no-op', () => {
+        let t = new PDFRenderer();
+        let text = t.transform('text/javascript', 'window.x = 1');
+        expect(text).to.be('window.x = 1');
+      });
+
+    });
+
+    describe('#render()', () => {
+
+      it('should create a script tag', () => {
+        let t = new JavascriptRenderer();
+        let w = t.render('text/javascript', 'window.x = 1');
+        let el = w.node.firstChild as HTMLElement;
+        expect(el.localName).to.be('script');
+        expect(el.textContent).to.be('window.x = 1');
+
+        // Ensure script has not been run yet
+        expect((window as any).x).to.be(void 0);
+        // Put it on the DOM
+        w.attach(document.body);
+        // Should be evaluated now
+        expect((window as any).x).to.be(1);
+        w.dispose();
+      });
+
+    });
+
+  });
 
   describe('SVGRenderer', () => {
 
-    it('should have the image/svg+xml mimetype', () => {
-      let t = new SVGRenderer();
-      expect(t.mimetypes).to.eql(['image/svg+xml']);
+    describe('#mimetypes', () => {
+
+      it('should have the image/svg+xml mimetype', () => {
+        let t = new SVGRenderer();
+        expect(t.mimetypes).to.eql(['image/svg+xml']);
+      });
+
+    });
+
+    describe('#sanitizable()', () => {
+
+      it('should be `true`', () => {
+        let t = new SVGRenderer();
+        expect(t.sanitizable('image/svg+xml')).to.be(true);
+      });
+
+    });
+
+    describe('#isSafe()', () => {
+
+      it('should be `false`', () => {
+        let t = new SVGRenderer();
+        expect(t.isSafe('image/svg+xml')).to.be(false);
+      });
+
+    });
+
+    describe('#transform()', () => {
+
+      it('should be a no-op', () => {
+        const svg = `
+            <?xml version="1.0" standalone="no"?>
+            <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+            SYSTEM "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+            <svg></svg>`;
+        let t = new SVGRenderer();
+        let text = t.transform('image/svg+xml', svg);
+        expect(text).to.be(svg);
+      });
+
     });
 
-    it('should create an svg tag', () => {
-      const svg = `
-          <?xml version="1.0" standalone="no"?>
-          <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
-          SYSTEM "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-          <svg></svg>
-      `;
-      let t = new SVGRenderer();
-      let w = t.render('image/svg+xml', svg);
+    describe('#render()', () => {
+
+      it('should create an svg tag', () => {
+        const svg = `
+            <?xml version="1.0" standalone="no"?>
+            <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+            SYSTEM "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+            <svg></svg>
+        `;
+        let t = new SVGRenderer();
+        let w = t.render('image/svg+xml', svg);
+        let svgEl = w.node.getElementsByTagName('svg')[0];
+        expect(svgEl).to.be.ok();
+      });
+
     });
 
   });
 
   describe('MarkdownRenderer', () => {
 
-    it('should have the text/markdown mimetype', function() {
-      let t = new MarkdownRenderer();
-      expect(t.mimetypes).to.eql(["text/markdown"]);
+    describe('#mimetypes', () => {
+
+      it('should have the text/markdown mimetype', function() {
+        let t = new MarkdownRenderer();
+        expect(t.mimetypes).to.eql(['text/markdown']);
+      });
+
+    });
+
+    describe('#sanitizable()', () => {
+
+      it('should be `true`', () => {
+        let t = new MarkdownRenderer();
+        expect(t.sanitizable('text/markdown')).to.be(true);
+      });
+
+    });
+
+    describe('#isSafe()', () => {
+
+      it('should be `false`', () => {
+        let t = new MarkdownRenderer();
+        expect(t.isSafe('text/markdown')).to.be(false);
+      });
+
     });
 
-    it('should create nice markup', () => {
-      let md = require('../../../examples/filebrowser/sample.md');
-      let t = new MarkdownRenderer();
-      let w = t.render('text/markdown', md as string);
-      expect(w.node.innerHTML).to.be(`<h1>Title first level</h1>\n<h2>Title second Level</h2>\n<h3>Title third level</h3>\n<h4>h4</h4>\n<h5>h5</h5>\n<h6>h6</h6>\n<h1>h1</h1>\n<h2>h2</h2>\n<h3>h3</h3>\n<h4>h4</h4>\n<h5>h6</h5>\n<p>This is just a sample paragraph<br>You can look at different level of nested unorderd list ljbakjn arsvlasc asc asc awsc asc ascd ascd ascd asdc asc</p>\n<ul>\n<li>level 1<ul>\n<li>level 2</li>\n<li>level 2</li>\n<li>level 2<ul>\n<li>level 3</li>\n<li>level 3<ul>\n<li>level 4<ul>\n<li>level 5<ul>\n<li>level 6</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>level 2</li>\n</ul>\n</li>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1<br>Ordered list</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n<li>level 1</li>\n<li>level 1<br>some Horizontal line</li>\n</ul>\n<hr>\n<h2>and another one</h2>\n<p>Colons can be used to align columns.</p>\n<table>\n<thead>\n<tr>\n<th>Tables</th>\n<th>Are</th>\n<th>Cool</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>col 3 is</td>\n<td>right-aligned</td>\n<td>1600</td>\n</tr>\n<tr>\n<td>col 2 is</td>\n<td>centered</td>\n<td>12</td>\n</tr>\n<tr>\n<td>zebra stripes</td>\n<td>are neat</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>\n<p>There must be at least 3 dashes separating each header cell.<br>The outer pipes (|) are optional, and you don\'t need to make the<br>raw Markdown line up prettily. You can also use inline Markdown.</p>\n`);
+    describe('#transform()', () => {
+
+      it('should create nice markup', (done) => {
+        let md = require('../../../examples/filebrowser/sample.md');
+        let t = new MarkdownRenderer();
+        t.transform('text/markdown', md as string).then(text => {
+          expect(text).to.be(`<h1 id="title-first-level">Title first level</h1>\n<h2 id="title-second-level">Title second Level</h2>\n<h3 id="title-third-level">Title third level</h3>\n<h4 id="h4">h4</h4>\n<h5 id="h5">h5</h5>\n<h6 id="h6">h6</h6>\n<h1 id="h1">h1</h1>\n<h2 id="h2">h2</h2>\n<h3 id="h3">h3</h3>\n<h4 id="h4">h4</h4>\n<h5 id="h6">h6</h5>\n<p>This is just a sample paragraph<br>You can look at different level of nested unorderd list ljbakjn arsvlasc asc asc awsc asc ascd ascd ascd asdc asc</p>\n<ul>\n<li>level 1<ul>\n<li>level 2</li>\n<li>level 2</li>\n<li>level 2<ul>\n<li>level 3</li>\n<li>level 3<ul>\n<li>level 4<ul>\n<li>level 5<ul>\n<li>level 6</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>level 2</li>\n</ul>\n</li>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1<br>Ordered list</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n<li>level 1</li>\n<li>level 1<br>some Horizontal line</li>\n</ul>\n<hr>\n<h2 id="and-another-one">and another one</h2>\n<p>Colons can be used to align columns.</p>\n<table>\n<thead>\n<tr>\n<th>Tables</th>\n<th style="text-align:center">Are</th>\n<th style="text-align:right">Cool</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>col 3 is</td>\n<td style="text-align:center">right-aligned</td>\n<td style="text-align:right">1600</td>\n</tr>\n<tr>\n<td>col 2 is</td>\n<td style="text-align:center">centered</td>\n<td style="text-align:right">12</td>\n</tr>\n<tr>\n<td>zebra stripes</td>\n<td style="text-align:center">are neat</td>\n<td style="text-align:right">1</td>\n</tr>\n</tbody>\n</table>\n<p>There must be at least 3 dashes separating each header cell.<br>The outer pipes (|) are optional, and you don&#39;t need to make the<br>raw Markdown line up prettily. You can also use inline Markdown.</p>\n`);
+        }).then(done, done);
+      });
+
+    });
+
+    describe('#render()', () => {
+
+      it('should set the inner html', () => {
+        let t = new MarkdownRenderer();
+        let html = '<p>hello</p>';
+        let w = t.render('text/markdown', html);
+        expect(w.node.innerHTML).to.be(html);
+      });
+
     });
 
   });
 
   describe('HTMLRenderer', () => {
 
-    it('should have the text/html mimetype', () => {
-      let t = new HTMLRenderer();
-      expect(t.mimetypes).to.eql(['text/html']);
+    describe('#mimetypes', () => {
+
+      it('should have the text/html mimetype', () => {
+        let t = new HTMLRenderer();
+        expect(t.mimetypes).to.eql(['text/html']);
+      });
+
+    });
+
+    describe('#sanitizable()', () => {
+
+      it('should be `true`', () => {
+        let t = new HTMLRenderer();
+        expect(t.sanitizable('text/html')).to.be(true);
+      });
+
+    });
+
+    describe('#isSafe()', () => {
+
+      it('should be `false`', () => {
+        let t = new HTMLRenderer();
+        expect(t.isSafe('text/html')).to.be(false);
+      });
+
     });
 
-    it('should create a div with all the passed in elements', () => {
-      let t = new HTMLRenderer();
-      const htmlText = '<h1>This is great</h1>';
-      let w = t.render('text/html', htmlText);
-      let el = w.node.firstChild as HTMLElement;
-      expect(el.innerHTML).to.be('This is great');
+    describe('#transform()', () => {
+
+      it('should be a no-op', () => {
+        let t = new HTMLRenderer();
+        const htmlText = '<h1>This is great</h1>';
+        let text = t.transform('text/html', htmlText);
+        expect(text).to.be(htmlText);
+      });
+
     });
 
-    it('should execute a script tag when attached', () => {
-      const htmlText = '<script>window.y=3;</script>';
-      let t = new HTMLRenderer();
-      let w = t.render('text/html', htmlText);
-      expect((window as any).y).to.be(void 0);
-      w.attach(document.body);
-      expect((window as any).y).to.be(3);
-      w.dispose();
+    describe('#render()', () => {
+
+      it('should set the inner HTML', () => {
+        let t = new HTMLRenderer();
+        const htmlText = '<h1>This is great</h1>';
+        let w = t.render('text/html', htmlText);
+        expect(w.node.innerHTML).to.be('<h1>This is great</h1>');
+      });
+
+      it('should execute a script tag when attached', () => {
+        const htmlText = '<script>window.y=3;</script>';
+        let t = new HTMLRenderer();
+        let w = t.render('text/html', htmlText);
+        expect((window as any).y).to.be(void 0);
+        w.attach(document.body);
+        expect((window as any).y).to.be(3);
+        w.dispose();
+      });
+
     });
 
   });
 
   describe('ImageRenderer', () => {
 
-    it('should support multiple mimetypes', () => {
-      let t = new ImageRenderer();
-      expect(t.mimetypes).to.eql(['image/png', 'image/jpeg', 'image/gif']);
-    });
-
-    it('should create an <img> with the right mimetype', () => {
-      const imageData = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
-      let t = new ImageRenderer();
-      let w = t.render('image/png', imageData);
-      let el = w.node.firstChild as HTMLImageElement;
-      expect(el.src).to.be('data:image/png;base64,' + imageData);
-      expect(el.localName).to.be('img');
-      expect(el.innerHTML).to.be('');
-
-      const imageData2 = 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs='
-      w = t.render('image/gif', imageData2);
-      el = w.node.firstChild as HTMLImageElement;
-      expect(el.src).to.be('data:image/gif;base64,' + imageData2)
-      expect(el.localName).to.be('img');
-      expect(el.innerHTML).to.be('');
+    describe('#mimetypes', () => {
+
+      it('should support multiple mimetypes', () => {
+        let t = new ImageRenderer();
+        expect(t.mimetypes).to.eql(['image/png', 'image/jpeg', 'image/gif']);
+      });
+
+    });
+
+    describe('#sanitizable()', () => {
+
+      it('should be `false`', () => {
+        let t = new ImageRenderer();
+        expect(t.sanitizable('image/png')).to.be(false);
+      });
+
+    });
+
+    describe('#isSafe()', () => {
+
+      it('should be `true`', () => {
+        let t = new ImageRenderer();
+        expect(t.isSafe('image/png')).to.be(true);
+      });
+
+    });
+
+    describe('#transform()', () => {
+
+      it('should be a no-op', () => {
+        let t = new ImageRenderer();
+        const imageData = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+        let text = t.transform('image/png', imageData);
+        expect(text).to.be(imageData);
+      });
+
+    });
+
+    describe('#render()', () => {
+
+      it('should create an <img> with the right mimetype', () => {
+        const imageData = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+        let t = new ImageRenderer();
+        let w = t.render('image/png', imageData);
+        let el = w.node.firstChild as HTMLImageElement;
+        expect(el.src).to.be('data:image/png;base64,' + imageData);
+        expect(el.localName).to.be('img');
+        expect(el.innerHTML).to.be('');
+
+        const imageData2 = 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=';
+        w = t.render('image/gif', imageData2);
+        el = w.node.firstChild as HTMLImageElement;
+        expect(el.src).to.be('data:image/gif;base64,' + imageData2);
+        expect(el.localName).to.be('img');
+        expect(el.innerHTML).to.be('');
+      });
+
     });
 
   });

+ 96 - 36
test/src/rendermime/rendermime.spec.ts

@@ -13,7 +13,7 @@ import {
 } from '../../../lib/renderers';
 
 import {
-  IRenderer, MimeMap, RenderMime
+  RenderMime
 } from '../../../lib/rendermime';
 
 
@@ -31,7 +31,7 @@ const TRANSFORMERS = [
 
 export
 function defaultRenderMime(): RenderMime<Widget> {
-  let renderers: MimeMap<IRenderer<Widget>> = {};
+  let renderers: RenderMime.MimeMap<RenderMime.IRenderer<Widget>> = {};
   let order: string[] = [];
   for (let t of TRANSFORMERS) {
     for (let m of t.mimetypes) {
@@ -39,7 +39,7 @@ function defaultRenderMime(): RenderMime<Widget> {
       order.push(m);
     }
   }
-  return new RenderMime<Widget>(renderers, order);
+  return new RenderMime<Widget>({ renderers, order });
 }
 
 
@@ -58,26 +58,84 @@ describe('jupyter-ui', () => {
 
     describe('#render()', () => {
 
-      it('should render a mimebundle', () => {
+      it('should render a mimebundle', (done) => {
         let r = defaultRenderMime();
-        let w = r.render({ 'text/plain': 'foo' });
-        expect(w instanceof Widget).to.be(true);
+        r.render({ 'text/plain': 'foo' }).then(w => {
+          expect(w instanceof Widget).to.be(true);
+        }).then(done, done);
       });
 
-      it('should return `undefined` for an unregistered mime type', () => {
+      it('should return `undefined` for an unregistered mime type', (done) => {
         let r = defaultRenderMime();
-        expect(r.render({ 'text/fizz': 'buzz' })).to.be(void 0);
+        r.render({ 'text/fizz': 'buzz' }).then(value => {
+          expect(value).to.be(void 0);
+        }).then(done, done);
       });
 
-      it('should render with the mimetype of highest precidence', () => {
-        let bundle: MimeMap<string> = {
+      it('should render with the mimetype of highest precidence', (done) => {
+        let bundle: RenderMime.MimeMap<string> = {
           'text/plain': 'foo',
           'text/html': '<h1>foo</h1>'
-        }
+        };
         let r = defaultRenderMime();
-        let w = r.render(bundle);
-        let el = w.node.firstChild as HTMLElement;
-        expect(el.localName).to.be('h1');
+        r.render(bundle, true).then(w => {
+          let el = w.node.firstChild as HTMLElement;
+          expect(el.localName).to.be('h1');
+        }).then(done, done);
+      });
+
+      it('should render the mimetype that is safe', (done) => {
+        let bundle: RenderMime.MimeMap<string> = {
+          'text/plain': 'foo',
+          'text/javascript': 'window.x = 1',
+          'image/png': 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
+        };
+        let r = defaultRenderMime();
+        r.render(bundle, false).then(w => {
+          let el = w.node.firstChild as HTMLElement;
+          expect(el.localName).to.be('img');
+        }).then(done, done);
+      });
+
+      it('should render the mimetype that is sanitizable', (done) => {
+        let bundle: RenderMime.MimeMap<string> = {
+          'text/plain': 'foo',
+          'text/html': '<h1>foo</h1>'
+        };
+        let r = defaultRenderMime();
+        r.render(bundle, false).then(w => {
+          let el = w.node.firstChild as HTMLElement;
+          expect(el.localName).to.be('h1');
+        }).then(done, done);
+      });
+
+      it('should sanitize markdown', (done) => {
+        let md = require('../../../examples/filebrowser/sample.md');
+        let r = defaultRenderMime();
+        r.render({ 'text/markdown': md as string }).then(widget => {
+          expect(widget.node.innerHTML).to.be(`<h1>Title first level</h1>\n<h2>Title second Level</h2>\n<h3>Title third level</h3>\n<h4>h4</h4>\n<h5>h5</h5>\n<h6>h6</h6>\n<h1>h1</h1>\n<h2>h2</h2>\n<h3>h3</h3>\n<h4>h4</h4>\n<h5>h6</h5>\n<p>This is just a sample paragraph<br>You can look at different level of nested unorderd list ljbakjn arsvlasc asc asc awsc asc ascd ascd ascd asdc asc</p>\n<ul>\n<li>level 1<ul>\n<li>level 2</li>\n<li>level 2</li>\n<li>level 2<ul>\n<li>level 3</li>\n<li>level 3<ul>\n<li>level 4<ul>\n<li>level 5<ul>\n<li>level 6</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>level 2</li>\n</ul>\n</li>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1<br>Ordered list</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1<ol>\n<li>level 1</li>\n<li>level 1</li>\n<li>level 1</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n<li>level 1</li>\n<li>level 1<br>some Horizontal line</li>\n</ul>\n<hr>\n<h2>and another one</h2>\n<p>Colons can be used to align columns.</p>\n<table>\n<thead>\n<tr>\n<th>Tables</th>\n<th>Are</th>\n<th>Cool</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>col 3 is</td>\n<td>right-aligned</td>\n<td>1600</td>\n</tr>\n<tr>\n<td>col 2 is</td>\n<td>centered</td>\n<td>12</td>\n</tr>\n<tr>\n<td>zebra stripes</td>\n<td>are neat</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>\n<p>There must be at least 3 dashes separating each header cell.<br>The outer pipes (|) are optional, and you don\'t need to make the<br>raw Markdown line up prettily. You can also use inline Markdown.</p>\n`);
+        }).then(done, done);
+      });
+
+      it('should sanitize html', (done) => {
+        let bundle: RenderMime.MimeMap<string> = {
+          'text/html': '<h1>foo <script>window.x=1></scrip></h1>'
+        };
+        let r = defaultRenderMime();
+        r.render(bundle).then(widget => {
+          expect(widget.node.innerHTML).to.be('<h1>foo </h1>');
+        }).then(done, done);
+      });
+
+      it('should sanitize svg', (done) => {
+        let bundle: RenderMime.MimeMap<string> = {
+          'image/svg+xml': '<svg><script>windox.x=1</script></svg>'
+        };
+        let r = defaultRenderMime();
+        r.render(bundle).then(widget => {
+          expect(widget.node.innerHTML.indexOf('svg')).to.not.be(-1);
+          expect(widget.node.innerHTML.indexOf('script')).to.be(-1);
+        }).then(done, done);
       });
 
     });
@@ -85,10 +143,10 @@ describe('jupyter-ui', () => {
     describe('#preferredMimetype()', () => {
 
       it('should find the preferred mimetype in a bundle', () => {
-        let bundle: MimeMap<string> = {
+        let bundle: RenderMime.MimeMap<string> = {
           'text/plain': 'foo',
           'text/html': '<h1>foo</h1>'
-        }
+        };
         let r = defaultRenderMime();
         expect(r.preferredMimetype(bundle)).to.be('text/html');
       });
@@ -98,6 +156,24 @@ describe('jupyter-ui', () => {
         expect(r.preferredMimetype({ 'text/fizz': 'buzz' })).to.be(void 0);
       });
 
+      it('should select the mimetype that is safe', () => {
+        let bundle: RenderMime.MimeMap<string> = {
+          'text/plain': 'foo',
+          'text/javascript': 'window.x = 1',
+          'image/png': 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
+        };
+        let r = defaultRenderMime();
+        expect(r.preferredMimetype(bundle, false)).to.be('image/png');
+      });
+
+      it('should render the mimetype that is sanitizable', () => {
+        let bundle: RenderMime.MimeMap<string> = {
+          'text/plain': 'foo',
+          'text/html': '<h1>foo</h1>'
+        };
+        let r = defaultRenderMime();
+        expect(r.preferredMimetype(bundle, false)).to.be('text/html');
+      });
     });
 
     describe('#clone()', () => {
@@ -106,38 +182,19 @@ describe('jupyter-ui', () => {
         let r = defaultRenderMime();
         let c = r.clone();
         expect(c.order).to.eql(r.order);
-        expect(c.getRenderer('text/html')).to.be(r.getRenderer('text/html'));
         let t = new TextRenderer();
         c.addRenderer('text/foo', t);
-        expect(c.getRenderer('text/foo')).to.be(t);
-        expect(r.getRenderer('text/foo')).to.be(void 0);
         expect(r).to.not.be(c);
       });
 
     });
 
-    describe('#getRenderer()', () => {
-
-      it('should get a renderer by mimetype', () => {
-        let r = defaultRenderMime();
-        let t = r.getRenderer('text/latex');
-        expect(t.mimetypes.indexOf('text/latex')).to.not.be(-1);
-      });
-
-      it('should return `undefined` for an unregistered type', () => {
-        let r = defaultRenderMime();
-        expect(r.getRenderer('text/foo')).to.be(void 0);
-      });
-
-    });
-
     describe('#addRenderer()', () => {
 
       it('should add a renderer by mimetype', () => {
         let r = defaultRenderMime();
         let t = new TextRenderer();
         r.addRenderer('text/foo', t);
-        expect(r.getRenderer('text/foo')).to.be(t);
         let index = r.order.indexOf('text/foo');
         expect(index).to.be(0);
       });
@@ -159,7 +216,10 @@ describe('jupyter-ui', () => {
       it('should remove a renderer by mimetype', () => {
         let r = defaultRenderMime();
         r.removeRenderer('text/html');
-        expect(r.getRenderer('text/html')).to.be(void 0);
+        let bundle: RenderMime.MimeMap<string> = {
+          'text/html': '<h1>foo</h1>'
+        };
+        expect(r.preferredMimetype(bundle)).to.be(void 0);
       });
 
       it('should be a no-op if the mimetype is not registered', () => {

+ 2 - 0
typings/codemirror/codemirror.d.ts

@@ -42,6 +42,8 @@ declare module CodeMirror {
     }
     var modeInfo: modeinfo[];
 
+    function runMode(code: string, mode: modespec, el: HTMLElement): void;
+
     var version: string;
 
     /** If you want to define extra methods in terms of the CodeMirror API, it is possible to use defineExtension.

+ 3 - 3
typings/marked/marked.d.ts

@@ -11,7 +11,7 @@ interface MarkedStatic {
      * @param callback Function called when the markdownString has been fully parsed when using async highlighting
      * @return String of compiled HTML
      */
-    (src: string, callback: Function): string;
+    (src: string, callback: (err: Error, content: string) => void): string;
 
     /**
      * Compiles markdown to HTML.
@@ -21,7 +21,7 @@ interface MarkedStatic {
      * @param callback Function called when the markdownString has been fully parsed when using async highlighting
      * @return String of compiled HTML
      */
-    (src: string, options?: MarkedOptions, callback?: Function): string;
+    (src: string, options?: MarkedOptions, callback?: (err: Error, content: string) => void): string;
 
     /**
      * @param src String of markdown source to be compiled
@@ -161,4 +161,4 @@ declare module "marked" {
     export = marked;
 }
 
-declare var marked: MarkedStatic;
+declare var marked: MarkedStatic;