S. Chris Colbert 7 years ago
parent
commit
ebbe2864cc

+ 23 - 37
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 {
 import {
   ReadonlyJSONObject
   ReadonlyJSONObject
 } from '@phosphor/coreutils';
 } from '@phosphor/coreutils';
@@ -84,7 +85,7 @@ namespace IRenderMime {
      * with '.', like '.png', '.txt', etc.  They may themselves contain a
      * with '.', like '.png', '.txt', etc.  They may themselves contain a
      * period (e.g. .table.json).
      * period (e.g. .table.json).
      */
      */
-    readonly fileExtensions: string[];
+    readonly fileExtensions: ReadonlyArray<string>;
 
 
     /**
     /**
      * The name of the widget to display in dialogs.
      * The name of the widget to display in dialogs.
@@ -102,7 +103,7 @@ namespace IRenderMime {
      *
      *
      * **See also:** [[fileExtensions]].
      * **See also:** [[fileExtensions]].
      */
      */
-    readonly defaultFor?: string[];
+    readonly defaultFor?: ReadonlyArray<string>;
 
 
     /**
     /**
      * Whether the widget factory is read only.
      * 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.
      * 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.
      * A renderer factory to be registered to render the MIME type.
      */
      */
-    rendererFactory: IRendererFactory;
+    readonly rendererFactory: IRendererFactory;
 
 
     /**
     /**
      * The rank passed to `RenderMime.addFactory`.
      * The rank passed to `RenderMime.addFactory`.
      */
      */
-    rank?: number;
+    readonly rank?: number;
 
 
     /**
     /**
      * The timeout after user activity to re-render the data.
      * The timeout after user activity to re-render the data.
      */
      */
-    renderTimeout?: number;
+    readonly renderTimeout?: number;
 
 
     /**
     /**
      * Preferred data type from the model.  Defaults to `string`.
      * Preferred data type from the model.  Defaults to `string`.
      */
      */
-    dataType?: 'string' | 'json';
+    readonly dataType?: 'string' | 'json';
 
 
     /**
     /**
      * The icon class name for the widget.
      * The icon class name for the widget.
      */
      */
-    iconClass?: string;
+    readonly iconClass?: string;
 
 
     /**
     /**
      * The icon label for the widget.
      * The icon label for the widget.
      */
      */
-    iconLabel?: string;
+    readonly iconLabel?: string;
 
 
     /**
     /**
      * The options used to open a document with the renderer factory.
      * The options used to open a document with the renderer factory.
      */
      */
-    documentWidgetFactoryOptions?: IDocumentWidgetFactoryOptions;
+    readonly documentWidgetFactoryOptions?: IDocumentWidgetFactoryOptions;
   }
   }
 
 
   /**
   /**
@@ -180,7 +181,7 @@ namespace IRenderMime {
     /**
     /**
      * The default export.
      * The default export.
      */
      */
-    default: IExtension | IExtension[];
+    readonly default: IExtension | ReadonlyArray<IExtension>;
   }
   }
 
 
   /**
   /**
@@ -190,6 +191,10 @@ namespace IRenderMime {
   interface IRenderer extends Widget {
   interface IRenderer extends Widget {
     /**
     /**
      * Render a mime model.
      * Render a mime model.
+     *
+     * @param model - The mime model to render.
+     *
+     * @returns A promise which resolves when rendering is complete.
      */
      */
     renderModel(model: IMimeModel): Promise<void>;
     renderModel(model: IMimeModel): Promise<void>;
   }
   }
