Browse Source

Merge pull request #2974 from ian-r-rose/pluggable_latex

Pluggable latex
Steven Silvester 7 years ago
parent
commit
cfae0a6d03

+ 1 - 1
jupyterlab/package.json

@@ -1,6 +1,7 @@
 {
 {
   "name": "@jupyterlab/application-top",
   "name": "@jupyterlab/application-top",
   "version": "0.10.2",
   "version": "0.10.2",
+  "private": true,
   "scripts": {
   "scripts": {
     "build": "node update-core.js && webpack",
     "build": "node update-core.js && webpack",
     "build:prod": "node update-core.js && webpack --devtool source-map",
     "build:prod": "node update-core.js && webpack --devtool source-map",
@@ -86,7 +87,6 @@
     "url-loader": "^0.5.7",
     "url-loader": "^0.5.7",
     "webpack": "^2.2.1"
     "webpack": "^2.2.1"
   },
   },
-  "private": true,
   "jupyterlab": {
   "jupyterlab": {
     "extensions": {
     "extensions": {
       "@jupyterlab/application-extension": "",
       "@jupyterlab/application-extension": "",

+ 2 - 2
packages/codemirror/src/codemirror-ipythongfm.ts

@@ -12,8 +12,8 @@ import 'codemirror/addon/mode/multiplex';
 /**
 /**
  * Define an IPython GFM (GitHub Flavored Markdown) mode.
  * Define an IPython GFM (GitHub Flavored Markdown) mode.
  *
  *
- * Is just a slightly altered GFM Mode with support for latex.
- * Latex support was supported by Codemirror GFM as of
+ * Is just a slightly altered GFM Mode with support for LaTeX.
+ * LaTeX support was supported by Codemirror GFM as of
  *   https://github.com/codemirror/CodeMirror/pull/567
  *   https://github.com/codemirror/CodeMirror/pull/567
  *  But was later removed in
  *  But was later removed in
  *   https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c
  *   https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c

+ 25 - 0
packages/rendermime-interfaces/src/index.ts

@@ -271,6 +271,11 @@ namespace IRenderMime {
      * An optional link handler.
      * An optional link handler.
      */
      */
     linkHandler: ILinkHandler | null;
     linkHandler: ILinkHandler | null;
+
+    /**
+     * The LaTeX typesetter.
+     */
+    latexTypesetter: ILatexTypesetter;
   }
   }
 
 
   /**
   /**
@@ -310,4 +315,24 @@ namespace IRenderMime {
      */
      */
     getDownloadUrl(path: string): Promise<string>;
     getDownloadUrl(path: string): Promise<string>;
   }
   }
+
+  /**
+   * The interface for a LaTeX typesetter.
+   */
+  export
+  interface ILatexTypesetter {
+    /**
+     * Typeset a DOM element.
+     *
+     * @param element - the DOM element to typeset. The typesetting may
+     *   happen synchronously or asynchronously.
+     *
+     * #### Notes
+     * The application-wide rendermime object has a settable
+     * `latexTypesetter` property which is used wherever LaTeX
+     * typesetting is required. Extensions wishing to provide their
+     * own typesetter may replace that on the global `lab.rendermime`.
+     */
+    typeset(element: HTMLElement): void;
+  }
 }
 }

+ 67 - 60
packages/rendermime/src/latex.ts

@@ -9,19 +9,83 @@
 // Other minor modifications are also due to StackExchange and are used with
 // Other minor modifications are also due to StackExchange and are used with
 // permission.
 // permission.
 
 
+import {
+  IRenderMime
+} from '@jupyterlab/rendermime-interfaces';
+
 const inline = '$'; // the inline math delimiter
 const inline = '$'; // the inline math delimiter
 
 
 // MATHSPLIT contains the pattern for math delimiters and special symbols
 // MATHSPLIT contains the pattern for math delimiters and special symbols
 // needed for searching for math in the text input.
 // needed for searching for math in the text input.
 const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)|\[|\]))/i;
 const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)|\[|\]))/i;
 
 
-// A module-level initialization flag.
-let initialized = false;
-
 
 
 // Stub for window MathJax.
 // Stub for window MathJax.
 declare var MathJax: any;
 declare var MathJax: any;
 
 
