瀏覽代碼

Merge pull request #2595 from sccolbert/feature-rendermime-api

Feature rendermime api
Steven Silvester 7 年之前
父節點
當前提交
9c2623bb67

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

@@ -24,7 +24,7 @@ import {
 } from '@jupyterlab/console';
 
 import {
-  RenderMime
+  RenderMime, defaultRendererFactories
 } from '@jupyterlab/rendermime';
 
 import '@jupyterlab/theming/style/index.css';
@@ -69,8 +69,9 @@ function startApp(path: string, manager: ServiceManager.IManager) {
     commands.processKeydownEvent(event);
   });
 
-  let initialFactories = RenderMime.getDefaultFactories();
-  let rendermime = new RenderMime({ initialFactories });
+  let rendermime = new RenderMime({
+    initialFactories: defaultRendererFactories
+  });
 
   let editorFactory = editorServices.factoryService.newInlineEditor.bind(
     editorServices.factoryService);

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

@@ -37,7 +37,7 @@ import {
 } from '@jupyterlab/docregistry';
 
 import {
-  RenderMime
+  RenderMime, defaultRendererFactories
 } from '@jupyterlab/rendermime';
 
 
@@ -94,8 +94,9 @@ function createApp(manager: ServiceManager.IManager): void {
     commands.processKeydownEvent(event);
   }, useCapture);
 