@@ -202,28 +207,14 @@ namespace IRenderMime {
     /**
     /**
      * The mimeTypes this renderer accepts.
      * The mimeTypes this renderer accepts.
      */
      */
-    readonly mimeTypes: string[];
-
-    /**
-     * Whether the renderer can render given the render options.
-     *
-     * @param options - The options that would be used to render the data.
-     */
-    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.
      * @param options - The options used to render the data.
      */
      */
     createRenderer(options: IRendererOptions): IRenderer;
     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 +227,6 @@ namespace IRenderMime {
      */
      */
     mimeType: string;
     mimeType: string;
 
 
-    /**
-     * Whether the data is trusted.
-     */
-    trusted: boolean;
-
     /**
     /**
      * The html sanitizer.
      * The html sanitizer.
      */
      */
@@ -249,12 +235,12 @@ namespace IRenderMime {
     /**
     /**
      * An optional url resolver.
      * An optional url resolver.
      */
      */
-    resolver?: IResolver;
+    resolver: IResolver | null;
 
 
     /**
     /**
      * An optional link handler.
      * An optional link handler.
      */
      */
-    linkHandler?: ILinkHandler;
+    linkHandler: ILinkHandler | null;
   }
   }
 
 
   /**
   /**

+ 60 - 255
packages/rendermime/src/factories.ts

@@ -1,301 +1,106 @@
-// 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 {
 import {
   IRenderMime
   IRenderMime
 } from '@jupyterlab/rendermime-interfaces';
 } from '@jupyterlab/rendermime-interfaces';
 
 
-import {
-  RenderedHTML, RenderedMarkdown, RenderedText, RenderedImage,
-  RenderedJavaScript, RenderedSVG, RenderedPDF, RenderedLatex
-} from '.';
+import * as Renderers
+  from './widgets';
 
 
 
 
 /**
 /**
- * A renderer for raw html.
+ * A mime renderer factory for raw html.
  */
  */
 export
 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 = {
+  mimeTypes: ['text/html'],
+  createRenderer: options => new Renderers.RenderedHTML(options)
+};
 
 
 
 
 /**
 /**
- * A renderer factory for `<img>` data.
+ * A mime renderer factory for images.
  */
  */
 export
 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 = {
+  mimeTypes: ['image/png', 'image/jpeg', 'image/gif'],
+  createRenderer: options => new Renderers.RenderedImage(options)
+};
 
 
 
 
 /**
 /**
- * A renderer factory for plain text and Jupyter console text data.
+ * A mime renderer factory for javascript.
  */
  */
 export
 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 javaScriptRendererFactory: IRenderMime.IRendererFactory = {
+  mimeTypes: ['text/javascript', 'application/javascript'],
+  createRenderer: options => new Renderers.RenderedJavaScript(options)
+};
 
 
 
 
 /**
 /**
- * A renderer factory for raw `<script>` data.
+ * A mime renderer factory for LaTeX.
  */
  */
 export
 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 latexRendererFactory: IRenderMime.IRendererFactory = {
+  mimeTypes: ['text/latex'],
+  createRenderer: options => new Renderers.RenderedLatex(options)
+};
 
 
 
 
 /**
 /**
- * A renderer factory for `<svg>` data.
+ * A mime renderer factory for Markdown.
  */
  */
 export
 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 markdownRendererFactory: IRenderMime.IRendererFactory = {
+  mimeTypes: ['text/markdown'],
+  createRenderer: options => new Renderers.RenderedMarkdown(options)
+};
 
 
 
 
 /**
 /**
- * A renderer factory for PDF data.
+ * A mime renderer factory for pdf.
  */
  */
 export
 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 pdfRendererFactory: IRenderMime.IRendererFactory = {
+  mimeTypes: ['application/pdf'],
+  createRenderer: options => new Renderers.RenderedPDF(options)
+};
 
 
 
 
 /**
 /**
- * A renderer factory for LateX data.
+ * A mime renderer factory for svg.
  */
  */
 export
 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 svgRendererFactory: IRenderMime.IRendererFactory = {
+  mimeTypes: ['image/svg+xml'],
+  createRenderer: options => new Renderers.RenderedSVG(options)
+};
 
 
 
 
 /**
 /**
- * A renderer factory for Jupyter Markdown data.
+ * A mime renderer factory for text and jupyter console text data.
  */
  */
 export
 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;
-  }
+const textRendererFactory: IRenderMime.IRendererFactory = {
+  mimeTypes: ['text/plain', 'application/vnd.jupyter.stdout', 'application/vnd.jupyter.stderr'],
+  createRenderer: options => new Renderers.RenderedText(options)
+};
 
 
-  /**
-   * 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;
-  }
-}
+/**
+ * The builtin factories provided by the package.
+ */
+export
+const defaultRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
+  htmlRendererFactory,
+  imageRendererFactory,
+  javaScriptRendererFactory,
+  latexRendererFactory,
+  markdownRendererFactory,
+  pdfRendererFactory,
+  svgRendererFactory,
+  textRendererFactory
+];

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

@@ -1,7 +1,8 @@
-// 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 '@jupyterlab/rendermime-interfaces';
 export * from './latex';
 export * from './latex';

+ 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
 // Some magic for deferring mathematical expressions to MathJax
 // by hiding them from the Markdown parser.
 // by hiding them from the Markdown parser.
 // Some of the code here is adapted with permission from Davide Cervone
 // 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 {
 import {
   ReadonlyJSONObject
   ReadonlyJSONObject
 } from '@phosphor/coreutils';
 } 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 {
 import {
   JSONExt, JSONObject, JSONValue, ReadonlyJSONObject
   JSONExt, JSONObject, JSONValue, ReadonlyJSONObject
 } from '@phosphor/coreutils';
 } from '@phosphor/coreutils';

+ 714 - 0
packages/rendermime/src/renderhelpers.ts