+/**
+ * The MathJax Typesetter.
+ */
+export
+class MathJaxTypesetter implements IRenderMime.ILatexTypesetter {
+  /**
+   * Typeset the math in a node.
+   *
+   * #### Notes
+   * MathJax schedules the typesetting asynchronously,
+   * but there are not currently any callbacks or Promises
+   * firing when it is done.
+   */
+  typeset(node: HTMLElement): void {
+    if (!this._initialized) {
+      this._init();
+    }
+    if ((window as any).MathJax) {
+      MathJax.Hub.Queue(
+        ['Typeset', MathJax.Hub, node,
+        ['resetEquationNumbers', MathJax.InputJax.TeX]]
+      );
+    }
+  }
+
+  /**
+   * Initialize MathJax.
+   */
+  private _init(): void {
+    if (!(window as any).MathJax) {
+      return;
+    }
+    MathJax.Hub.Config({
+      tex2jax: {
+        inlineMath: [ ['$', '$'], ['\\(', '\\)'] ],
+        displayMath: [ ['$$', '$$'], ['\\[', '\\]'] ],
+        processEscapes: true,
+        processEnvironments: true
+      },
+      // Center justify equations in code and markdown cells. Elsewhere
+      // we use CSS to left justify single line equations in code cells.
+      displayAlign: 'center',
+      CommonHTML: {
+         linebreaks: { automatic: true }
+       },
+      'HTML-CSS': {
+          availableFonts: [],
+          imageFont: null,
+          preferredFont: null,
+          webFont: 'STIX-Web',
+          styles: {'.MathJax_Display': {'margin': 0}},
+          linebreaks: { automatic: true }
+      },
+      skipStartupTypeset: true
+    });
+    MathJax.Hub.Configured();
+    this._initialized = true;
+  }
+
+  private _initialized = false;
+}
+
+
 /**
 /**
  *  Break up the text into its component parts and search
  *  Break up the text into its component parts and search
  *    through them for math delimiters, braces, linebreaks, etc.
  *    through them for math delimiters, braces, linebreaks, etc.
@@ -38,11 +102,6 @@ function removeMath(text: string): { text: string, math: string[] } {
   let braces: number = 0;
   let braces: number = 0;
   let deTilde: (text: string) => string;
   let deTilde: (text: string) => string;
 
 
-  if (!initialized) {
-    init();
-    initialized = true;
-  }
-
   // Except for extreme edge cases, this should catch precisely those pieces of the markdown
   // Except for extreme edge cases, this should catch precisely those pieces of the markdown
   // source that will later be turned into code spans. While MathJax will not TeXify code spans,
   // source that will later be turned into code spans. While MathJax will not TeXify code spans,
   // we still have to consider them at this point; the following issue has happened several times:
   // we still have to consider them at this point; the following issue has happened several times:
@@ -156,58 +215,6 @@ function replaceMath(text: string, math: string[]): string {
   return text.replace(/@@(\d+)@@/g, process);
   return text.replace(/@@(\d+)@@/g, process);
 };
 };
 
 
-
-/**
- * Typeset the math in a node.
- */
-export
-function typeset(node: HTMLElement): void {
-  if (!initialized) {
-    init();
-    initialized = true;
-  }
-  if ((window as any).MathJax) {
-    MathJax.Hub.Queue(
-      ['Typeset', MathJax.Hub, node,
-      ['resetEquationNumbers', MathJax.InputJax.TeX]]
-    );
-  }
-}
-
-
-/**
- * Initialize latex handling.
- */
-function init() {
-  if (!(window as any).MathJax) {
-    return;
-  }
-  MathJax.Hub.Config({
-    tex2jax: {
-      inlineMath: [ ['$', '$'], ['\\(', '\\)'] ],
-      displayMath: [ ['$$', '$$'], ['\\[', '\\]'] ],
-      processEscapes: true,
-      processEnvironments: true
-    },
-    // Center justify equations in code and markdown cells. Elsewhere
-    // we use CSS to left justify single line equations in code cells.
-    displayAlign: 'center',
-    CommonHTML: {
-       linebreaks: { automatic: true }
-     },
-    'HTML-CSS': {
-        availableFonts: [],
-        imageFont: null,
-        preferredFont: null,
-        webFont: 'STIX-Web',
-        styles: {'.MathJax_Display': {'margin': 0}},
-        linebreaks: { automatic: true }
-    },
-  });
-  MathJax.Hub.Configured();
-}
-
-
 /**
 /**
  * Process math blocks.
  * Process math blocks.
  *
  *

+ 36 - 9
packages/rendermime/src/renderers.ts

@@ -26,7 +26,7 @@ import {
 } from '@jupyterlab/rendermime-interfaces';
 } from '@jupyterlab/rendermime-interfaces';
 
 
 import {
 import {
-  typeset, removeMath, replaceMath
+  removeMath, replaceMath
 } from './latex';
 } from './latex';
 
 
 
 
@@ -41,7 +41,8 @@ export
 function renderHTML(options: renderHTML.IOptions): Promise<void> {
 function renderHTML(options: renderHTML.IOptions): Promise<void> {
   // Unpack the options.
   // Unpack the options.
   let {
   let {
-    host, source, trusted, sanitizer, resolver, linkHandler, shouldTypeset
+    host, source, trusted, sanitizer, resolver, linkHandler,
+    shouldTypeset, latexTypesetter
   } = options;
   } = options;
 
 
   // Bail early if the source is empty.
   // Bail early if the source is empty.
@@ -87,7 +88,9 @@ function renderHTML(options: renderHTML.IOptions): Promise<void> {
   }
   }
 
 
   // Return the final rendered promise.
   // Return the final rendered promise.
-  return promise.then(() => { if (shouldTypeset) { typeset(host); } });
+  return promise.then(() => {
+    if (shouldTypeset) { latexTypesetter.typeset(host); }
+  });
 }
 }
 
 
 
 
@@ -135,6 +138,11 @@ namespace renderHTML {
      * Whether the node should be typeset.
      * Whether the node should be typeset.
      */
      */
     shouldTypeset: boolean;
     shouldTypeset: boolean;
