@jupyterlab/mathjax-extension
Version:
A JupyterLab extension providing MathJax Typesetting
218 lines (185 loc) • 5.71 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
* @packageDocumentation
* @module mathjax-extension
*/
import { PromiseDelegate } from '@lumino/coreutils';
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { ILatexTypesetter } from '@jupyterlab/rendermime';
import type { MathDocument } from 'mathjax-full/js/core/MathDocument';
namespace CommandIDs {
/**
* Copy raw LaTeX to clipboard.
*/
export const copy = 'mathjax:clipboard';
/**
* Scale MathJax elements.
*/
export const scale = 'mathjax:scale';
}
namespace CommandArgs {
export type scale = {
scale: number;
};
}
/**
* The MathJax Typesetter.
*/
export class MathJaxTypesetter implements ILatexTypesetter {
protected async _ensureInitialized() {
if (!this._initialized) {
this._mathDocument = await Private.ensureMathDocument();
this._initialized = true;
}
}
/**
* Get an instance of the MathDocument object.
*/
async mathDocument(): Promise<MathDocument<any, any, any>> {
await this._ensureInitialized();
return this._mathDocument;
}
/**
* Typeset the math in a node.
*/
async typeset(node: HTMLElement): Promise<void> {
try {
await this._ensureInitialized();
} catch (e) {
console.error(e);
return;
}
this._mathDocument.options.elements = [node];
this._mathDocument.clear().render();
delete this._mathDocument.options.elements;
Private.hardenAnchorLinks(node);
}
protected _initialized: boolean = false;
protected _mathDocument: MathDocument<any, any, any>;
}
/**
* The MathJax extension.
*/
const mathJaxPlugin: JupyterFrontEndPlugin<ILatexTypesetter> = {
id: '@jupyterlab/mathjax-extension:plugin',
description: 'Provides the LaTeX mathematical expression interpreter.',
provides: ILatexTypesetter,
activate: (app: JupyterFrontEnd) => {
const typesetter = new MathJaxTypesetter();
app.commands.addCommand(CommandIDs.copy, {
execute: async () => {
const md = await typesetter.mathDocument();
const oJax: any = md.outputJax;
await navigator.clipboard.writeText(oJax.math.math);
},
label: 'MathJax Copy Latex'
});
app.commands.addCommand(CommandIDs.scale, {
execute: async (args: CommandArgs.scale) => {
const md = await typesetter.mathDocument();
const scale = args['scale'] || 1.0;
md.outputJax.options.scale = scale;
md.rerender();
// Harden only the re-rendered anchors
for (const math of md.math) {
const root = math.typesetRoot as HTMLElement | null;
if (root) {
Private.hardenAnchorLinks(root);
}
}
},
label: args =>
'Mathjax Scale ' + (args['scale'] ? `x${args['scale']}` : 'Reset')
});
return typesetter;
},
autoStart: true
};
export default mathJaxPlugin;
/**
* A namespace for module-private functionality.
*/
namespace Private {
let _loading: PromiseDelegate<MathDocument<any, any, any>> | null = null;
export async function ensureMathDocument(): Promise<
MathDocument<any, any, any>
> {
if (!_loading) {
_loading = new PromiseDelegate();
void import('mathjax-full/js/input/tex/require/RequireConfiguration');
const [
{ mathjax },
{ CHTML },
{ TeX },
{ TeXFont },
{ AllPackages },
{ SafeHandler },
{ HTMLHandler },
{ browserAdaptor },
{ AssistiveMmlHandler }
] = await Promise.all([
import('mathjax-full/js/mathjax'),
import('mathjax-full/js/output/chtml'),
import('mathjax-full/js/input/tex'),
import('mathjax-full/js/output/chtml/fonts/tex'),
import('mathjax-full/js/input/tex/AllPackages'),
import('mathjax-full/js/ui/safe/SafeHandler'),
import('mathjax-full/js/handlers/html/HTMLHandler'),
import('mathjax-full/js/adaptors/browserAdaptor'),
import('mathjax-full/js/a11y/assistive-mml')
]);
mathjax.handlers.register(
AssistiveMmlHandler(SafeHandler(new HTMLHandler(browserAdaptor())))
);
class EmptyFont extends TeXFont {
protected static defaultFonts = {} as any;
}
const chtml = new CHTML({
// Override dynamically generated fonts in favor of our font css
font: new EmptyFont()
});
const tex = new TeX({
packages: AllPackages.concat('require'),
inlineMath: [
['$', '$'],
['\\(', '\\)']
],
displayMath: [
['$$', '$$'],
['\\[', '\\]']
],
processEscapes: true,
processEnvironments: true
});
const mathDocument = mathjax.document(window.document, {
InputJax: tex,
OutputJax: chtml
});
_loading.resolve(mathDocument);
}
return _loading.promise;
}
/**
* Utility function to harden anchor links in a given element
*/
export function hardenAnchorLinks(element: HTMLElement): void {
const anchors = element.querySelectorAll<HTMLAnchorElement>('.MathJax a');
anchors.forEach(anchor => {
// Add rel="noopener" if not already present
const existingRel = anchor.rel || '';
const relValues = existingRel.split(/\s+/).filter(v => v.length > 0);
if (!relValues.includes('noopener')) {
relValues.push('noopener');
}
anchor.rel = relValues.join(' ');
// Add target="_blank" if not already present
if (anchor.target !== '_blank') {
anchor.target = '_blank';
}
});
}
}