@@ -0,0 +1,714 @@
+/*-----------------------------------------------------------------------------
+| 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 {
+  ReadonlyJSONObject
+} from '@phosphor/coreutils';
+
+import {
+  URLExt
+} from '@jupyterlab/coreutils';
+
+import {
+  IRenderMime
+} from '@jupyterlab/rendermime-interfaces';
+
+import {
+  typeset, removeMath, replaceMath
+} from './latex';
+
+
+/**
+ * A collection of helper functions which render common formats.
+ */
+export
+namespace RenderHelpers {
+
+
+  /**
+   * 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 {
+      node, source, trusted, sanitizer, resolver, linkHandler, shouldTypeset
+    } = options;
+
+    // Clear the content if there is no source.
+    if (!source) {
+      node.textContent = '';
+      return Promise.resolve(undefined);
+    }
+
+    // Initialize the marked library if necessary.
+    Private.initializeMarked();
+
+    // 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 to the parsed content.
+      node.innerHTML = content;
+
+      // Apply ids to the header nodes.
+      Private.headerAnchors(node);
+
+      // TODO - this was in the old code, but why?
+      // <node owner widget>.fit();
+
+      // Patch the urls if a resolver is available.
+      let promise: Promise<void>;
+      if (resolver) {
+        promise = Private.handleUrls(node, resolver, linkHandler);
+      } else {
+        promise = Promise.resolve(undefined);
+      }
+
+      // Return the rendered promise.
+      return promise;
+    }).then(() => { if (shouldTypeset) { typeset(node) } });
+  }
+
+  /**
+   * The namespace for the `renderMarkdown` function statics.
+   */
+  export
+  namespace renderMarkdown {
+    /**
+     * The options for the `renderMarkdown` function.
+     */
+    export
+    interface IRenderOptions {
+      /**
+       * The node to use as the host of the rendered Markdown.
+       */
+      node: HTMLElement;
+
+      /**
+       * The Markdown source to render.
+       */
+      source: string;
+
+      /**
+       * Whether the source is trusted.
+       */
+      trusted: boolean;
+
+      /**
+       * The html sanitizer.
+       */
+      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 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 { node, source, shouldTypeset } = options;
+
+    // Set the source on the node.
+    node.textContent = source;
+
+    // Typeset the node if needed.
+    if (shouldTypeset) {
+      typeset(node);
+    }
+
+    // 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 node to use as the host of the rendered LaTeX.
+       */
+      node: HTMLElement;
+
+      /**
+       * The LaTeX source to render.
+       */
+      source: string;
+
+      /**
+       * 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 { mimeType, model, node } = options;
+
+    // Get the source text from the model.
+    let source = String(model.data[mimeType]);
+
+    // Set the source of the image.
+    node.src = `data:${mimeType};base64,${source}`;
+
+    // Look up the metadata for the model.
+    let metadata = model.metadata[mimeType] as ReadonlyJSONObject;
+
+    // Set the size of the image if specified by the metadata.
+    if (metadata) {
+      let { height, width } = metadata;
+      if (typeof height === 'number') {
+        node.height = height;
+      }
+      if (typeof width === 'number') {
+        node.width = width;
+      }
+    }
+
+    // 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.
+       */
+      node: HTMLImageElement;
+
+      /**
+       * The mime type for the image data in the model.
+       */
+      mimeType: string;
+
+      /**
+       * The mime model which holds the data to render.
+       */
+      model: IRenderMime.IMimeModel;
+    }
+  }
+
+  /**
+   * 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 { mimeType, model, node } = options;
+
+    // Get the source text from the model.
+    let source = String(model.data[mimeType]);
+
+    // Escape the terminal codes for HTMl.
+    let data = escape_for_html(source);
+
+    // Set the inner HTML for the host node.
+    node.innerHTML = ansi_to_html(data, { use_classes: true });
+
+    // 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 node to use as the host for the text content.
+       */
+      node: HTMLElement;
+
+      /**
+       * The mime type for the text data in the model.
+       */
+      mimeType: string;
+
+      /**
+       * The mime model which holds the data to render.
+       */
+      model: IRenderMime.IMimeModel;
+    }
+  }
+
+  /**
+   * Render JavaScript into a host node.
+   *
+   * @params options - The options for rendering.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  export
+  function renderJavaScript(options: renderJavaScript.IRenderOptions): Promise<void> {
+    // Unpack the options.
+    let { mimeType, model, node } = options;
+
+    // Clear the content of the node.
+    node.textContent = '';
+
+    // Get the source text from the model.
+    let source = String(model.data[mimeType]);
+
+    // Bail early if there is no source.
+    if (!source) {
+      return Promise.resolve(undefined);
+    }
+
+    // Create the "script" node to hold the source.
+    let script: HTMLElement;
+    if (model.trusted) {
+      script = document.createElement('script');
+    } else {
+      script = document.createElement('pre');
+    }
+
+    // Set the source for the script.
+    script.textContent = source;
+
+    // Add the script to the host node.
+    node.appendChild(script);
+
+    // Return the rendered promise.
+    return Promise.resolve(undefined);
+  }
+
+  /**
+   * The namespace for the `renderJavaScript` function statics.
+   */
+  export
+  namespace renderJavaScript {
+    /**
+     * The options for the `renderJavaScript` function.
+     */
+    export
+    interface IRenderOptions {
+      /**
+       * The node to use as the host for the script content.
+       */
+      node: HTMLElement;
+
+      /**
+       * The mime type for the JavaScript source in the model.
+       */
+      mimeType: string;
+
+      /**
+       * The mime model which holds the JavaScript source.
+       */
+      model: IRenderMime.IMimeModel;
+    }
+  }
+
+  /**
+   * 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 {
+      mimeType, model, node, resolver, linkHandler, shouldTypeset
+    } = options;
+
+    // Get the source text from the model.
+    let source = String(model.data[mimeType]);
+
+    // Clear the content if there is no source.
+    if (!source) {
+      node.textContent = '';
+      return Promise.resolve(undefined);
+    }
+
+    // Create the "script" node to hold the source.
+    if (model.trusted) {
+      node.innerHTML = source;
+    } else {
+      node.textContent = 'Run the cell to display SVG.'
+    }
+
+    // Patch the urls if a resolver is available.
+    let promise: Promise<void>;
+    if (resolver) {
+      promise = Private.handleUrls(node, resolver, linkHandler);
+    } else {
+      promise = Promise.resolve(undefined);
+    }
+
+    // Return the final rendered promise.
+    return promise.then(() => { if (shouldTypeset) { typeset(node); } });
+  }
+
+  /**
+   * The namespace for the `renderSVG` function statics.
+   */
+  export
+  namespace renderSVG {
+    /**
+     * The options for the `renderSVG` function.
+     */
+    export
+    interface IRenderOptions {
+      /**
+       * The mimeType to render.
+       */
+      mimeType: string;
+
+      /**
+       * An optional url resolver.
+       */
+      resolver: IRenderMime.IResolver | null;
+
+      /**
+       * An optional link handler.
+       */
+      linkHandler: IRenderMime.ILinkHandler | null;
+
+      /**
+       * The node to use as the host of the rendered SVG.
+       */
+      node: HTMLElement;
+
+      /**
+       * The mime model which holds the data to render.
+       */
+      model: IRenderMime.IMimeModel;
+
+      /**
+       * 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 { mimeType, model, node } = options;
+
+    // Get the source text from the model.
+    let source = String(model.data[mimeType]);
+
+    // Clear the content if there is no source.
+    if (!source) {
+      node.textContent = '';
+      return Promise.resolve(undefined);
+    }
+
+    // Update the node with the display content.
+    if (model.trusted) {
+      let href = `data:application/pdf;base64,${source}`;
+      node.innerHTML = `<a target="_blank" href="${href}">View PDF</a>`;
+    } else {
+      node.textContent = 'Run the cell to display PDF.'
+    }
+
+    // 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 mimeType to render.
+       */
+      mimeType: string;
+
+      /**
+       * The node to use as the host of the rendered SVG.
+       */
+      node: HTMLElement;
+
+      /**
+       * The mime model which holds the data to render.
+       */
+      model: IRenderMime.IMimeModel;
+    }
+  }
+}
+
+
+/**
+ * The namespace for module implementation details.
+ */
+namespace Private {
+  /**
+   * 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);
+  }
+
+  /**
+   * 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);
+    });
+  }
+
+  /**
+   * 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(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.
+   */
+  export
+  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);
+        });
+      }
+    });
+  }
+
+  /**
+   * 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> {
+    return new Promise<string>((resolve, reject) => {
+      marked(content, (err: any, content: string) => {
+        if (err) {
+          reject(err);
+        } else {
+          resolve(content);
+        }
+      });
+    });
+  }
+}

+ 173 - 132
packages/rendermime/src/rendermime.ts

@@ -1,12 +1,13 @@
-// 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 {
 import {
   Contents, Session
   Contents, Session
 } from '@jupyterlab/services';
 } from '@jupyterlab/services';
 
 
 import {
 import {
-  ArrayExt, each, find
+  ArrayExt, find
 } from '@phosphor/algorithm';
 } from '@phosphor/algorithm';
 
 
 import {
 import {
@@ -21,201 +22,178 @@ import {
   IClientSession, ISanitizer, defaultSanitizer
   IClientSession, ISanitizer, defaultSanitizer
 } from '@jupyterlab/apputils';
 } from '@jupyterlab/apputils';
 
 
-import {
-  JavaScriptRendererFactory, HTMLRendererFactory, MarkdownRendererFactory,
-  LatexRendererFactory, SVGRendererFactory, ImageRendererFactory,
-  TextRendererFactory, PDFRendererFactory
-} from './factories';
-
 
 
 /**
 /**
- * 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.
  */
  */
 export
 export
 class RenderMime {
 class RenderMime {
   /**
   /**
-   * Construct a renderer.
+   * Construct a new rendermime.
+   *
+   * @param options - The options for initializing the instance.
    */
    */
   constructor(options: RenderMime.IOptions = {}) {
   constructor(options: RenderMime.IOptions = {}) {
-    this.sanitizer = options.sanitizer || defaultSanitizer;
+    // Parse the options.
     this.resolver = options.resolver || null;
     this.resolver = options.resolver || null;
     this.linkHandler = options.linkHandler || 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;
+
+    // Initialize the factory map.
+    if (options.initialFactories) {
+      for (let factory of options.initialFactories) {
+        Private.addToMap(this._factories, factory, 100);
       }
       }
     }
     }
   }
   }
 
 
   /**
   /**
-   * 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.
    * The ordered list of mimeTypes.
    */
    */
   get mimeTypes(): ReadonlyArray<string> {
   get mimeTypes(): ReadonlyArray<string> {
-    return this._mimeTypes;
+    return this._types || (this._types = Private.sortedTypes(this._factories));
   }
   }
 
 
   /**
   /**
-   * Create a renderer for a mime model.
+   * Create a renderer for a mime type.
    *
    *
-   * @param model - the mime model.
+   * @param mimeType - The mime type of interest.
    *
    *
-   * @param mimeType - the optional explicit mimeType to use.
+   * @returns A new renderer for the given mime type.
    *
    *
-   * #### Notes
-   * If no mimeType is given, the [preferredMimeType] is used.
+   * @throws An error if no factory exists for the mime type.
    */
    */