+
+    /**
+     * The LaTeX typesetter for the application.
+     */
+    latexTypesetter: IRenderMime.ILatexTypesetter;
   }
   }
 }
 }
 
 
@@ -233,14 +241,14 @@ namespace renderImage {
 export
 export
 function renderLatex(options: renderLatex.IRenderOptions): Promise<void> {
 function renderLatex(options: renderLatex.IRenderOptions): Promise<void> {
   // Unpack the options.
   // Unpack the options.
-  let { host, source, shouldTypeset } = options;
+  let { host, source, shouldTypeset, latexTypesetter } = options;
 
 
   // Set the source on the node.
   // Set the source on the node.
   host.textContent = source;
   host.textContent = source;
 
 
   // Typeset the node if needed.
   // Typeset the node if needed.
   if (shouldTypeset) {
   if (shouldTypeset) {
-    typeset(host);
+    latexTypesetter.typeset(host);
   }
   }
 
 
   // Return the rendered promise.
   // Return the rendered promise.
@@ -272,6 +280,11 @@ namespace renderLatex {
      * Whether the node should be typeset.
      * Whether the node should be typeset.
      */
      */
     shouldTypeset: boolean;
     shouldTypeset: boolean;
+
+    /**
+     * The LaTeX typesetter for the application.
+     */
+    latexTypesetter: IRenderMime.ILatexTypesetter;
   }
   }
 }
 }
 
 
@@ -287,7 +300,8 @@ export
 function renderMarkdown(options: renderMarkdown.IRenderOptions): Promise<void> {
 function renderMarkdown(options: renderMarkdown.IRenderOptions): Promise<void> {
   // Unpack the options.
   // Unpack the options.
   let {
   let {
-    host, source, trusted, sanitizer, resolver, linkHandler, shouldTypeset
+    host, source, trusted, sanitizer, resolver, linkHandler,
+    latexTypesetter, shouldTypeset
   } = options;
   } = options;
 
 
   // Clear the content if there is no source.
   // Clear the content if there is no source.
@@ -343,7 +357,7 @@ function renderMarkdown(options: renderMarkdown.IRenderOptions): Promise<void> {
 
 
     // Return the rendered promise.
     // Return the rendered promise.
     return promise;
     return promise;
-  }).then(() => { if (shouldTypeset) { typeset(host); } });
+  }).then(() => { if (shouldTypeset) { latexTypesetter.typeset(host); } });
 }
 }
 
 
 
 
@@ -391,6 +405,11 @@ namespace renderMarkdown {
      * Whether the node should be typeset.
      * Whether the node should be typeset.
      */
      */
     shouldTypeset: boolean;
     shouldTypeset: boolean;
+
+    /**
+     * The LaTeX typesetter for the application.
+     */
+    latexTypesetter: IRenderMime.ILatexTypesetter;
   }
   }
 }
 }
 
 