-  let initialFactories = RenderMime.getDefaultFactories();
-  let rendermime = new RenderMime({ initialFactories });
+  let rendermime = new RenderMime({
+    initialFactories: defaultRendererFactories
+  });
 
   let opener = {
     open: (widget: Widget) => {

+ 2 - 2
packages/application/src/index.ts

@@ -10,7 +10,7 @@ import {
 } from '@jupyterlab/docregistry';
 
 import {
-  IRenderMime, RenderMime
+  IRenderMime, RenderMime, defaultRendererFactories
 } from '@jupyterlab/rendermime';
 
 import {
@@ -66,7 +66,7 @@ class JupyterLab extends Application<ApplicationShell> {
         linker.connectNode(node, 'file-operations:open', { path });
       }
     };
-    let initialFactories = RenderMime.getDefaultFactories();
+    let initialFactories = defaultRendererFactories;
     this.rendermime = new RenderMime({ initialFactories, linkHandler });
 
     let registry = this.docRegistry = new DocumentRegistry();

+ 4 - 4
packages/application/src/mimerenderers.ts

@@ -35,9 +35,9 @@ function createRendermimePlugins(extensions: IRenderMime.IExtensionModule[]): Ju
       data = mod as any;
     }
     if (!Array.isArray(data)) {
-      data = [data];
+      data = [data] as ReadonlyArray<IRenderMime.IExtension>;
     }
-    data.forEach(item => {
+    (data as ReadonlyArray<IRenderMime.IExtension>).forEach(item => {
       let plugin = createRendermimePlugin(item);
       plugins.push(plugin);
     });
@@ -59,10 +59,10 @@ function createRendermimePlugin(item: IRenderMime.IExtension): JupyterLabPlugin<
       // Add the mime renderer.
       if (item.rank !== undefined) {
         app.rendermime.addFactory(
-          item.rendererFactory, item.mimeType, item.rank
+          item.rendererFactory, item.rank
         );
       } else {
-        app.rendermime.addFactory(item.rendererFactory, item.mimeType);
+        app.rendermime.addFactory(item.rendererFactory);
       }
 
       // Handle the widget factory.

+ 1 - 1
packages/cells/src/widget.ts

@@ -836,7 +836,7 @@ class MarkdownCell extends Cell {
     if (text !== this._prevText) {
       let mimeModel = new MimeModel({ data: { 'text/markdown': text }});
       if (!this._renderer) {
-        this._renderer = this._rendermime.createRenderer(mimeModel);
+        this._renderer = this._rendermime.createRenderer('text/markdown');
         this._renderer.addClass(MARKDOWN_OUTPUT_CLASS);
       }
       this._prevText = text;

+ 1 - 1
packages/docregistry/src/default.ts

@@ -505,7 +505,7 @@ class MimeRenderer extends Widget implements DocumentRegistry.IReadyWidget {
     }
     let mimeModel = new MimeModel({ data });
     if (!this._renderer) {
-      this._renderer = this.rendermime.createRenderer(mimeModel);
+      this._renderer = this.rendermime.createRenderer(this._mimeType);
       (this.layout as PanelLayout).addWidget(this._renderer);
     }
     return this._renderer.renderModel(mimeModel);

+ 9 - 4
packages/inspector/src/handler.ts

@@ -185,11 +185,16 @@ class InspectionHandler implements IDisposable, IInspector.IInspectable {
       }
 
       const data = value.data;
-      const model = new MimeModel({ data, trusted: true });
 
-      let widget = this._rendermime.createRenderer(model);
-      widget.renderModel(model);
-      update.content = widget;
+      let mimeType = this._rendermime.preferredMimeType(data, true);
+      if (mimeType) {
+        let widget = this._rendermime.createRenderer(mimeType);
+        const model = new MimeModel({ data });
+        widget.renderModel(model);
+        update.content = widget;
+      } else {
+        update.content = null;
+      }
       this._inspected.emit(update);
     });
   }

+ 5 - 2
packages/outputarea/src/widget.ts

@@ -414,8 +414,11 @@ class OutputArea extends Widget {
     prompt.addClass(OUTPUT_AREA_PROMPT_CLASS);
     panel.addWidget(prompt);
 
-    let output = this.rendermime.createRenderer(model);
-    if (output) {
+    let mimeType = this.rendermime.preferredMimeType(
+      model.data, !model.trusted
+    );
+    if (mimeType) {
+      let output = this.rendermime.createRenderer(mimeType);
       output.renderModel(model);
       output.addClass(OUTPUT_AREA_OUTPUT_CLASS);
       panel.addWidget(output);

+ 44 - 42
packages/rendermime-interfaces/src/index.ts

@@ -1,6 +1,7 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 import {
   ReadonlyJSONObject
 } from '@phosphor/coreutils';
@@ -21,7 +22,7 @@ namespace IRenderMime {
   export
   interface IMimeModel {
     /**
-     * Whether the model is trusted.
+     * Whether the data in the model is trusted.
      */
     readonly trusted: boolean;
 
@@ -39,9 +40,9 @@ namespace IRenderMime {
      * Set the data associated with the model.
      *
      * #### Notes
-     * Calling this function may trigger an asynchronous operation that could
-     * cause the renderer to be rendered with a new model containing the new
-     * data.
+     * Calling this function may trigger an asynchronous operation
+     * that could cause the renderer to be rendered with a new model
+     * containing the new data.
      */
     setData(options: IMimeModel.ISetDataOptions): void;
   }
@@ -71,8 +72,8 @@ namespace IRenderMime {
   /**
    * The options used to initialize a document widget factory.
    *
-   * This interface is intended to be used by mime renderer extensions to
-   * declaratively define a document opener that uses its renderer factory.
+   * This interface is intended to be used by mime renderer extensions
+   * to define a document opener that uses its renderer factory.
    */
   export
   interface IDocumentWidgetFactoryOptions {
@@ -84,7 +85,7 @@ namespace IRenderMime {
      * with '.', like '.png', '.txt', etc.  They may themselves contain a
      * period (e.g. .table.json).
      */
-    readonly fileExtensions: string[];
+    readonly fileExtensions: ReadonlyArray<string>;
 
     /**
      * The name of the widget to display in dialogs.
@@ -102,7 +103,7 @@ namespace IRenderMime {
      *
      * **See also:** [[fileExtensions]].
      */
-    readonly defaultFor?: string[];
+    readonly defaultFor?: ReadonlyArray<string>;
 
     /**
      * Whether the widget factory is read only.
@@ -133,42 +134,42 @@ namespace IRenderMime {
     /**
      * The MIME type for the renderer, which is the output MIME type it will handle.
      */
-    mimeType: string;
+    readonly mimeType: string;
 
     /**
      * A renderer factory to be registered to render the MIME type.
      */
-    rendererFactory: IRendererFactory;
+    readonly rendererFactory: IRendererFactory;
 
     /**
      * The rank passed to `RenderMime.addFactory`.
      */
-    rank?: number;
+    readonly rank?: number;
 
     /**
      * The timeout after user activity to re-render the data.
      */
-    renderTimeout?: number;
+    readonly renderTimeout?: number;
 
     /**
      * Preferred data type from the model.  Defaults to `string`.
      */
-    dataType?: 'string' | 'json';
+    readonly dataType?: 'string' | 'json';
 
     /**
      * The icon class name for the widget.
      */
-    iconClass?: string;
+    readonly iconClass?: string;
 
     /**
      * The icon label for the widget.
      */
-    iconLabel?: string;
+    readonly iconLabel?: string;
 
     /**
      * The options used to open a document with the renderer factory.
      */
-    documentWidgetFactoryOptions?: IDocumentWidgetFactoryOptions;
+    readonly documentWidgetFactoryOptions?: IDocumentWidgetFactoryOptions;
   }
 
   /**
@@ -180,16 +181,24 @@ namespace IRenderMime {
     /**
      * The default export.
      */
-    default: IExtension | IExtension[];
+    readonly default: IExtension | ReadonlyArray<IExtension>;
   }
 
   /**
-   * A widget that provides a ready promise.
+   * A widget which dislays the contents of a mime model.
    */
   export
   interface IRenderer extends Widget {
     /**
      * Render a mime model.
+     *
+     * @param model - The mime model to render.
+     *
+     * @returns A promise which resolves when rendering is complete.
+     *
+     * #### Notes
+     * This method may be called multiple times during the lifetime
+     * of the widget to update it if and when new data is available.
      */
     renderModel(model: IMimeModel): Promise<void>;
   }
@@ -200,30 +209,28 @@ namespace IRenderMime {
   export
   interface IRendererFactory {
     /**
-     * The mimeTypes this renderer accepts.
+     * Whether the factory is a "safe" factory.
+     *
+     * #### Notes
+     * A "safe" factory produces renderer widgets which can render
+     * untrusted model data in a usable way. *All* renderers must
+     * handle untrusted data safely, but some may simply failover
+     * with a "Run cell to view output" message. A "safe" renderer
+     * is an indication that its sanitized output will be useful.
      */
-    readonly mimeTypes: string[];
+    readonly safe: boolean;
 
     /**
-     * Whether the renderer can render given the render options.
-     *
-     * @param options - The options that would be used to render the data.
+     * The mime types handled by this factory.
      */
-    canCreateRenderer(options: IRendererOptions): boolean;
+    readonly mimeTypes: ReadonlyArray<string>;
 
     /**
-     * Create a renderer the transformed mime data.
+     * Create a renderer which displays the mime data.
      *
      * @param options - The options used to render the data.
      */
     createRenderer(options: IRendererOptions): IRenderer;
-
-    /**
-     * Whether the renderer will sanitize the data given the render options.
-     *
-     * @param options - The options that would be used to render the data.
-     */
-    wouldSanitize(options: IRendererOptions): boolean;
   }
 
   /**
@@ -236,11 +243,6 @@ namespace IRenderMime {
      */
     mimeType: string;
 
-    /**
-     * Whether the data is trusted.
-     */
-    trusted: boolean;
-
     /**
      * The html sanitizer.
      */
@@ -249,12 +251,12 @@ namespace IRenderMime {
     /**
      * An optional url resolver.
      */
-    resolver?: IResolver;
+    resolver: IResolver | null;
 
     /**
      * An optional link handler.
      */
-    linkHandler?: ILinkHandler;
+    linkHandler: ILinkHandler | null;
   }
 
   /**

+ 58 - 257
packages/rendermime/src/factories.ts

@@ -1,301 +1,102 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 import {
   IRenderMime
 } from '@jupyterlab/rendermime-interfaces';
 
-import {
-  RenderedHTML, RenderedMarkdown, RenderedText, RenderedImage,
-  RenderedJavaScript, RenderedSVG, RenderedPDF, RenderedLatex
-} from '.';
+import * as widgets
+  from './widgets';
 
 
 /**
- * A renderer for raw html.
+ * A mime renderer factory for raw html.
  */
 export
-class HTMLRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['text/html'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return this.mimeTypes.indexOf(options.mimeType) !== -1;
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedHTML(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return !options.trusted;
-  }
-}
+const htmlRendererFactory: IRenderMime.IRendererFactory = {
+  safe: true,
+  mimeTypes: ['text/html'],
+  createRenderer: options => new widgets.RenderedHTML(options)
+};
 
 
 /**
- * A renderer factory for `<img>` data.
+ * A mime renderer factory for images.
  */
 export
-class ImageRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['image/png', 'image/jpeg', 'image/gif'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return this.mimeTypes.indexOf(options.mimeType) !== -1;
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedImage(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return false;
-  }
-}
+const imageRendererFactory: IRenderMime.IRendererFactory = {
+  safe: true,
+  mimeTypes: ['image/png', 'image/jpeg', 'image/gif'],
+  createRenderer: options => new widgets.RenderedImage(options)
+};
 
 
 /**
- * A renderer factory for plain text and Jupyter console text data.
+ * A mime renderer factory for LaTeX.
  */
 export
-class TextRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['text/plain', 'application/vnd.jupyter.stdout',
-               'application/vnd.jupyter.stderr'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return this.mimeTypes.indexOf(options.mimeType) !== -1;
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedText(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return false;
-  }
-}
+const latexRendererFactory: IRenderMime.IRendererFactory = {
+  safe: true,
+  mimeTypes: ['text/latex'],
+  createRenderer: options => new widgets.RenderedLatex(options)
+};
 
 
 /**
- * A renderer factory for raw `<script>` data.
+ * A mime renderer factory for Markdown.
  */
 export
-class JavaScriptRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['text/javascript', 'application/javascript'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return (
-      options.trusted &&
-      this.mimeTypes.indexOf(options.mimeType) !== -1
-    );
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedJavaScript(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return false;
-  }
-}
+const markdownRendererFactory: IRenderMime.IRendererFactory = {
+  safe: true,
+  mimeTypes: ['text/markdown'],
+  createRenderer: options => new widgets.RenderedMarkdown(options)
+};
 
 
 /**
- * A renderer factory for `<svg>` data.
+ * A mime renderer factory for pdf.
  */
 export
-class SVGRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['image/svg+xml'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return (
-      options.trusted &&
-      this.mimeTypes.indexOf(options.mimeType) !== -1
-    );
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedSVG(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return false;
-  }
-}
+const pdfRendererFactory: IRenderMime.IRendererFactory = {
+  safe: false,
+  mimeTypes: ['application/pdf'],
+  createRenderer: options => new widgets.RenderedPDF(options)
+};
 
 
 /**
- * A renderer factory for PDF data.
+ * A mime renderer factory for svg.
  */
 export
-class PDFRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['application/pdf'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return (
-      options.trusted &&
-      this.mimeTypes.indexOf(options.mimeType) !== -1
-    );
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedPDF(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return false;
-  }
-}
+const svgRendererFactory: IRenderMime.IRendererFactory = {
+  safe: false,
+  mimeTypes: ['image/svg+xml'],
+  createRenderer: options => new widgets.RenderedSVG(options)
+};
 
 
 /**
- * A renderer factory for LateX data.
+ * A mime renderer factory for plain and jupyter console text data.
  */
 export
-class LatexRendererFactory implements IRenderMime.IRendererFactory  {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['text/latex'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return this.mimeTypes.indexOf(options.mimeType) !== -1;
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedLatex(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return false;
-  }
-}
+const textRendererFactory: IRenderMime.IRendererFactory = {
+  safe: true,
+  mimeTypes: ['text/plain', 'application/vnd.jupyter.stdout', 'application/vnd.jupyter.stderr'],
+  createRenderer: options => new widgets.RenderedText(options)
+};
 
 
 /**
- * A renderer factory for Jupyter Markdown data.
+ * The builtin factories provided by the rendermime package.
  */
 export
-class MarkdownRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this factory accepts.
-   */
-  mimeTypes = ['text/markdown'];
-
-  /**
-   * Whether the factory can create a renderer given the options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return this.mimeTypes.indexOf(options.mimeType) !== -1;
-  }
-
-  /**
-   * Create a renderer the transformed mime data.
-   *
-   * @param options - The options used to render the data.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedMarkdown(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return !options.trusted;
-  }
-}
+const defaultRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
+  htmlRendererFactory,
+  markdownRendererFactory,
+  latexRendererFactory,
+  svgRendererFactory,
+  imageRendererFactory,
+  pdfRendererFactory,
+  textRendererFactory
+];

+ 7 - 5
packages/rendermime/src/index.ts

@@ -1,12 +1,14 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import '../style/index.css';
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+import '../style/index.css';  // Why is this first?
 
 export * from '@jupyterlab/rendermime-interfaces';
+export * from './factories';
 export * from './latex';
 export * from './mimemodel';
 export * from './outputmodel';
+export * from './renderers';
 export * from './rendermime';
-export * from './factories';
 export * from './widgets';

+ 4 - 3
packages/rendermime/src/latex.ts

@@ -1,6 +1,7 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 // Some magic for deferring mathematical expressions to MathJax
 // by hiding them from the Markdown parser.
 // Some of the code here is adapted with permission from Davide Cervone

+ 4 - 3
packages/rendermime/src/mimemodel.ts

@@ -1,6 +1,7 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 import {
   ReadonlyJSONObject
 } from '@phosphor/coreutils';

+ 4 - 3
packages/rendermime/src/outputmodel.ts

@@ -1,6 +1,7 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 import {
   JSONExt, JSONObject, JSONValue, ReadonlyJSONObject
 } from '@phosphor/coreutils';

+ 801 - 0
packages/rendermime/src/renderers.ts

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

+ 151 - 139
packages/rendermime/src/rendermime.ts

@@ -1,14 +1,11 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 import {
   Contents, Session
 } from '@jupyterlab/services';
 
-import {
-  ArrayExt, each, find
-} from '@phosphor/algorithm';
-
 import {
   IRenderMime
 } from '@jupyterlab/rendermime-interfaces';
@@ -22,200 +19,193 @@ import {
 } from '@jupyterlab/apputils';
 
 import {
-  JavaScriptRendererFactory, HTMLRendererFactory, MarkdownRendererFactory,
-  LatexRendererFactory, SVGRendererFactory, ImageRendererFactory,
-  TextRendererFactory, PDFRendererFactory
-} from './factories';
+  ReadonlyJSONObject
+} from '@phosphor/coreutils';
 
 
 /**
- * A composite renderer.
+ * An object which manages mime renderer factories.
  *
- * The renderer is used to render mime models using registered
- * mime renderers, selecting the preferred mime renderer to
- * render the model into a widget.
+ * This object is used to render mime models using registered mime
+ * renderers, selecting the preferred mime renderer to render the
+ * model into a widget.
+ *
+ * #### Notes
+ * This class is not intended to be subclassed.
  */
 export
 class RenderMime {
   /**
-   * Construct a renderer.
+   * Construct a new rendermime.
+   *
+   * @param options - The options for initializing the instance.
    */
   constructor(options: RenderMime.IOptions = {}) {
-    this.sanitizer = options.sanitizer || defaultSanitizer;
+    // Parse the options.
     this.resolver = options.resolver || null;
     this.linkHandler = options.linkHandler || null;
-    let factories = options.initialFactories || [];
-    for (let factory of factories) {
-      for (let mime of factory.mimeTypes) {
-        this._addFactory(factory, mime);
+    this.sanitizer = options.sanitizer || defaultSanitizer;
+
+    // Add the initial factories.
+    if (options.initialFactories) {
+      for (let factory of options.initialFactories) {
+        this.addFactory(factory);
       }
     }
   }
 
   /**
-   * The object used to resolve relative urls for the rendermime instance.
+   * The sanitizer used by the rendermime instance.
    */
-  readonly resolver: IRenderMime.IResolver;
+  readonly sanitizer: ISanitizer;
 
   /**
-   * The object used to handle path opening links.
+   * The object used to resolve relative urls for the rendermime instance.
    */
-  readonly linkHandler: IRenderMime.ILinkHandler;
+  readonly resolver: IRenderMime.IResolver | null;
 
   /**
-   * The sanitizer used by the rendermime instance.
+   * The object used to handle path opening links.
    */
-  readonly sanitizer: ISanitizer;
+  readonly linkHandler: IRenderMime.ILinkHandler | null;
 
   /**
    * The ordered list of mimeTypes.
    */
   get mimeTypes(): ReadonlyArray<string> {
-    return this._mimeTypes;
+    return this._types || (this._types = Private.sortedTypes(this._ranks));
   }
 
   /**
-   * Create a renderer for a mime model.
+   * Find the preferred mime type for a mime bundle.
    *
-   * @param model - the mime model.
+   * @param bundle - The bundle of mime data.
    *
-   * @param mimeType - the optional explicit mimeType to use.
+   * @param preferSafe - Whether to prefer a safe factory.
    *
-   * #### Notes
-   * If no mimeType is given, the [preferredMimeType] is used.
+   * @returns The preferred mime type from the available factories,
+   *   or `undefined` if the mime type cannot be rendered.
    */
-  createRenderer(model: IRenderMime.IMimeModel, mimeType?: string): IRenderMime.IRenderer {
-    mimeType = mimeType || this.preferredMimeType(model);
-    let factory = this._factories[mimeType];
-    if (!factory) {
-      throw new Error('Cannot render model');
+  preferredMimeType(bundle: ReadonlyJSONObject, preferSafe: boolean): string | undefined {
+    // Try to find a safe factory first, if preferred.
+    if (preferSafe) {
+      for (let mt of this.mimeTypes) {
+        if (mt in bundle && this._factories[mt].safe) {
+          return mt;
+        }
+      }
     }
-    let options = {
-      mimeType,
-      trusted: model.trusted,
-      resolver: this.resolver,
-      sanitizer: this.sanitizer,
-      linkHandler: this.linkHandler
-    };
-    if (!factory.canCreateRenderer(options)) {
-      throw new Error('Cannot create renderer');
+
+    // Otherwise, search for the best factory among all factories.
+    for (let mt of this.mimeTypes) {
+      if (mt in bundle) {
+        return mt;
+      }
     }
-    return factory.createRenderer(options);
+
+    // Otherwise, no matching mime type exists.
+    return undefined;
   }
 
   /**
-   * Find the preferred mimeType for a model.
+   * Create a renderer for a mime type.
    *
-   * @param model - the mime model of interest.
+   * @param mimeType - The mime type of interest.
    *
-   * #### Notes
-   * The mimeTypes in the model are checked in preference order
-   * until a renderer returns `true` for `.canCreateRenderer`.
+   * @returns A new renderer for the given mime type.
+   *
+   * @throws An error if no factory exists for the mime type.
    */
-  preferredMimeType(model: IRenderMime.IMimeModel): string | undefined {
-    let sanitizer = this.sanitizer;
-    return find(this._mimeTypes, mimeType => {
-      if (mimeType in model.data) {
-        let options = { mimeType, sanitizer, trusted: model.trusted };
-        let renderer = this._factories[mimeType];
-        return renderer.canCreateRenderer(options);
-      }
-      return false;
+  createRenderer(mimeType: string): IRenderMime.IRenderer {
+    // Throw an error if no factory exists for the mime type.
+    if (!(mimeType in this._factories)) {
+      throw new Error(`No factory for mime type: '${mimeType}'`);
+    }
+
+    // Invoke the best factory for the given mime type.
+    return this._factories[mimeType].createRenderer({
+      mimeType,
+      resolver: this.resolver,
+      sanitizer: this.sanitizer,
+      linkHandler: this.linkHandler
     });
   }
 
   /**
-   * Clone the rendermime instance with shallow copies of data.
+   * Create a clone of this rendermime instance.
+   *
+   * @param options - The options for configuring the clone.
+   *
+   * @returns A new independent clone of the rendermime.
    */
   clone(options: RenderMime.ICloneOptions = {}): RenderMime {
-    let rendermime = new RenderMime({
+    // Create the clone.
+    let clone = new RenderMime({
+      resolver: options.resolver || this.resolver,
       sanitizer: options.sanitizer || this.sanitizer,
-      linkHandler: options.linkHandler || this.linkHandler,
-      resolver: options.resolver || this.resolver
-    });
-    each(this._mimeTypes, mimeType => {
-      let rank = this._ranks[mimeType];
-      rendermime.addFactory(this._factories[mimeType], mimeType, rank);
+      linkHandler: options.linkHandler || this.linkHandler
     });
-    return rendermime;
-  }
 
-  /**
-   * Add a renderer factory for a given mimeType.
-   *
-   * @param factory - The renderer factory.
-   *
-   * @param mimeType - The renderer mimeType.
-   *
-   * @param rank - The rank of the renderer. Defaults to 100.  Lower rank
-   *   indicates higher priority for rendering.
-   *
-   * #### Notes
-   * The renderer will replace an existing renderer for the given
-   * mimeType.
-   */
-  addFactory(factory: IRenderMime.IRendererFactory, mimeType: string, rank?: number): void {
-    this._addFactory(factory, mimeType, rank);
-  }
+    // Clone the internal state.
+    clone._factories = { ...this._factories };
+    clone._ranks = { ...this._ranks };
+    clone._id = this._id;
 
-  /**
-   * Remove a renderer factory by mimeType.
-   *
-   * @param mimeType - The mimeType of the factory.
-   */
-  removeFactory(mimeType: string): void {
-    this._removeFactory(mimeType);
+    // Return the cloned object.
+    return clone;
   }
 
   /**
-   * Get a renderer factory by mimeType.
+   * Get the renderer factory registered for a mime type.
    *
-   * @param mimeType - The mimeType of the renderer.
+   * @param mimeType - The mime type of interest.
    *
-   * @returns The renderer for the given mimeType, or undefined if the mimeType is unknown.
+   * @returns The factory for the mime type, or `undefined`.
    */
   getFactory(mimeType: string): IRenderMime.IRendererFactory | undefined {
     return this._factories[mimeType];
   }
 
   /**
-   * Add a factory to the rendermime instance.
+   * Add a renderer factory to the rendermime.
+   *
+   * @param factory - The renderer factory of interest.
+   *
+   * @param rank - The rank of the renderer. A lower rank indicates
+   *   a higher priority for rendering. The default is `100`.
+   *
+   * #### Notes
+   * The renderer will replace an existing renderer for the given
+   * mimeType.
    */
-  private _addFactory(factory: IRenderMime.IRendererFactory, mimeType: string, rank = 100): void {
-    // Remove any existing factory.
-    if (mimeType in this._factories) {
-      this._removeFactory(mimeType);
+  addFactory(factory: IRenderMime.IRendererFactory, rank = 100): void {
+    for (let mt of factory.mimeTypes) {
+      this._factories[mt] = factory;
+      this._ranks[mt] = { rank, id: this._id++ };
     }
-
-    // Add the new factory in the correct order.
-    this._ranks[mimeType] = rank;
-    let index = ArrayExt.upperBound(
-      this._mimeTypes, mimeType, (a, b) => {
-        return this._ranks[a] - this._ranks[b];
-    });
-    ArrayExt.insert(this._mimeTypes, index, mimeType);
-    this._factories[mimeType] = factory;
+    this._types = null;
   }
 
   /**
-   * Remove a renderer factory by mimeType.
+   * Remove the factory for a mime type.
    *
-   * @param mimeType - The mimeType of the factory.
+   * @param mimeType - The mime type of interest.
    */
-  private _removeFactory(mimeType: string): void {
+  removeFactory(mimeType: string): void {
     delete this._factories[mimeType];
     delete this._ranks[mimeType];
-    ArrayExt.removeFirstOf(this._mimeTypes, mimeType);
+    this._types = null;
   }
 
-  private _factories: { [key: string]: IRenderMime.IRendererFactory } = Object.create(null);
-  private _mimeTypes: string[] = [];
-  private _ranks: { [key: string]: number } = Object.create(null);
+  private _id = 0;
+  private _ranks: Private.RankMap = {};
+  private _types: string[] | null = null;
+  private _factories: Private.FactoryMap = {};
 }
 
 
 /**
- * The namespace for RenderMime statics.
+ * The namespace for `RenderMime` class statics.
  */
 export
 namespace RenderMime {
@@ -227,7 +217,7 @@ namespace RenderMime {
     /**
      * Intial factories to add to the rendermime instance.
      */
-    initialFactories?: IRenderMime.IRendererFactory[];
+    initialFactories?: ReadonlyArray<IRenderMime.IRendererFactory>;
 
     /**
      * The sanitizer used to sanitize untrusted html inputs.
@@ -249,23 +239,6 @@ namespace RenderMime {
     linkHandler?: IRenderMime.ILinkHandler;
   }
 
-  /**
-   * Get the default factories.
-   */
-  export
-  function getDefaultFactories(): IRenderMime.IRendererFactory[] {
-    return [
-      new JavaScriptRendererFactory(),
-      new HTMLRendererFactory(),
-      new MarkdownRendererFactory(),
-      new LatexRendererFactory(),
-      new SVGRendererFactory(),
-      new ImageRendererFactory(),
-      new PDFRendererFactory(),
-      new TextRendererFactory()
-    ];
-  }
-
   /**
    * The options used to clone a rendermime instance.
    */
@@ -341,3 +314,42 @@ namespace RenderMime {
     contents: Contents.IManager;
   }
 }
+
+
+/**
+ * The namespace for the module implementation details.
+ */
+namespace Private {
+  /**
+   * A type alias for a mime rank and tie-breaking id.
+   */
+  export
+  type RankPair = { readonly id: number, readonly rank: number };
+
+  /**
+   * A type alias for a mapping of mime type -> rank pair.
+   */
+  export
+  type RankMap = { [key: string]: RankPair };
+
+  /**
+   * A type alias for a mapping of mime type -> ordered factories.
+   */
+  export
+  type FactoryMap = { [key: string]: IRenderMime.IRendererFactory };
+
+  /**
+   * Get the mime types in the map, ordered by rank.
+   */
+  export
+  function sortedTypes(map: RankMap): string[] {
+    return Object.keys(map).sort((a, b) => {
+      let p1 = map[a];
+      let p2 = map[b];
+      if (p1.rank !== p2.rank) {
+        return p1.rank - p2.rank;
+      }
+      return p1.id - p2.id;
+    });
+  }
+}

+ 4 - 3
packages/rendermime/src/typings.d.ts

@@ -1,4 +1,5 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 /// <reference path="../typings/ansi_up/ansi_up.d.ts"/>

+ 167 - 448
packages/rendermime/src/widgets.ts

@@ -1,16 +1,10 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  ansi_to_html, escape_for_html
-} from 'ansi_up';
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 import {
-  Mode, CodeMirrorEditor
-} from '@jupyterlab/codemirror';
-
-import * as marked
-  from 'marked';
+  IRenderMime
+} from '@jupyterlab/rendermime-interfaces';
 
 import {
   Message
@@ -21,85 +15,30 @@ import {
 } from '@phosphor/widgets';
 
 import {
-  JSONObject
-} from '@phosphor/coreutils';
+  typeset
+} from './latex';
 
-import {
-  URLExt
-} from '@jupyterlab/coreutils';
-
-import {
-  IRenderMime
-} from '@jupyterlab/rendermime-interfaces';
+import * as renderers
+  from './renderers';
 
-import {
-  typeset, removeMath, replaceMath
-} from '.';
-
-
-/*
- * The class name added to common rendered HTML.
- */
-const HTML_COMMON_CLASS = 'jp-RenderedHTMLCommon';
-
-/*
- * The class name added to rendered HTML.
- */
-const HTML_CLASS = 'jp-RenderedHTML';
-
-/*
- * The class name added to rendered markdown.
- */
-const MARKDOWN_CLASS = 'jp-RenderedMarkdown';
-
-/*
- * The class name added to rendered Latex.
- */
-const LATEX_CLASS = 'jp-RenderedLatex';
-
-/*
- * The class name added to rendered images.
- */
-const IMAGE_CLASS = 'jp-RenderedImage';
-
-/*
- * The class name added to rendered text.
- */
-const TEXT_CLASS = 'jp-RenderedText';
 
 /**
- * The class name added to an error output.
- */
-const ERROR_CLASS = 'jp-mod-error';
-
-/*
- * The class name added to rendered javascript.
- */
-const JAVASCRIPT_CLASS = 'jp-RenderedJavascript';
-
-/*
- * The class name added to rendered SVG.
- */
-const SVG_CLASS = 'jp-RenderedSVG';
-
-/*
- * The class name added to rendered PDF.
- */
-const PDF_CLASS = 'jp-RenderedPDF';
-
-
-/*
- * A widget for displaying any widget whoes representation is rendered HTML.
+ * A common base class for mime renderers.
  */
 export
 abstract class RenderedCommon extends Widget implements IRenderMime.IRenderer {
-  /* Construct a new rendered HTML common widget.*/
+  /**
+   * Construct a new rendered common widget.
+   *
+   * @param options - The options for initializing the widget.
+   */
   constructor(options: IRenderMime.IRendererOptions) {
     super();
     this.mimeType = options.mimeType;
     this.sanitizer = options.sanitizer;
     this.resolver = options.resolver;
     this.linkHandler = options.linkHandler;
+    this.node.dataset['mimeType'] = this.mimeType;
   }
 
   /**
@@ -113,133 +52,91 @@ abstract class RenderedCommon extends Widget implements IRenderMime.IRenderer {
   readonly sanitizer: IRenderMime.ISanitizer;
 
   /**
-   * The link handler.
+   * The resolver object.
    */
-  readonly linkHandler: IRenderMime.ILinkHandler;
+  readonly resolver: IRenderMime.IResolver | null;
 
   /**
-   * The resolver object.
+   * The link handler.
    */
-  readonly resolver: IRenderMime.IResolver | null;
+  readonly linkHandler: IRenderMime.ILinkHandler | null;
 
   /**
    * Render a mime model.
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
    */
-  abstract renderModel(model: IRenderMime.IMimeModel): Promise<void>;
-}
+  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
+    // TODO compare model against old model for early bail?
 
+    // Toggle the trusted class on the widget.
+    this.toggleClass('jp-mod-trusted', model.trusted);
 
-/*
- * A widget for displaying any widget whoes representation is rendered HTML.
- * */
-export
-abstract class RenderedHTMLCommon extends RenderedCommon {
-  /* Construct a new rendered HTML common widget.*/
-  constructor(options: IRenderMime.IRendererOptions) {
-    super(options);
-    this.addClass(HTML_COMMON_CLASS);
+    // Render the actual content.
+    return this.render(model);
   }
+
+  /**
+   * Render the mime model.
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  abstract render(model: IRenderMime.IMimeModel): Promise<void>;
 }
 
 
 /**
- * A widget for displaying HTML and rendering math.
+ * A common base class for HTML mime renderers.
  */
 export
-class RenderedHTML extends RenderedHTMLCommon {
+abstract class RenderedHTMLCommon extends RenderedCommon {
   /**
-   * Construct a new html widget.
+   * Construct a new rendered HTML common widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    this.addClass(HTML_CLASS);
-  }
-
-  /**
-   * Render a mime model.
-   */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let source = Private.getSource(model, this.mimeType);
-    if (!model.trusted) {
-      source = this.sanitizer.sanitize(source);
-    }
-    Private.setHtml(this.node, source);
-    if (this.resolver) {
-      return Private.handleUrls(
-        this.node, this.resolver, this.linkHandler
-      ).then(() => {
-        if (this.isAttached) {
-          typeset(this.node);
-        }
-      });
-    }
-    if (this.isAttached) {
-      typeset(this.node);
-    }
-    return Promise.resolve(void 0);
-  }
-
-  /**
-   * A message handler invoked on an `'after-attach'` message.
-   */
-  onAfterAttach(msg: Message): void {
-    typeset(this.node);
+    this.addClass('jp-RenderedHTMLCommon');
   }
 }
 
 
 /**
- * A widget for displaying Markdown with embeded latex.
+ * A mime renderer for displaying HTML and math.
  */
 export
-class RenderedMarkdown extends RenderedHTMLCommon {
+class RenderedHTML extends RenderedHTMLCommon {
   /**
-   * Construct a new markdown widget.
+   * Construct a new rendered HTML widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    this.addClass(MARKDOWN_CLASS);
-
-    // Initialize the marked library if necessary.
-    Private.initializeMarked();
+    this.addClass('jp-RenderedHTML');
   }
 
   /**
    * Render a mime model.
-   */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    return new Promise<void>((resolve, reject) => {
-      let source = Private.getSource(model, this.mimeType);
-      let parts = removeMath(source);
-      // Add the markdown content asynchronously.
-      marked(parts['text'], (err: any, content: string) => {
-        if (err) {
-          console.error(err);
-          return;
-        }
-        content = replaceMath(content, parts['math']);
-        if (!model.trusted) {
-          content = this.sanitizer.sanitize(content);
-        }
-        Private.setHtml(this.node, content);
-        Private.headerAnchors(this.node);
-        this.fit();
-        if (this.resolver) {
-          Private.handleUrls(
-            this.node, this.resolver, this.linkHandler
-          ).then(() => {
-            if (this.isAttached) {
-              typeset(this.node);
-            }
-            resolve(void 0);
-          });
-        } else {
-          if (this.isAttached) {
-            typeset(this.node);
-          }
-          resolve(void 0);
-        }
-      });
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return renderers.renderHTML({
+      host: this.node,
+      source: String(model.data[this.mimeType]),
+      trusted: model.trusted,
+      resolver: this.resolver,
+      sanitizer: this.sanitizer,
+      linkHandler: this.linkHandler,
+      shouldTypeset: this.isAttached
     });
   }
 
@@ -253,28 +150,33 @@ class RenderedMarkdown extends RenderedHTMLCommon {
 
 
 /**
- * A widget for displaying LaTeX output.
+ * A mime renderer for displaying LaTeX output.
  */
 export
 class RenderedLatex extends RenderedCommon {
   /**
-   * Construct a new latex widget.
+   * Construct a new rendered Latex widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    this.addClass(LATEX_CLASS);
+    this.addClass('jp-RenderedLatex');
   }
 
   /**
    * Render a mime model.
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
    */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let source = Private.getSource(model, this.mimeType);
-    this.node.textContent = source;
-    if (this.isAttached) {
-      typeset(this.node);
-    }
-    return Promise.resolve(void 0);
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return renderers.renderLatex({
+      host: this.node,
+      source: String(model.data[this.mimeType]),
+      shouldTypeset: this.isAttached
+    });
   }
 
   /**
@@ -287,134 +189,141 @@ class RenderedLatex extends RenderedCommon {
 
 
 /**
- * A widget for displaying rendered images.
+ * A mime renderer for displaying images.
  */
 export
 class RenderedImage extends RenderedCommon {
   /**
    * Construct a new rendered image widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    let img = document.createElement('img');
-    this.node.appendChild(img);
-    this.addClass(IMAGE_CLASS);
+    this.addClass('jp-RenderedImage');
   }
 
   /**
    * Render a mime model.
-   */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let source = Private.getSource(model, this.mimeType);
-    let img = this.node.firstChild as HTMLImageElement;
-    img.src = `data:${this.mimeType};base64,${source}`;
-    let metadata = model.metadata[this.mimeType] as JSONObject;
-    if (metadata) {
-      let metaJSON = metadata as JSONObject;
-      if (typeof metaJSON['height'] === 'number') {
-        img.height = metaJSON['height'] as number;
-      }
-      if (typeof metaJSON['width'] === 'number') {
-        img.width = metaJSON['width'] as number;
-      }
-    }
-    return Promise.resolve(void 0);
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return renderers.renderImage({
+      host: this.node,
+      mimeType: this.mimeType,
+      source: String(model.data[this.mimeType]),
+      width: model.metadata.width as number | undefined,
+      height: model.metadata.height as number | undefined
+    });
   }
 }
 
 
 /**
- * A widget for displaying rendered text.
+ * A mime renderer for displaying Markdown with embeded latex.
  */
 export
-class RenderedText extends RenderedCommon {
+class RenderedMarkdown extends RenderedHTMLCommon {
   /**
-   * Construct a new rendered text widget.
+   * Construct a new rendered markdown widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    let pre = document.createElement('pre');
-    this.node.appendChild(pre);
-    this.addClass(TEXT_CLASS);
-    if (this.mimeType === 'application/vnd.jupyter.stderr') {
-      this.addClass(ERROR_CLASS);
-    }
+    this.addClass('jp-RenderedMarkdown');
   }
 
   /**
    * Render a mime model.
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return renderers.renderMarkdown({
+      host: this.node,
+      source: String(model.data[this.mimeType]),
+      trusted: model.trusted,
+      resolver: this.resolver,
+      sanitizer: this.sanitizer,
+      linkHandler: this.linkHandler,
+      shouldTypeset: this.isAttached
+    });
+  }
+
+  /**
+   * A message handler invoked on an `'after-attach'` message.
    */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let source = Private.getSource(model, this.mimeType);
-    let data = escape_for_html(source);
-    let pre = this.node.firstChild as HTMLPreElement;
-    while (pre.firstChild) {
-      pre.removeChild(pre.firstChild);
-    }
-    pre.innerHTML = ansi_to_html(data, {use_classes: true});
-    return Promise.resolve(void 0);
+  onAfterAttach(msg: Message): void {
+    typeset(this.node);
   }
 }
 
 
 /**
- * A widget for displaying rendered JavaScript.
+ * A widget for displaying rendered PDF content.
  */
 export
-class RenderedJavaScript extends RenderedCommon {
+class RenderedPDF extends RenderedCommon {
   /**
-   * Construct a new rendered JavaScript widget.
+   * Construct a new rendered PDF widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    let s = document.createElement('script');
-    s.type = options.mimeType;
-    this.node.appendChild(s);
-    this.addClass(JAVASCRIPT_CLASS);
+    this.addClass('jp-RenderedPDF');
   }
 
   /**
    * Render a mime model.
    */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let s = this.node.firstChild as HTMLScriptElement;
-    let source = Private.getSource(model, this.mimeType);
-    s.textContent = source;
-    return Promise.resolve(void 0);
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return renderers.renderPDF({
+      host: this.node,
+      source: String(model.data[this.mimeType]),
+      trusted: model.trusted
+    });
   }
 }
 
 
 /**
- * A widget for displaying rendered SVG content.
+ * A widget for displaying SVG content.
  */
 export
 class RenderedSVG extends RenderedCommon {
   /**
    * Construct a new rendered SVG widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    this.addClass(SVG_CLASS);
+    this.addClass('jp-RenderedSVG');
   }
 
   /**
    * Render a mime model.
-   */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let source = Private.getSource(model, this.mimeType);
-    Private.setHtml(this.node, source);
-    let svgElement = this.node.getElementsByTagName('svg')[0];
-    if (!svgElement) {
-      let msg = 'SVGRender: Error: Failed to create <svg> element';
-      return Promise.reject(new Error(msg));
-    }
-    if (this.resolver) {
-      return Private.handleUrls(
-        this.node, this.resolver, this.linkHandler
-      );
-    }
-    return Promise.resolve(void 0);
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return renderers.renderSVG({
+      host: this.node,
+      source: String(model.data[this.mimeType]),
+      trusted: model.trusted,
+      resolver: this.resolver,
+      linkHandler: this.linkHandler,
+      shouldTypeset: this.isAttached
+    });
   }
 
   /**
@@ -427,221 +336,31 @@ class RenderedSVG extends RenderedCommon {
 
 
 /**
- * A widget for displaying rendered PDF content.
+ * A widget for displaying plain text and console text.
  */
 export
-class RenderedPDF extends RenderedCommon {
+class RenderedText extends RenderedCommon {
   /**
-   * Construct a new rendered PDF widget.
+   * Construct a new rendered text widget.
+   *
+   * @param options - The options for initializing the widget.
    */
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
-    let a = document.createElement('a');
-    a.target = '_blank';
-    a.textContent = 'View PDF';
-    this.node.appendChild(a);
-    this.addClass(PDF_CLASS);
+    this.addClass('jp-RenderedText');
   }
 
   /**
    * Render a mime model.
-   */
-  renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let source = Private.getSource(model, this.mimeType);
-    let a = this.node.firstChild as HTMLAnchorElement;
-    a.href = `data:application/pdf;base64,${source}`;
-    return Promise.resolve(void 0);
-  }
-}
-
-
-/**
- * The namespace for module private data.
- */
-namespace Private {
-  /**
-   * Extract the source text from render options.
-   */
-  export
-  function getSource(model: IRenderMime.IMimeModel, mimeType: string): string {
-    return String(model.data[mimeType]);
-  }
-
-  /**
-   * Set trusted html to a node.
-   */
-  export
-  function setHtml(node: HTMLElement, html: string): void {
-    // Remove any existing child nodes.
-    while (node.firstChild) {
-      node.removeChild(node.firstChild);
-    }
-    try {
-      let range = document.createRange();
-      node.appendChild(range.createContextualFragment(html));
-    } catch (error) {
-      console.warn('Environment does not support Range ' +
-                   'createContextualFragment, falling back on innerHTML');
-      node.innerHTML = html;
-    }
-  }
-
-  /**
-   * Resolve the relative urls in element `src` and `href` attributes.
-   *
-   * @param node - The head html element.
-   *
-   * @param resolver - A url resolver.
    *
-   * @param linkHandler - An optional link handler for nodes.
+   * @param model - The mime model to render.
    *
-   * @returns a promise fulfilled when the relative urls have been resolved.
-   */
-  export
-  function handleUrls(node: HTMLElement, resolver: IRenderMime.IResolver, linkHandler?: IRenderMime.ILinkHandler): Promise<void> {
-    let promises: Promise<void>[] = [];
-    // Handle HTML Elements with src attributes.
-    let nodes = node.querySelectorAll('*[src]');
-    for (let i = 0; i < nodes.length; i++) {
-      promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
-    }
-    let anchors = node.getElementsByTagName('a');
-    for (let i = 0; i < anchors.length; i++) {
-      promises.push(handleAnchor(anchors[i], resolver, linkHandler || null));
-    }
-    let links = node.getElementsByTagName('link');
-    for (let i = 0; i < links.length; i++) {
-      promises.push(handleAttr(links[i], 'href', resolver));
-    }
-    return Promise.all(promises).then(() => { return void 0; });
-  }
-
-  /**
-   * Handle a node with a `src` or `href` attribute.
-   */
-  function handleAttr(node: HTMLElement, name: 'src' | 'href', resolver: IRenderMime.IResolver): Promise<void> {
-    let source = node.getAttribute(name);
-    if (!source) {
-      return Promise.resolve(void 0);
-    }
-    node.setAttribute(name, '');
-    return resolver.resolveUrl(source).then(path => {
-      return resolver.getDownloadUrl(path);
-    }).then(url => {
-      node.setAttribute(name, url);
-    });
-  }
-
-  /**
-   * Apply ids to headers.
-   */
-  export
-  function headerAnchors(node: HTMLElement): void {
-    let headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
-    for (let headerType of headerNames){
-      let headers = node.getElementsByTagName(headerType);
-      for (let i=0; i < headers.length; i++) {
-        let header = headers[i];
-        header.id = header.innerHTML.replace(/ /g, '-');
-        let anchor = document.createElement('a');
-        anchor.target = '_self';
-        anchor.textContent = '¶';
-        anchor.href = '#' + header.id;
-        anchor.classList.add('jp-InternalAnchorLink');
-        header.appendChild(anchor);
-      }
-    }
-  }
-
-  /**
-   * Handle an anchor node.
-   */
-  function handleAnchor(anchor: HTMLAnchorElement, resolver: IRenderMime.IResolver, linkHandler: IRenderMime.ILinkHandler | null): Promise<void> {
-    anchor.target = '_blank';
-    // Get the link path without the location prepended.
-    // (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
-    let href = anchor.getAttribute('href');
-    // Bail if it is not a file-like url.
-    if (!href || href.indexOf('://') !== -1 && href.indexOf('//') === 0) {
-      return Promise.resolve(void 0);
-    }
-    // Remove the hash until we can handle it.
-    let hash = anchor.hash;
-    if (hash) {
-      // Handle internal link in the file.
-      if (hash === href) {
-        anchor.target = '_self';
-        return Promise.resolve(void 0);
-      }
-      // For external links, remove the hash until we have hash handling.
-      href = href.replace(hash, '');
-    }
-    // Get the appropriate file path.
-    return resolver.resolveUrl(href).then(path => {
-      // Handle the click override.
-      if (linkHandler && URLExt.isLocal(path)) {
-        linkHandler.handleLink(anchor, path);
-      }
-      // Get the appropriate file download path.
-      return resolver.getDownloadUrl(path);
-    }).then(url => {
-      // Set the visible anchor.
-      anchor.href = url + hash;
-    });
-  }
-}
-
-/**
- * A namespace for private module data.
- */
-namespace Private {
-  let initialized = false;
-
-  /**
-   * Support GitHub flavored Markdown, leave sanitizing to external library.
+   * @returns A promise which resolves when rendering is complete.
    */
-  export
-  function initializeMarked(): void {
-    if (initialized) {
-      return;
-    }
-    initialized = true;
-    marked.setOptions({
-      gfm: true,
-      sanitize: false,
-      tables: true,
-      // breaks: true; We can't use GFM breaks as it causes problems with tables
-      langPrefix: `cm-s-${CodeMirrorEditor.defaultConfig.theme} language-`,
-      highlight: (code, lang, callback) => {
-        if (!lang) {
-            // no language, no highlight
-            if (callback) {
-                callback(null, code);
-                return;
-            } else {
-                return code;
-            }
-        }
-        Mode.ensure(lang).then(spec => {
-          let el = document.createElement('div');
-          if (!spec) {
-              console.log(`No CodeMirror mode: ${lang}`);
-              callback(null, code);
-              return;
-          }
-          try {
-            Mode.run(code, spec.mime, 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);
-        });
-      }
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return renderers.renderText({
+      host: this.node,
+      source: String(model.data[this.mimeType])
     });
   }
 }

+ 1 - 1
packages/rendermime/style/index.css

@@ -64,7 +64,7 @@
 .jp-RenderedText pre .ansi-bright-white-bg { background-color: #A1A6B2; }
 
 
-.jp-RenderedText.jp-mod-error {
+.jp-RenderedText[data-mime-type="application/vnd.jupyter.stderr"] {
   background: #fdd;
 }
 

+ 6 - 4
packages/tooltip/src/widget.ts

@@ -77,11 +77,13 @@ class Tooltip extends Widget {
     let model = new MimeModel({
       data: options.bundle
     });
-    this._content = this._rendermime.createRenderer(model);
-    if (this._content) {
-      this._content.renderModel(model);
-      layout.addWidget(this._content);
+    let mimeType = this._rendermime.preferredMimeType(options.bundle, false);
+    if (!mimeType) {
+      return;
     }
+    this._content = this._rendermime.createRenderer(mimeType);
+    this._content.renderModel(model);
+    layout.addWidget(this._content);
   }
 
   /**

+ 6 - 31
packages/vega/src/index.ts

@@ -117,39 +117,14 @@ class RenderedVega extends Widget implements IRenderMime.IRenderer {
 
 
 /**
- * A mime renderer factory for Vega/Vega-Lite data.
+ * A mime renderer factory for vega data.
  */
 export
-class VegaRendererFactory implements IRenderMime.IRendererFactory {
-  /**
-   * The mimeTypes this renderer accepts.
-   */
-  mimeTypes = [VEGA_MIME_TYPE, VEGALITE_MIME_TYPE];
-
-  /**
-   * Whether the renderer can create a renderer given the render options.
-   */
-  canCreateRenderer(options: IRenderMime.IRendererOptions): boolean {
-    return this.mimeTypes.indexOf(options.mimeType) !== -1;
-  }
-
-  /**
-   * Render the transformed mime bundle.
-   */
-  createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
-    return new RenderedVega(options);
-  }
-
-  /**
-   * Whether the renderer will sanitize the data given the render options.
-   */
-  wouldSanitize(options: IRenderMime.IRendererOptions): boolean {
-    return false;
-  }
-}
-
-
-const rendererFactory = new VegaRendererFactory();
+const rendererFactory: IRenderMime.IRendererFactory = {
+  safe: true,
+  mimeTypes: [VEGA_MIME_TYPE, VEGALITE_MIME_TYPE],
+  createRenderer: options => new RenderedVega(options)
+};
 
 const extensions: IRenderMime.IExtension | IRenderMime.IExtension[] = [
   // Vega

+ 88 - 181
test/src/rendermime/factories.spec.ts

@@ -16,8 +16,9 @@ import {
 } from '@jupyterlab/apputils';
 
 import {
-  LatexRendererFactory, PDFRendererFactory, JavaScriptRendererFactory,
-  SVGRendererFactory, MarkdownRendererFactory, TextRendererFactory, HTMLRendererFactory, ImageRendererFactory
+  latexRendererFactory, pdfRendererFactory, svgRendererFactory,
+  markdownRendererFactory, textRendererFactory, htmlRendererFactory,
+  imageRendererFactory
 } from '@jupyterlab/rendermime';
 
 import {
@@ -25,21 +26,6 @@ import {
 } from '@jupyterlab/rendermime';
 
 
-function runCanCreateRenderer(renderer: IRenderMime.IRendererFactory, trusted: boolean): boolean {
-  let canCreateRenderer = true;
-
-  for (let mimeType of renderer.mimeTypes) {
-    let options = { mimeType, sanitizer, trusted };
-    if (!renderer.canCreateRenderer(options)) {
-      canCreateRenderer = false;
-    }
-  }
-  let options = { trusted, mimeType: 'fizz/buzz', sanitizer };
-  expect(renderer.canCreateRenderer(options)).to.be(false);
-  return canCreateRenderer;
-}
-
-
 function createModel(mimeType: string, source: JSONValue, trusted = false): IRenderMime.IMimeModel {
   let data: JSONObject = {};
   data[mimeType] = source;
@@ -47,29 +33,31 @@ function createModel(mimeType: string, source: JSONValue, trusted = false): IRen
 }
 
 const sanitizer = defaultSanitizer;
+const defaultOptions: any = {
+  sanitizer,
+  linkHandler: null,
+  resolver: null
+};
 
 
 describe('rendermime/factories', () => {
 
-  describe('TextRendererFactory', () => {
+  describe('textRendererFactory', () => {
 
     describe('#mimeTypes', () => {
 
       it('should have text related mimeTypes', () => {
         let mimeTypes = ['text/plain', 'application/vnd.jupyter.stdout',
                'application/vnd.jupyter.stderr'];
-        let f = new TextRendererFactory();
-        expect(f.mimeTypes).to.eql(mimeTypes);
+        expect(textRendererFactory.mimeTypes).to.eql(mimeTypes);
       });
 
     });
 
-    describe('#canCreateRenderer()', () => {
+    describe('#safe', () => {
 
-      it('should be able to render trusted and untrusted text data', () => {
-        let f = new TextRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(true);
+      it('should be safe', () => {
+        expect(textRendererFactory.safe).to.be(true);
       });
 
     });
@@ -77,35 +65,32 @@ describe('rendermime/factories', () => {
     describe('#createRenderer()', () => {
 
       it('should output the correct HTML', () => {
-        let f = new TextRendererFactory();
+        let f = textRendererFactory;
         let mimeType = 'text/plain';
         let model = createModel(mimeType, 'x = 2 ** a');
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.innerHTML).to.be('<pre>x = 2 ** a</pre>');
         });
       });
 
       it('should output the correct HTML with ansi colors', () => {
-        let f = new TextRendererFactory();
+        let f = textRendererFactory;
         let source = 'There is no text but \x1b[01;41;32mtext\x1b[00m.\nWoo.';
         let mimeType = 'application/vnd.jupyter.console-text';
         let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.innerHTML).to.be('<pre>There is no text but <span class="ansi-bright-green-fg ansi-red-bg">text</span>.\nWoo.</pre>');
         });
       });
 
       it('should escape inline html', () => {
-        let f = new TextRendererFactory();
+        let f = textRendererFactory;
         let source = 'There is no text <script>window.x=1</script> but \x1b[01;41;32mtext\x1b[00m.\nWoo.';
         let mimeType = 'application/vnd.jupyter.console-text';
         let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.innerHTML).to.be('<pre>There is no text &lt;script&gt;window.x=1&lt;/script&gt; but <span class="ansi-bright-green-fg ansi-red-bg">text</span>.\nWoo.</pre>');
         });
@@ -115,23 +100,20 @@ describe('rendermime/factories', () => {
 
   });
 
-  describe('LatexRendererFactory', () => {
+  describe('latexRendererFactory', () => {
 
     describe('#mimeTypes', () => {
 
       it('should have the text/latex mimeType', () => {
-        let t = new LatexRendererFactory();
-        expect(t.mimeTypes).to.eql(['text/latex']);
+        expect(latexRendererFactory.mimeTypes).to.eql(['text/latex']);
       });
 
     });
 
-    describe('#canCreateRenderer()', () => {
+    describe('#safe', () => {
 
-      it('should be able to render trusted and untrusted latex data', () => {
-        let f = new LatexRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(true);
+      it('should be safe', () => {
+        expect(latexRendererFactory.safe).to.be(true);
       });
 
     });
@@ -140,11 +122,10 @@ describe('rendermime/factories', () => {
 
       it('should set the textContent of the widget', () => {
         let source = '\sum\limits_{i=0}^{\infty} \frac{1}{n^2}';
-        let f = new LatexRendererFactory();
+        let f = latexRendererFactory;
         let mimeType = 'text/latex';
         let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.textContent).to.be(source);
         });
@@ -154,23 +135,20 @@ describe('rendermime/factories', () => {
 
   });
 
-  describe('PDFRendererFactory', () => {
+  describe('pdfRendererFactory', () => {
 
     describe('#mimeTypes', () => {
 
       it('should have the application/pdf mimeType', () => {
-        let f = new PDFRendererFactory();
-        expect(f.mimeTypes).to.eql(['application/pdf']);
+        expect(pdfRendererFactory.mimeTypes).to.eql(['application/pdf']);
       });
 
     });
 
-    describe('#canCreateRenderer()', () => {
+    describe('#safe', () => {
 
-      it('should be able to render trusted pdf data', () => {
-        let f = new PDFRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(false);
+      it('should be unsafe', () => {
+        expect(pdfRendererFactory.safe).to.be(false);
       });
 
     });
@@ -179,11 +157,10 @@ describe('rendermime/factories', () => {
 
       it('should render the correct HTML', () => {
         let source = 'test';
-        let f = new PDFRendererFactory();
+        let f = pdfRendererFactory;
         let mimeType = 'application/pdf';
-        let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let model = createModel(mimeType, source, true);
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           let node = w.node.firstChild as HTMLAnchorElement;
           expect(node.localName).to.be('a');
@@ -196,73 +173,20 @@ describe('rendermime/factories', () => {
 
   });
 
-  describe('JavaScriptRendererFactory', () => {
-
-    describe('#mimeTypes', () => {
-
-      it('should have the text/javascript mimeType', () => {
-        let mimeTypes = ['text/javascript', 'application/javascript'];
-        let f = new JavaScriptRendererFactory();
-        expect(f.mimeTypes).to.eql(mimeTypes);
-      });
-
-    });
-
-    describe('#canCreateRenderer()', () => {
-
-      it('should be able to render trusted JavaScript data', () => {
-        let f = new JavaScriptRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(false);
-      });
-
-    });
-
-    describe('#createRenderer()', () => {
-
-      it('should create a script tag', () => {
-        let f = new JavaScriptRendererFactory();
-        let source = 'window.x = 1';
-        let mimeType = 'text/javascript';
-        let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
-        return w.renderModel(model).then(() => {
-          let el = w.node.firstChild as HTMLElement;
-          expect(el.localName).to.be('script');
-          expect(el.textContent).to.be(source);
-
-          // Ensure script has not been run yet
-          expect((window as any).x).to.be(void 0);
-          // Put it on the DOM
-          Widget.attach(w, document.body);
-          // Should be evaluated now
-          expect((window as any).x).to.be(1);
-          w.dispose();
-        });
-      });
-
-    });
-
-  });
-
-  describe('SVGRendererFactory', () => {
+  describe('svgRendererFactory', () => {
 
     describe('#mimeTypes', () => {
 
       it('should have the image/svg+xml mimeType', () => {
-        let f = new SVGRendererFactory();
-        expect(f.mimeTypes).to.eql(['image/svg+xml']);
+        expect(svgRendererFactory.mimeTypes).to.eql(['image/svg+xml']);
       });
 
     });
 
-    describe('#canCreateRenderer()', () => {
+    describe('#safe', () => {
 
-      it('should be able to render trusted SVG data', () => {
-        let f = new SVGRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(false);
+      it('should not be safe', () => {
+        expect(svgRendererFactory.safe).to.be(false);
       });
 
     });
@@ -271,11 +195,10 @@ describe('rendermime/factories', () => {
 
       it('should create an svg tag', () => {
         const source = '<svg></svg>';
-        let f = new SVGRendererFactory();
+        let f = svgRendererFactory;
         let mimeType = 'image/svg+xml';
-        let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let model = createModel(mimeType, source, true);
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           let svgEl = w.node.getElementsByTagName('svg')[0];
           expect(svgEl).to.be.ok();
@@ -286,23 +209,20 @@ describe('rendermime/factories', () => {
 
   });
 
-  describe('MarkdownRendererFactory', () => {
+  describe('markdownRendererFactory', () => {
 
     describe('#mimeTypes', () => {
 
       it('should have the text/markdown mimeType', function() {
-        let f = new MarkdownRendererFactory();
-        expect(f.mimeTypes).to.eql(['text/markdown']);
+        expect(markdownRendererFactory.mimeTypes).to.eql(['text/markdown']);
       });
 
     });
 
-    describe('#canCreateRenderer()', () => {
+    describe('#safe', () => {
 
-      it('should be able to render trusted and untrusted markdown data', () => {
-        let f = new MarkdownRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(true);
+      it('should be safe', () => {
+        expect(markdownRendererFactory.safe).to.be(true);
       });
 
     });
@@ -310,12 +230,11 @@ describe('rendermime/factories', () => {
     describe('#createRenderer()', () => {
 
       it('should set the inner html', () => {
-        let f = new MarkdownRendererFactory();
+        let f = markdownRendererFactory;
         let source = '<p>hello</p>';
         let mimeType = 'text/markdown';
         let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.innerHTML).to.be(source);
         });
@@ -323,11 +242,10 @@ describe('rendermime/factories', () => {
 
       it('should add header anchors', () => {
         let source = require('../../../examples/filebrowser/sample.md') as string;
-        let f = new MarkdownRendererFactory();
+        let f = markdownRendererFactory;
         let mimeType = 'text/markdown';
         let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           Widget.attach(w, document.body);
           let node = document.getElementById('Title-third-level');
@@ -342,12 +260,11 @@ describe('rendermime/factories', () => {
       });
 
       it('should sanitize the html', () => {
-        let f = new MarkdownRendererFactory();
+        let f = markdownRendererFactory;
         let source = '<p>hello</p><script>alert("foo")</script>';
         let mimeType = 'text/markdown';
         let model = createModel(mimeType, source);
-        let trusted = false;
-        let w = f.createRenderer({ mimeType, trusted, sanitizer });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.innerHTML).to.not.contain('script');
         });
@@ -357,23 +274,20 @@ describe('rendermime/factories', () => {
 
   });
 
-  describe('HTMLRendererFactory', () => {
+  describe('htmlRendererFactory', () => {
 
     describe('#mimeTypes', () => {
 
       it('should have the text/html mimeType', () => {
-        let f = new HTMLRendererFactory();
-        expect(f.mimeTypes).to.eql(['text/html']);
+        expect(htmlRendererFactory.mimeTypes).to.eql(['text/html']);
       });
 
     });
 
-    describe('#canCreateRenderer()', () => {
+    describe('#safe', () => {
 
-      it('should be able to render trusted and untrusted html data', () => {
-        let f = new HTMLRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(true);
+      it('should be safe', () => {
+        expect(htmlRendererFactory.safe).to.be(true);
       });
 
     });
@@ -381,39 +295,37 @@ describe('rendermime/factories', () => {
     describe('#createRenderer()', () => {
 
       it('should set the inner HTML', () => {
-        let f = new HTMLRendererFactory();
+        let f = htmlRendererFactory;
         const source = '<h1>This is great</h1>';
         let mimeType = 'text/html';
         let model = createModel(mimeType, source);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.innerHTML).to.be('<h1>This is great</h1>');
         });
       });
 
-      it('should execute a script tag when attached', () => {
-        const source = '<script>window.y=3;</script>';
-        let f = new HTMLRendererFactory();
-        let mimeType = 'text/html';
-        let model = createModel(mimeType, source, true);
-        let trusted = true;
-        let w = f.createRenderer({ mimeType, sanitizer, trusted });
-        return w.renderModel(model).then(() => {
-          expect((window as any).y).to.be(void 0);
-          Widget.attach(w, document.body);
-          expect((window as any).y).to.be(3);
-          w.dispose();
-        });
-      });
+      // TODO we are disabling script execution for now.
+      // it('should execute a script tag when attached', () => {
+      //   const source = '<script>window.y=3;</script>';
+      //   let f = htmlRendererFactory;
+      //   let mimeType = 'text/html';
+      //   let model = createModel(mimeType, source, true);
+      //   let w = f.createRenderer({ mimeType, ...defaultOptions });
+      //   return w.renderModel(model).then(() => {
+      //     expect((window as any).y).to.be(void 0);
+      //     Widget.attach(w, document.body);
+      //     expect((window as any).y).to.be(3);
+      //     w.dispose();
+      //   });
+      // });
 
       it('should sanitize when untrusted', () => {
         const source = '<pre><script>window.y=3;</script></pre>';
-        let f = new HTMLRendererFactory();
+        let f = htmlRendererFactory;
         let mimeType = 'text/html';
         let model = createModel(mimeType, source);
-        let trusted = false;
-        let w = f.createRenderer({ mimeType, trusted, sanitizer });
+        let w = f.createRenderer({ mimeType, ...defaultOptions });
         return w.renderModel(model).then(() => {
           expect(w.node.innerHTML).to.be('<pre></pre>');
         });
@@ -423,10 +335,9 @@ describe('rendermime/factories', () => {
 
     it('should sanitize html', () => {
       let model = createModel('text/html', '<h1>foo <script>window.x=1></scrip></h1>');
-      let f = new HTMLRendererFactory();
-      let trusted = false;
+      let f = htmlRendererFactory;
       let mimeType = 'text/html';
-      let w = f.createRenderer({ mimeType, sanitizer, trusted });
+      let w = f.createRenderer({ mimeType, ...defaultOptions });
       return w.renderModel(model).then(() => {
         expect(w.node.innerHTML).to.be('<h1>foo </h1>');
       });
@@ -434,23 +345,20 @@ describe('rendermime/factories', () => {
 
   });
 
-  describe('ImageRendererFactory', () => {
+  describe('imageRendererFactory', () => {
 
     describe('#mimeTypes', () => {
 
       it('should support multiple mimeTypes', () => {
-        let f = new ImageRendererFactory();
-        expect(f.mimeTypes).to.eql(['image/png', 'image/jpeg', 'image/gif']);
+        expect(imageRendererFactory.mimeTypes).to.eql(['image/png', 'image/jpeg', 'image/gif']);
       });
 
     });
 
-    describe('#canCreateRenderer()', () => {
+    describe('#safe', () => {
 
-      it('should be able to render trusted and untrusted image data', () => {
-        let f = new ImageRendererFactory();
-        expect(runCanCreateRenderer(f, true)).to.be(true);
-        expect(runCanCreateRenderer(f, false)).to.be(true);
+      it('should be safe', () => {
+        expect(imageRendererFactory.safe).to.be(true);
       });
 
     });
@@ -459,13 +367,13 @@ describe('rendermime/factories', () => {
 
       it('should create an <img> with the right mimeType', () => {
         let source = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
-        let f = new ImageRendererFactory();
+        let f = imageRendererFactory;
         let mimeType = 'image/png';
         let model = createModel(mimeType, source);
-        let w = f.createRenderer({ mimeType, sanitizer, trusted: true });
-        let el = w.node.firstChild as HTMLImageElement;
+        let w = f.createRenderer({ mimeType, ...defaultOptions  });
 
         return w.renderModel(model).then(() => {
+          let el = w.node.firstChild as HTMLImageElement;
           expect(el.src).to.be('data:image/png;base64,' + source);
           expect(el.localName).to.be('img');
           expect(el.innerHTML).to.be('');
@@ -473,11 +381,10 @@ describe('rendermime/factories', () => {
           source = 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=';
           mimeType = 'image/gif';
           model = createModel(mimeType, source);
-          let trusted = true;
-          w = f.createRenderer({ mimeType, sanitizer, trusted });
+          w = f.createRenderer({ mimeType, ...defaultOptions });
           return w.renderModel(model);
         }).then(() => {
-          el = w.node.firstChild as HTMLImageElement;
+          let el = w.node.firstChild as HTMLImageElement;
           expect(el.src).to.be('data:image/gif;base64,' + source);
           expect(el.localName).to.be('img');
           expect(el.innerHTML).to.be('');

+ 23 - 25
test/src/rendermime/rendermime.spec.ts

@@ -20,11 +20,7 @@ import {
 } from '@phosphor/widgets';
 
 import {
-  TextRendererFactory
-} from '@jupyterlab/rendermime';
-
-import {
-  MimeModel, IRenderMime, RenderMime
+  MimeModel, IRenderMime, RenderedText, RenderMime
 } from '@jupyterlab/rendermime';
 
 import {
@@ -39,6 +35,12 @@ function createModel(data: JSONObject): IRenderMime.IMimeModel {
   return new MimeModel({ data });
 }
 
+const fooFactory: IRenderMime.IRendererFactory  = {
+  mimeTypes: ['text/foo'],
+  safe: true,
+  createRenderer: options => new RenderedText(options)
+};
+
 
 describe('rendermime/index', () => {
 
@@ -84,21 +86,19 @@ describe('rendermime/index', () => {
     describe('#createRenderer()', () => {
 
       it('should create a mime renderer', () => {
-        let model = createModel({ 'text/plain': 'foo' });
-        let w = r.createRenderer(model);
+        let w = r.createRenderer('text/plain');
         expect(w instanceof Widget).to.be(true);
       });
 
       it('should raise an error for an unregistered mime type', () => {
-        let model = createModel({ 'text/fizz': 'foo' });
-        expect(() => { r.createRenderer(model); }).to.throwError();
+        expect(() => { r.createRenderer('text/fizz'); }).to.throwError();
       });
 
       it('should render json data', () => {
         let model = createModel({
           'application/json': { 'foo': 1 }
         });
-        let w = r.createRenderer(model);
+        let w = r.createRenderer('application/json');
         return w.renderModel(model).then(() => {
           expect(w.node.textContent).to.be('{\n  "foo": 1\n}');
         });
@@ -122,7 +122,7 @@ describe('rendermime/index', () => {
             }
           }
         });
-        let w = r.createRenderer(model);
+        let w = r.createRenderer('text/html');
         w.renderModel(model);
       });
 
@@ -139,7 +139,7 @@ describe('rendermime/index', () => {
             }
           }
         });
-        let w = r.createRenderer(model);
+        let w = r.createRenderer('text/html');
         w.renderModel(model);
       });
     });
@@ -151,12 +151,12 @@ describe('rendermime/index', () => {
           'text/plain': 'foo',
           'text/html': '<h1>foo</h1>'
         });
-        expect(r.preferredMimeType(model)).to.be('text/html');
+        expect(r.preferredMimeType(model.data, false)).to.be('text/html');
       });
 
       it('should return `undefined` if there are no registered mimeTypes', () => {
         let model = createModel({ 'text/fizz': 'buzz' });
-        expect(r.preferredMimeType(model)).to.be(void 0);
+        expect(r.preferredMimeType(model.data, false)).to.be(void 0);
       });
 
       it('should select the mimeType that is safe', () => {
@@ -165,7 +165,7 @@ describe('rendermime/index', () => {
           'text/javascript': 'window.x = 1',
           'image/png': 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
         });
-        expect(r.preferredMimeType(model)).to.be('image/png');
+        expect(r.preferredMimeType(model.data, true)).to.be('image/png');
       });
 
       it('should render the mimeType that is sanitizable', () => {
@@ -173,7 +173,7 @@ describe('rendermime/index', () => {
           'text/plain': 'foo',
           'text/html': '<h1>foo</h1>'
         });
-        expect(r.preferredMimeType(model)).to.be('text/html');
+        expect(r.preferredMimeType(model.data, true)).to.be('text/html');
       });
     });
 
@@ -182,8 +182,7 @@ describe('rendermime/index', () => {
       it('should clone the rendermime instance with shallow copies of data', () => {
         let c = r.clone();
         expect(toArray(c.mimeTypes)).to.eql(r.mimeTypes);
-        let factory = new TextRendererFactory();
-        c.addFactory(factory, 'text/foo');
+        c.addFactory(fooFactory);
         expect(r).to.not.be(c);
       });
 
@@ -192,16 +191,14 @@ describe('rendermime/index', () => {
     describe('#addFactory()', () => {
 
       it('should add a factory', () => {
-        let factory = new TextRendererFactory();
-        r.addFactory(factory, 'text/foo');
+        r.addFactory(fooFactory);
         let index = r.mimeTypes.indexOf('text/foo');
         expect(index).to.be(r.mimeTypes.length - 1);
       });
 
-      it('should take an optional order index', () => {
-        let factory = new TextRendererFactory();
+      it('should take an optional rank', () => {
         let len = r.mimeTypes.length;
-        r.addFactory(factory, 'text/foo', 0);
+        r.addFactory(fooFactory, 0);
         let index = r.mimeTypes.indexOf('text/foo');
         expect(index).to.be(0);
         expect(r.mimeTypes.length).to.be(len + 1);
@@ -214,7 +211,7 @@ describe('rendermime/index', () => {
       it('should remove a factory by mimeType', () => {
         r.removeFactory('text/html');
         let model = createModel({ 'text/html': '<h1>foo</h1>' });
-        expect(r.preferredMimeType(model)).to.be(void 0);
+        expect(r.preferredMimeType(model.data, true)).to.be(void 0);
       });
 
       it('should be a no-op if the mimeType is not registered', () => {
@@ -226,7 +223,8 @@ describe('rendermime/index', () => {
     describe('#getFactory()', () => {
 
       it('should get a factory by mimeType', () => {
-        expect(r.getFactory('text/plain')).to.be.a(TextRendererFactory);
+        let f = r.getFactory('text/plain');
+        expect(f.mimeTypes).to.contain('text/plain');
       });
 
       it('should return undefined for missing mimeType', () => {

+ 9 - 10
test/src/utils.ts

@@ -28,7 +28,7 @@ import {
 } from '@jupyterlab/notebook';
 
 import {
-  IRenderMime, RenderMime, TextRendererFactory, RenderedHTML
+  IRenderMime, RenderMime, RenderedHTML, defaultRendererFactories
 } from '@jupyterlab/rendermime';
 
 
@@ -163,20 +163,19 @@ namespace Private {
     }
   }
 
-
-  class JSONRendererFactory extends TextRendererFactory {
-
-    mimeTypes = ['application/json'];
-
+  const jsonRendererFactory = {
+    mimeTypes: ['application/json'],
+    safe: true,
     createRenderer(options: IRenderMime.IRendererOptions): IRenderMime.IRenderer {
       return new JSONRenderer(options);
     }
-  }
+  };
 
-  const initialFactories = RenderMime.getDefaultFactories();
   export
-  const rendermime = new RenderMime({ initialFactories });
-  rendermime.addFactory(new JSONRendererFactory(), 'application/json');
+  const rendermime = new RenderMime({
+    initialFactories: defaultRendererFactories
+  });
+  rendermime.addFactory(jsonRendererFactory, 10);
 }