-  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');
+  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}'`);
     }
     }
+
+    // Create the renderer options for the factory.
     let options = {
     let options = {
       mimeType,
       mimeType,
-      trusted: model.trusted,
       resolver: this.resolver,
       resolver: this.resolver,
       sanitizer: this.sanitizer,
       sanitizer: this.sanitizer,
       linkHandler: this.linkHandler
       linkHandler: this.linkHandler
     };
     };
-    if (!factory.canCreateRenderer(options)) {
-      throw new Error('Cannot create renderer');
-    }
-    return factory.createRenderer(options);
+
+    // Invoke the best factory for the given mime type.
+    return this._factories[mimeType][0].factory.createRenderer(options);
   }
   }
 
 
   /**
   /**
-   * Find the preferred mimeType for a model.
+   * Find the preferred mime type for a collection of types.
    *
    *
-   * @param model - the mime model of interest.
+   * @param types - The mime types from which to choose.
    *
    *
-   * #### Notes
-   * The mimeTypes in the model are checked in preference order
-   * until a renderer returns `true` for `.canCreateRenderer`.
+   * @returns The preferred mime type from the available factories,
+   *   or `undefined` if the mime type cannot be rendered.
    */
    */
-  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;
-    });
+  preferredMimeType(types: string[]): string | undefined {
+    return find(this.mimeTypes, mt => types.indexOf(mt) !== -1);
   }
   }
 
 
   /**
   /**
-   * 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 {
   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,
       sanitizer: options.sanitizer || this.sanitizer,
-      linkHandler: options.linkHandler || this.linkHandler,
-      resolver: options.resolver || this.resolver
+      linkHandler: options.linkHandler || this.linkHandler
     });
     });
-    each(this._mimeTypes, mimeType => {
-      let rank = this._ranks[mimeType];
-      rendermime.addFactory(this._factories[mimeType], mimeType, rank);
-    });
-    return rendermime;
+
+    // Update the clone with a copy of the factory map.
+    clone._factories = Private.cloneMap(this._factories);
+
+    // Update the clone with a copy of the sorted types.
+    clone._types = this.mimeTypes.slice();
+
+    // Return the cloned object.
+    return clone;
   }
   }
 
 
   /**
   /**
-   * Add a renderer factory for a given mimeType.
-   *
-   * @param factory - The renderer factory.
-   *
-   * @param mimeType - The renderer mimeType.
+   * Get the renderer factories registered for a mime type.
    *
    *
-   * @param rank - The rank of the renderer. Defaults to 100.  Lower rank
-   *   indicates higher priority for rendering.
+   * @param mimeType - The mime type of interest.
    *
    *
-   * #### Notes
-   * The renderer will replace an existing renderer for the given
-   * mimeType.
+   * @returns A new array of the factories for the given mime type.
    */
    */