@@ -405,7 +424,8 @@ export
 function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
 function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
   // Unpack the options.
   // Unpack the options.
   let {
   let {
-    host, source, trusted, resolver, linkHandler, shouldTypeset, unconfined
+    host, source, trusted, resolver, linkHandler,
+    shouldTypeset, latexTypesetter, unconfined
   } = options;
   } = options;
 
 
   // Clear the content if there is no source.
   // Clear the content if there is no source.
@@ -439,7 +459,9 @@ function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
   }
   }
 
 
   // Return the final rendered promise.
   // Return the final rendered promise.
-  return promise.then(() => { if (shouldTypeset) { typeset(host); } });
+  return promise.then(() => {
+    if (shouldTypeset) { latexTypesetter.typeset(host); }
+  });
 }
 }
 
 
 
 
@@ -487,6 +509,11 @@ namespace renderSVG {
      * Whether the svg should be unconfined.
      * Whether the svg should be unconfined.
      */
      */
     unconfined?: boolean;
     unconfined?: boolean;
+
+    /**
+     * The LaTeX typesetter for the application.
+     */
+    latexTypesetter: IRenderMime.ILatexTypesetter;
   }
   }
 }
 }
 
 

+ 34 - 2
packages/rendermime/src/rendermime.ts

@@ -26,6 +26,10 @@ import {
   MimeModel
   MimeModel
 } from './mimemodel';
 } from './mimemodel';
 
 
+import {
+  MathJaxTypesetter
+} from './latex';
+
 
 
 /**
 /**
  * An object which manages mime renderer factories.
  * An object which manages mime renderer factories.
@@ -48,6 +52,7 @@ class RenderMime {
     // Parse the options.
     // Parse the options.
     this.resolver = options.resolver || null;
     this.resolver = options.resolver || null;
     this.linkHandler = options.linkHandler || null;
     this.linkHandler = options.linkHandler || null;
+    this._latexTypesetter = options.latexTypesetter || new MathJaxTypesetter();
     this.sanitizer = options.sanitizer || defaultSanitizer;
     this.sanitizer = options.sanitizer || defaultSanitizer;
 
 
     // Add the initial factories.
     // Add the initial factories.
@@ -73,6 +78,20 @@ class RenderMime {
    */
    */
   readonly linkHandler: IRenderMime.ILinkHandler | null;
   readonly linkHandler: IRenderMime.ILinkHandler | null;
 
 
+  /**
+   * The LaTeX typesetter for the rendermime.
+   *
+   * #### Notes
+   * This is settable so that extension authors may provide
+   * alternative implementations of the `IRenderMime.ILatexTypesetter`.
+   */
+  get latexTypesetter(): IRenderMime.ILatexTypesetter {
+    return this._latexTypesetter;
+  }
+  set latexTypesetter(typesetter: IRenderMime.ILatexTypesetter) {
+    this._latexTypesetter = typesetter;
+  }
+
   /**
   /**
    * The ordered list of mimeTypes.
    * The ordered list of mimeTypes.
    */
    */
@@ -131,7 +150,8 @@ class RenderMime {
       mimeType,
       mimeType,
       resolver: this.resolver,
       resolver: this.resolver,
       sanitizer: this.sanitizer,
       sanitizer: this.sanitizer,
-      linkHandler: this.linkHandler
+      linkHandler: this.linkHandler,
+      latexTypesetter: this.latexTypesetter
     });
     });
   }
   }
 
 
@@ -158,7 +178,8 @@ class RenderMime {
     let clone = new RenderMime({
     let clone = new RenderMime({
       resolver: options.resolver || this.resolver || undefined,
       resolver: options.resolver || this.resolver || undefined,
       sanitizer: options.sanitizer || this.sanitizer || undefined,
       sanitizer: options.sanitizer || this.sanitizer || undefined,
-      linkHandler: options.linkHandler || this.linkHandler || undefined
+      linkHandler: options.linkHandler || this.linkHandler || undefined,
+      latexTypesetter: options.latexTypesetter || this.latexTypesetter
     });
     });
 
 
     // Clone the internal state.
     // Clone the internal state.
@@ -216,6 +237,7 @@ class RenderMime {
   private _ranks: Private.RankMap = {};
   private _ranks: Private.RankMap = {};
   private _types: string[] | null = null;
   private _types: string[] | null = null;
   private _factories: Private.FactoryMap = {};
   private _factories: Private.FactoryMap = {};
+  private _latexTypesetter: IRenderMime.ILatexTypesetter;
 }
 }
 
 
 
 