-  addFactory(factory: IRenderMime.IRendererFactory, mimeType: string, rank?: number): void {
-    this._addFactory(factory, mimeType, rank);
+  getFactories(mimeType: string): IRenderMime.IRendererFactory[] {
+    let pairs = this._factories[mimeType];
+    return pairs ? pairs.map(p => p.factory) : [];
   }
   }
 
 
   /**
   /**
-   * Remove a renderer factory by mimeType.
+   * Add a renderer factory to the rendermime.
    *
    *
-   * @param mimeType - The mimeType of the factory.
-   */
-  removeFactory(mimeType: string): void {
-    this._removeFactory(mimeType);
-  }
-
-  /**
-   * Get a renderer factory by mimeType.
+   * @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`.
    *
    *
-   * @param mimeType - The mimeType of the renderer.
    *
    *
-   * @returns The renderer for the given mimeType, or undefined if the mimeType is unknown.
+   * #### Notes
+   * The renderer will replace an existing renderer for the given
+   * mimeType.
    */
    */
-  getFactory(mimeType: string): IRenderMime.IRendererFactory | undefined {
-    return this._factories[mimeType];
+  addFactory(factory: IRenderMime.IRendererFactory, rank = 100): void {
+    Private.addToMap(this._factories, factory, rank);
+    this._types = null;
   }
   }
 
 
   /**
   /**
-   * Add a factory to the rendermime instance.
+   * Remove a specific renderer factory.
+   *
+   * @param factory - The renderer factory of interest.
    */
    */
-  private _addFactory(factory: IRenderMime.IRendererFactory, mimeType: string, rank = 100): void {
-    // Remove any existing factory.
-    if (mimeType in this._factories) {
-      this._removeFactory(mimeType);
-    }
-
-    // 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;
+  removeFactory(factory: IRenderMime.IRendererFactory): void {
+    Private.removeFromMap(this._factories, factory);
+    this._types = null;
   }
   }
 
 
   /**
   /**
-   * Remove a renderer factory by mimeType.
+   * Remove all renderer factories for a mime type.
    *
    *
-   * @param mimeType - The mimeType of the factory.
+   * @param mimeType - The mime type of interest.
    */
    */
-  private _removeFactory(mimeType: string): void {
+  removeFactories(mimeType: string): void {
     delete this._factories[mimeType];
     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 _types: string[] | null = null;
+  private _factories: Private.FactoryMap = {};
 }
 }
 
 
 
 
 /**
 /**
- * The namespace for RenderMime statics.
+ * The namespace for `RenderMime` class statics.
  */
  */
 export
 export
 namespace RenderMime {
 namespace RenderMime {
@@ -249,23 +227,6 @@ namespace RenderMime {
     linkHandler?: IRenderMime.ILinkHandler;
     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.
    * The options used to clone a rendermime instance.
    */
    */
@@ -341,3 +302,83 @@ namespace RenderMime {
     contents: Contents.IManager;
     contents: Contents.IManager;
   }
   }
 }
 }
+
+
+/**
+ * The namespace for the module implementation details.
+ */
+namespace Private {
+  /**
+   * A pair which holds a rank and renderer factory.
+   */
+  export
+  type FactoryPair = {
+    readonly rank: number;
+    readonly factory: IRenderMime.IRendererFactory;
+  };
+
+  /**
+   * A type alias for a mapping of mime type -> ordered factories.
+   */
+  export
+  type FactoryMap = { [key: string]: FactoryPair[] };
+
+  /**
+   * Add a factory to a factory map.
+   *
+   * This inserts the factory in each bucket according to rank.
+   */
+  export
+  function addToMap(map: FactoryMap, factory: IRenderMime.IRendererFactory, rank: number): void {
+    for (let key of factory.mimeTypes) {
+      let pairs = map[key] || (map[key] = []);
+      let i = ArrayExt.lowerBound(pairs, rank, rankCmp);
+      ArrayExt.insert(pairs, i, { rank, factory });
+    }
+  }
+
+  /**
+   * Remove a factory from a factory map.
+   *
+   * This removes all instances of the factory from the map.
+   *
+   * Empty buckets are also removed.
+   */
+  export
+  function removeFromMap(map: FactoryMap, factory: IRenderMime.IRendererFactory): void {
+    for (let key in map) {
+      let pairs = map[key];
+      ArrayExt.removeAllWhere(pairs, pair => pair.factory === factory);
+      if (pairs.length === 0) {
+        delete map[key];
+      }
+    }
+  }
+
+  /**
+   * Create a deep clone of a factory map.
+   */
+  export
+  function cloneMap(map: FactoryMap): FactoryMap {
+    let clone: FactoryMap = {};
+    for (let key in map) {
+      clone[key] = map[key].slice();
+    }
+    return clone;
+  }
+
+  /**
+   * Get the mime types in the map, ordered by rank.
+   */
+  export
+  function sortedTypes(map: FactoryMap): string[] {
+    return Object.keys(map).sort((a, b) => map[a][0].rank - map[b][0].rank);
+  }
+
+  /**
+   * A pair/rank comparison function.
+   */
+  function rankCmp(pair: FactoryPair, rank: number): number {
+    return pair.rank - rank;
+  }
+}

+ 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"/>
 /// <reference path="../typings/ansi_up/ansi_up.d.ts"/>

+ 277 - 410
packages/rendermime/src/widgets.ts

@@ -1,17 +1,7 @@
-// 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 {
-  Mode, CodeMirrorEditor
-} from '@jupyterlab/codemirror';
-
-import * as marked
-  from 'marked';
-
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 import {
 import {
   Message
   Message
 } from '@phosphor/messaging';
 } from '@phosphor/messaging';
@@ -20,86 +10,36 @@ import {
   Widget
   Widget
 } from '@phosphor/widgets';
 } from '@phosphor/widgets';
 
 