@@ -252,6 +274,11 @@ namespace RenderMime {
      * An optional path handler.
      * An optional path handler.
      */
      */
     linkHandler?: IRenderMime.ILinkHandler;
     linkHandler?: IRenderMime.ILinkHandler;
+
+    /**
+     * An optional LaTeX typesetter.
+     */
+    latexTypesetter?: IRenderMime.ILatexTypesetter;
   }
   }
 
 
   /**
   /**
@@ -273,6 +300,11 @@ namespace RenderMime {
      * The new path handler.
      * The new path handler.
      */
      */
     linkHandler?: IRenderMime.ILinkHandler;
     linkHandler?: IRenderMime.ILinkHandler;
+
+    /**
+     * The new LaTeX typesetter.
+     */
+    latexTypesetter?: IRenderMime.ILatexTypesetter;
   }
   }
 
 
   /**
   /**

+ 19 - 13
packages/rendermime/src/widgets.ts

@@ -18,10 +18,6 @@ import {
   Widget
   Widget
 } from '@phosphor/widgets';
 } from '@phosphor/widgets';
 
 
-import {
-  typeset
-} from './latex';
-
 import * as renderers
 import * as renderers
   from './renderers';
   from './renderers';
 
 
@@ -42,6 +38,7 @@ abstract class RenderedCommon extends Widget implements IRenderMime.IRenderer {
     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.latexTypesetter = options.latexTypesetter;
     this.node.dataset['mimeType'] = this.mimeType;
     this.node.dataset['mimeType'] = this.mimeType;
   }
   }
 
 
@@ -65,6 +62,11 @@ abstract class RenderedCommon extends Widget implements IRenderMime.IRenderer {
    */
    */
   readonly linkHandler: IRenderMime.ILinkHandler | null;
   readonly linkHandler: IRenderMime.ILinkHandler | null;
 
 
+  /**
+   * The latexTypesetter.
+   */
+  readonly latexTypesetter: IRenderMime.ILatexTypesetter;
+
   /**
   /**
    * Render a mime model.
    * Render a mime model.
    *
    *
@@ -140,7 +142,8 @@ class RenderedHTML extends RenderedHTMLCommon {
       resolver: this.resolver,
       resolver: this.resolver,
       sanitizer: this.sanitizer,
       sanitizer: this.sanitizer,
       linkHandler: this.linkHandler,
       linkHandler: this.linkHandler,
-      shouldTypeset: this.isAttached
+      shouldTypeset: this.isAttached,
+      latexTypesetter: this.latexTypesetter
     });
     });
   }
   }
 
 
@@ -148,7 +151,7 @@ class RenderedHTML extends RenderedHTMLCommon {
    * A message handler invoked on an `'after-attach'` message.
    * A message handler invoked on an `'after-attach'` message.
    */
    */
   onAfterAttach(msg: Message): void {
   onAfterAttach(msg: Message): void {
-    typeset(this.node);
+    this.latexTypesetter.typeset(this.node);
   }
   }
 }
 }
 
 
@@ -159,7 +162,7 @@ class RenderedHTML extends RenderedHTMLCommon {
 export
 export
 class RenderedLatex extends RenderedCommon {
 class RenderedLatex extends RenderedCommon {
   /**
   /**
-   * Construct a new rendered Latex widget.
+   * Construct a new rendered LaTeX widget.
    *
    *
    * @param options - The options for initializing the widget.
    * @param options - The options for initializing the widget.
    */
    */
@@ -179,7 +182,8 @@ class RenderedLatex extends RenderedCommon {
     return renderers.renderLatex({
     return renderers.renderLatex({
       host: this.node,
       host: this.node,
       source: String(model.data[this.mimeType]),
       source: String(model.data[this.mimeType]),
-      shouldTypeset: this.isAttached
+      shouldTypeset: this.isAttached,
+      latexTypesetter: this.latexTypesetter
     });
     });
   }
   }
 
 
@@ -187,7 +191,7 @@ class RenderedLatex extends RenderedCommon {
    * A message handler invoked on an `'after-attach'` message.
    * A message handler invoked on an `'after-attach'` message.
    */
    */
   onAfterAttach(msg: Message): void {
   onAfterAttach(msg: Message): void {
-    typeset(this.node);
+    this.latexTypesetter.typeset(this.node);
   }
   }
 }
 }
 
 
@@ -258,7 +262,8 @@ class RenderedMarkdown extends RenderedHTMLCommon {
       resolver: this.resolver,
       resolver: this.resolver,
       sanitizer: this.sanitizer,
       sanitizer: this.sanitizer,
       linkHandler: this.linkHandler,
       linkHandler: this.linkHandler,
-      shouldTypeset: this.isAttached
+      shouldTypeset: this.isAttached,
+      latexTypesetter: this.latexTypesetter,
     });
     });
   }
   }
 
 