-import {
-  JSONObject
-} from '@phosphor/coreutils';
-
-import {
-  URLExt
-} from '@jupyterlab/coreutils';
-
 import {
 import {
   IRenderMime
   IRenderMime
 } from '@jupyterlab/rendermime-interfaces';
 } from '@jupyterlab/rendermime-interfaces';
 
 
 import {
 import {
-  typeset, removeMath, replaceMath
-} from '.';
-
+  typeset
+} from './latex';
 
 
-/*
- * 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';
+import {
+  RenderHelpers
+} from './renderhelpers';
 
 
-/*
- * 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
 export
 abstract class RenderedCommon extends Widget implements IRenderMime.IRenderer {
 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) {
   constructor(options: IRenderMime.IRendererOptions) {
     super();
     super();
     this.mimeType = options.mimeType;
     this.mimeType = options.mimeType;
     this.sanitizer = options.sanitizer;
     this.sanitizer = options.sanitizer;
     this.resolver = options.resolver;
     this.resolver = options.resolver;
     this.linkHandler = options.linkHandler;
     this.linkHandler = options.linkHandler;
+    this.node.dataset['mime-type'] = this.mimeType;
   }
   }
 
 
   /**
   /**
@@ -113,70 +53,92 @@ abstract class RenderedCommon extends Widget implements IRenderMime.IRenderer {
   readonly sanitizer: IRenderMime.ISanitizer;
   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.
    * 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> {
+    // TODO compare model against old model for early bail?
+
+    // Set the trusted flag on the node.
+    this.node.dataset['trusted'] = `${model.trusted}`;
+
+    // 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 renderModel(model: IRenderMime.IMimeModel): Promise<void>;
+  abstract render(model: IRenderMime.IMimeModel): Promise<void>;
 }
 }
 
 
 
 
-/*
- * A widget for displaying any widget whoes representation is rendered HTML.
- * */
+/**
+ * A common base class for HTML mime renderers.
+ */
 export
 export
 abstract class RenderedHTMLCommon extends RenderedCommon {
 abstract class RenderedHTMLCommon extends RenderedCommon {
-  /* Construct a new rendered HTML common widget.*/
+  /**
+   * Construct a new rendered HTML common widget.
+   *
+   * @param options - The options for initializing the widget.
+   */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     super(options);
-    this.addClass(HTML_COMMON_CLASS);
+    this.addClass('jp-RenderedHTMLCommon');
   }
   }
 }
 }
 
 
 
 
 /**
 /**
- * A widget for displaying HTML and rendering math.
+ * A mime renderer for displaying HTML and math.
  */
  */
 export
 export
 class RenderedHTML extends RenderedHTMLCommon {
 class RenderedHTML extends RenderedHTMLCommon {
   /**
   /**
-   * Construct a new html widget.
+   * Construct a new rendered HTML widget.
+   *
+   * @param options - The options for initializing the widget.
    */
    */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     super(options);
-    this.addClass(HTML_CLASS);
+    this.addClass('jp-RenderedHTML');
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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);
+   *
+   * @param model - The mime model to render.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return RenderedHTML.renderHTML({
+      node: this.node,
+      source: String(model.data[this.mimeType]),
+      trusted: model.trusted,
+      resolver: this.resolver,
+      sanitizer: this.sanitizer,
+      linkHandler: this.linkHandler,
+      shouldTypeset: this.isAttached
+    });
   }
   }
 
 
   /**
   /**
@@ -189,57 +151,124 @@ class RenderedHTML extends RenderedHTMLCommon {
 
 
 
 
 /**
 /**
- * A widget for displaying Markdown with embeded latex.
+ * The namespace for the `RenderedHTML` class statics.
+ */
+export
+namespace RenderedHTML {
+  /**
+   * The options for the `render` function.
+   */
+  export
+  interface IRenderOptions {
+    /**
+     * The node to use as the host of the rendered HTML.
+     */
+    node: HTMLElement;
+
+    /**
+     * The HTML source to render.
+     */
+    source: string;
+
+    /**
+     * Whether the source is trusted.
+     */
+    trusted: boolean;
+
+    /**
+     * The html sanitizer.
+     */
+    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 HTML into a host node.
+   *
+   * @params options - The options for rendering.
+   *
+   * @returns A promise which resolves when rendering is complete.
+   */
+  export
+  function render(options: renderHTML.IOptions): Promise<void> {
+    // Unpack the options.
+    let {
+      node, source, trusted, sanitizer, resolver, linkHandler, shouldTypeset
+    } = options;
+
+    // Clear the content if there is no source.
+    if (!source) {
+      node.textContent = '';
+      return Promise.resolve(undefined);
+    }
+
+    // Sanitize the source if it is not trusted.
+    if (!trusted) {
+      source = sanitizer.sanitize(source);
+    }
+
+    // Set the inner HTML to the source.
+    node.innerHTML = source;
+
+    // Patch the urls if a resolver is available.
+    let promise: Promise<void>;
+    if (resolver) {
+      promise = Private.handleUrls(node, resolver, linkHandler);
+    } else {
+      promise = Promise.resolve(undefined);
+    }
+
+    // Return the final rendered promise.
+    return promise.then(() => { if (shouldTypeset) { typeset(node); } });
+  }
+}
+
+
+/**
+ * A mime renderer for displaying Markdown with embeded latex.
  */
  */
 export
 export
 class RenderedMarkdown extends RenderedHTMLCommon {
 class RenderedMarkdown extends RenderedHTMLCommon {
   /**
   /**
-   * Construct a new markdown widget.
+   * Construct a new rendered markdown widget.
+   *
+   * @param options - The options for initializing the widget.
    */
    */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     super(options);
-    this.addClass(MARKDOWN_CLASS);
-
-    // Initialize the marked library if necessary.
-    Private.initializeMarked();
+    this.addClass('jp-RenderedMarkdown');
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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 RenderHelpers.renderMarkdown({
+      node: 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 +282,33 @@ class RenderedMarkdown extends RenderedHTMLCommon {
 
 
 
 
 /**
 /**
- * A widget for displaying LaTeX output.
+ * A mime renderer for displaying LaTeX output.
  */
  */
 export
 export
 class RenderedLatex extends RenderedCommon {
 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) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     super(options);
-    this.addClass(LATEX_CLASS);
+    this.addClass('jp-RenderedLatex');
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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 RenderHelpers.renderLatex({
+      node: this.node,
+      source: String(model.data[this.mimeType]),
+      shouldTypeset: this.isAttached
+    });
   }
   }
 
 
   /**
   /**
@@ -287,134 +321,134 @@ class RenderedLatex extends RenderedCommon {
 
 
 
 
 /**
 /**
- * A widget for displaying rendered images.
+ * A mime renderer for displaying images.
  */
  */
 export
 export
 class RenderedImage extends RenderedCommon {
 class RenderedImage extends RenderedCommon {
   /**
   /**
    * Construct a new rendered image widget.
    * Construct a new rendered image widget.
+   *
+   * @param options - The options for initializing the widget.
    */
    */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     super(options);
-    let img = document.createElement('img');
-    this.node.appendChild(img);
-    this.addClass(IMAGE_CLASS);
+    this.addClass('jp-RenderedImage');
+    this.node.appendChild(document.createElement('img'));
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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);
-    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);
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return RenderHelpers.renderImage({
+      model,
+      mimeType: this.mimeType,
+      node: this.node.firstChild as HTMLImageElement
+    });
   }
   }
 }
 }
 
 
 
 
 /**
 /**
- * A widget for displaying rendered text.
+ * A widget for displaying plain text and console text.
  */
  */
 export
 export
 class RenderedText extends RenderedCommon {
 class RenderedText extends RenderedCommon {
   /**
   /**
    * Construct a new rendered text widget.
    * Construct a new rendered text widget.
+   *
+   * @param options - The options for initializing the widget.
    */
    */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     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-RenderedText');
+    this.node.appendChild(document.createElement('pre'));
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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);
-    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);
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return RenderHelpers.renderText({
+      model,
+      mimeType: this.mimeType,
+      node: this.node.firstChild as HTMLElement
+    });
   }
   }
 }
 }
 
 
 
 
 /**
 /**
- * A widget for displaying rendered JavaScript.
+ * A widget for displaying/executing JavaScript.
  */
  */
 export
 export
 class RenderedJavaScript extends RenderedCommon {
 class RenderedJavaScript extends RenderedCommon {
   /**
   /**
-   * Construct a new rendered JavaScript widget.
+   * Construct a new rendered Javascript widget.
+   *
+   * @param options - The options for initializing the widget.
    */
    */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     super(options);
-    let s = document.createElement('script');
-    s.type = options.mimeType;
-    this.node.appendChild(s);
-    this.addClass(JAVASCRIPT_CLASS);
+    this.addClass('jp-RenderedJavaScript');
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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 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 RenderHelpers.renderJavaScript({
+      model,
+      node: this.node,
+      mimeType: this.mimeType
+    });
   }
   }
 }
 }
 
 
 
 
 /**
 /**
- * A widget for displaying rendered SVG content.
+ * A widget for displaying SVG content.
  */
  */
 export
 export
 class RenderedSVG extends RenderedCommon {
 class RenderedSVG extends RenderedCommon {
   /**
   /**
    * Construct a new rendered SVG widget.
    * Construct a new rendered SVG widget.
+   *
+   * @param options - The options for initializing the widget.
    */
    */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     super(options);
-    this.addClass(SVG_CLASS);
+    this.addClass('jp-RenderedSVG');
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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 RenderHelpers.renderSVG({
+      model,
+      node: this.node,
+      mimeType: this.mimeType,
+      resolver: this.resolver,
+      linkHandler: this.linkHandler,
+      shouldTypeset: this.isAttached
+    });
   }
   }
 
 
   /**
   /**
@@ -433,215 +467,48 @@ export
 class RenderedPDF extends RenderedCommon {
 class RenderedPDF extends RenderedCommon {
   /**
   /**
    * Construct a new rendered PDF widget.
    * Construct a new rendered PDF widget.
+   *
+   * @param options - The options for initializing the widget.
    */
    */
   constructor(options: IRenderMime.IRendererOptions) {
   constructor(options: IRenderMime.IRendererOptions) {
     super(options);
     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-RenderedPDF');
   }
   }
 
 
   /**
   /**
    * Render a mime model.
    * 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);
+  render(model: IRenderMime.IMimeModel): Promise<void> {
+    return RenderHelpers.renderPDF({
+      model,
+      node: this.node,
+      mimeType: this.mimeType
+    });
   }
   }
 }
 }
 
 
 
 
 /**
 /**
- * The namespace for module private data.
+ * The namespace for module implementation details.
  */
  */
 namespace Private {
 namespace Private {
   /**
   /**
-   * Extract the source text from render options.
+   * Test whether two mime models are equivalent.
    */
    */
   export
   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.
-   *
-   * @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);
-      }
+  function equalModels(a: IRenderMime.IMimeModel, b: IRenderMime.IMimeModel): boolean {
+    if (a === b) {
+      return true;
     }
     }
-  }
-
-  /**
-   * 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);
+    if (a.trusted !== b.trusted) {
+      return false;
     }
     }
-    // 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, '');
+    if (a.data !== b.data) {
+      return false;
     }
     }
-    // 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.
-   */
-  export
-  function initializeMarked(): void {
-    if (initialized) {
-      return;
+    if (a.metadata !== b.metadata) {
+      return false;
     }
     }
-    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);
-        });
-      }
-    });
+    return true;
   }
   }
 }
 }

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

@@ -64,7 +64,7 @@
 .jp-RenderedText pre .ansi-bright-white-bg { background-color: #A1A6B2; }
 .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;
   background: #fdd;
 }
 }