@@ -266,7 +271,7 @@ class RenderedMarkdown extends RenderedHTMLCommon {
    * A message handler invoked on an `'after-attach'` message.
    * A message handler invoked on an `'after-attach'` message.
    */
    */
   onAfterAttach(msg: Message): void {
   onAfterAttach(msg: Message): void {
-    typeset(this.node);
+    this.latexTypesetter.typeset(this.node);
   }
   }
 }
 }
 
 
@@ -302,7 +307,8 @@ class RenderedSVG extends RenderedCommon {
       resolver: this.resolver,
       resolver: this.resolver,
       linkHandler: this.linkHandler,
       linkHandler: this.linkHandler,
       shouldTypeset: this.isAttached,
       shouldTypeset: this.isAttached,
-      unconfined: metadata && metadata.unconfined as boolean | undefined
+      unconfined: metadata && metadata.unconfined as boolean | undefined,
+      latexTypesetter: this.latexTypesetter
     });
     });
   }
   }
 
 
@@ -310,7 +316,7 @@ class RenderedSVG extends RenderedCommon {
    * A message handler invoked on an `'after-attach'` message.
    * A message handler invoked on an `'after-attach'` message.
    */
    */
   onAfterAttach(msg: Message): void {
   onAfterAttach(msg: Message): void {
-    typeset(this.node);
+    this.latexTypesetter.typeset(this.node);
   }
   }
 }
 }
 
 

+ 18 - 4
test/src/rendermime/latex.spec.ts

@@ -4,7 +4,7 @@
 import expect = require('expect.js');
 import expect = require('expect.js');
 
 
 import {
 import {
-  removeMath, replaceMath, typeset
+  removeMath, replaceMath, MathJaxTypesetter
 } from '@jupyterlab/rendermime';
 } from '@jupyterlab/rendermime';
 
 
 
 
@@ -104,12 +104,26 @@ describe('jupyter-ui', () => {
 
 
   });
   });
 
 
-  describe('typeset()', () => {
+  describe('MathJaxTypesetter', () => {
+
+    describe('#constructor()', () => {
+      
+      it('should create a MathJaxTypesetter', () => {
+        let typesetter = new MathJaxTypesetter();
+        expect(typesetter).to.be.a(MathJaxTypesetter);
+      });
+
+    });
+
+    describe('#typeset()', () => {
+      it('should be a no-op if MathJax is not defined', () => {
+        let typesetter = new MathJaxTypesetter();
+        typesetter.typeset(document.body);
+      });
 
 
-    it('should be a no-op if MathJax is not defined', () => {
-      typeset(document.body);
     });
     });
 
 
   });
   });
 
 
 });
 });
+

+ 20 - 1
test/src/rendermime/rendermime.spec.ts

@@ -20,7 +20,7 @@ import {
 } from '@phosphor/widgets';
 } from '@phosphor/widgets';
 
 
 import {
 import {
-  MimeModel, IRenderMime, RenderedText, RenderMime
+  MimeModel, IRenderMime, RenderedText, RenderMime, MathJaxTypesetter
 } from '@jupyterlab/rendermime';
 } from '@jupyterlab/rendermime';
 
 
 import {
 import {
@@ -83,6 +83,25 @@ describe('rendermime/index', () => {
 
 
     });
     });
 
 
+    describe('#latexTypesetter', () => {
+
+      it('should be the MathJax typesetter by default', () => {
+        expect(r.latexTypesetter instanceof MathJaxTypesetter).to.be(true);
+      });
+
+      it('should be settable and clonable', () => {
+        let typesetter1 = new MathJaxTypesetter();
+        r.latexTypesetter = typesetter1;
+        expect(r.latexTypesetter).to.be(typesetter1);
+        let clone1 = r.clone();
+        expect(clone1.latexTypesetter).to.be(typesetter1);
+        let typesetter2 = new MathJaxTypesetter();
+        let clone2 = r.clone({ latexTypesetter: typesetter2 });
+        expect(clone2.latexTypesetter).to.be(typesetter2);
+      });
+
+    });
+
     describe('#createRenderer()', () => {
     describe('#createRenderer()', () => {
 
 
       it('should create a mime renderer', () => {
       it('should create a mime renderer', () => {