Skip to content

Commit

Permalink
Feat: add post render hook (#60)
Browse files Browse the repository at this point in the history
* Feat: don't use JupyterLab math rendering

We still need to ship our own math renderer to render `math_` tokens, so let's avoid using two different renderers in the same implementation

* WIP: initial commit

* Feat: add support for JupyterLab's ILatexTypesetter

We migrate the math parsing responsibility to the MarkdownIt parser.
Any element matching `.math` will be rendered using the typesetter.

We add `markdown-it-dollarmath` to perform math parsing of $-math

* Refactor: rename typesetter → typesetter-adaptor
Feat: remove unneeded math wrappers

* Chore: restore yarn lock

* Refactor: remove debug print

* Feat: add rank to plugins

* Feat: add rank to plugins

* Feat: add pre-parse hook

* Feat: don't return node form postRender

* Package: set alpha version of new release

* Docs: improve documentation

* Fix: set double_inline
  • Loading branch information
agoose77 authored Jun 22, 2022
1 parent ee64d6c commit 29a2e1d
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 67 deletions.
30 changes: 28 additions & 2 deletions index.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"| `markdown-it-diagrams` | [diagrams](#Diagrams) |\n",
"| `markdown-it-deflist` | [definition lists](#Definition-Lists) |\n",
"| `markdown-it-footnote` | [footnotes](#Footnotes) |\n",
"| `markdown-it-task-lists` | [task lists](#Task-Lists) |"
"| `markdown-it-task-lists` | [task lists](#Task-Lists) |\n",
"| `markdown-it-dollarmath` | [dollar math](#Dollar-Math) |"
]
},
{
Expand Down Expand Up @@ -175,6 +176,31 @@
"```\n",
"\"\"\"}, raw=True)"
]
},
{
"cell_type": "markdown",
"metadata": {
"tags": []
},
"source": [
"## Dollar Math\n",
"<div class=\"math\">\n",
" \n",
"This is some math: $x + y$. This is some _display_ math:\n",
" \n",
"$$\n",
"x + y = z\n",
"$$\n",
" \n",
"</div>"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand All @@ -193,7 +219,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.10"
"version": "3.10.5"
},
"toc-autonumbering": true,
"toc-showcode": false,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agoose77/jupyterlab-markup",
"version": "2.0.0",
"version": "2.1.0-alpha.0",
"description": "Additional markdown rendering support in JupyterLab.",
"keywords": [
"jupyter",
Expand Down Expand Up @@ -60,6 +60,7 @@
"markdown-it": "^12.2.3",
"markdown-it-anchor": "^8.6.4",
"markdown-it-deflist": "^2.0.3",
"markdown-it-dollarmath": "^0.4.2",
"markdown-it-footnote": "^3.0.2",
"markdown-it-task-lists": "^2.1.1",
"react": "^17.0.1"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ build-backend = "hatchling.build"
name = "jupyterlab-markup"
description = "Extensible markdown rendering support in markdown"
readme = "README.md"
license = "BSD-3-Clause"
license = { file="LICENSE" }
requires-python = ">=3.7"
authors = [
{ name = "Angus Hollands", email = "[email protected]" },
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# setup.py shim for use with versions of JupyterLab that require
# it for extensions.
__import__("setuptools").setup()
45 changes: 45 additions & 0 deletions src/builtins/dollarmath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { PACKAGE_NS, simpleMarkdownItPlugin } from '..';

interface IRenderOptions {
displayMode: boolean;
}

/**
* ADd support for math parsing
*/
export const dollarmath = simpleMarkdownItPlugin(PACKAGE_NS, {
id: 'markdown-it-dollarmath',
title: 'Dollar Math',
description: 'Parse inline and display LaTeX math using $-delimiters',
documentationUrls: {
Plugin: 'https://github.com/executablebooks/markdown-it-dollarmath'
},
plugin: async () => {
const dollarmathPlugin = await import(
/* webpackChunkName: "markdown-it-anchor" */ 'markdown-it-dollarmath'
);
return [
dollarmathPlugin.default,
{
allow_space: true,
allow_digits: true,
double_inline: true,
allow_labels: true,
labelNormalizer(label: string) {
return label.replace(/[\s]+/g, '-');
},
renderer(content: string, opts: IRenderOptions) {
const { displayMode } = opts;
if (displayMode) {
return `$$${content}$$`;
} else {
return `$${content}$`;
}
},
labelRenderer(label: string) {
return `<a href="#${label}" class="mathlabel" title="Permalink to this equation">¶<a>`;
}
}
];
}
});
13 changes: 12 additions & 1 deletion src/builtins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@ import { svgbob } from './svgbob';
import { mermaid } from './mermaid';
import { footnote } from './footnote';
import { taskLists } from './task-lists';
import { typesetterAdaptor } from './typesetter-adaptor';
import { dollarmath } from './dollarmath';

/**
* Builtin plugins provided by this labextension
*/
export const BUILTINS = [anchor, deflist, footnote, mermaid, svgbob, taskLists];
export const BUILTINS = [
anchor,
deflist,
footnote,
mermaid,
svgbob,
taskLists,
typesetterAdaptor,
dollarmath
];
40 changes: 40 additions & 0 deletions src/builtins/typesetter-adaptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IMarkdownIt, PACKAGE_NS } from '..';
import { ILatexTypesetter } from '@jupyterlab/rendermime';
import { JupyterFrontEndPlugin } from '@jupyterlab/application';

/**
* Adds anchors to headers
*/
const plugin_id = 'typesetter-adaptor';
export const typesetterAdaptor: JupyterFrontEndPlugin<void> = {
id: `${PACKAGE_NS}:${plugin_id}`,
autoStart: true,
requires: [IMarkdownIt, ILatexTypesetter],
activate: (app, markdownIt: IMarkdownIt, typesetter: ILatexTypesetter) => {
const provider: IMarkdownIt.IPluginProvider = {
id: plugin_id,
title: 'ILatexTypesetter Adaptor',
description:
'Enable math rendering using JupyterLab ILatexTypesetter interface',
documentationUrls: {},
plugin: async () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return [md => {}];
},
postRenderHook: async () => {
const math_selectors = ['.math'];
return {
async postRender(node: HTMLElement): Promise<void> {
// Find nodes to typeset
const nodes = [
...node.querySelectorAll(math_selectors.join(','))
] as HTMLElement[];
// Only typeset these nodes
await Promise.all(nodes.map(node => typesetter.typeset(node)));
}
};
}
};
markdownIt.addPluginProvider(provider);
}
};
70 changes: 59 additions & 11 deletions src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import MarkdownIt from 'markdown-it';
import { RenderedMarkdown } from './widgets';
import { IMarkdownIt } from './tokens';

/**
* Comparator of IRanked implementations
*/
function rankedComparator(default_rank: number) {
return (left: IMarkdownIt.IRanked, right: IMarkdownIt.IRanked) =>
(left.rank ?? default_rank) - (right.rank ?? default_rank);
}

/**
* An implementation of a source of markdown renderers with markdown-it and plugins
*/
Expand Down Expand Up @@ -114,27 +122,32 @@ export class MarkdownItManager implements IMarkdownIt {
return new RenderedMarkdown(options);
};

/**
* Get a MarkdownIt instance
*/
async getMarkdownIt(
async getRenderer(
widget: RenderedMarkdown,
options: MarkdownIt.Options = {}
): Promise<MarkdownIt> {
): Promise<IMarkdownIt.IRenderer> {
// Create MarkdownIt instance
const allOptions = {
...(await this.getOptions(widget)),
...options,
...this.userMarkdownItOptions
};

let md = new MarkdownIt('default', allOptions);

for (const [id, provider] of this._pluginProviders.entries()) {
if (this.userDisabledPlugins.indexOf(id) !== -1) {
// Sort providers by rank
const rankComparator = rankedComparator(100);
const pluginProviders = [...this._pluginProviders.values()];
pluginProviders.sort(rankComparator);

// Lifecycle hooks
const preParseHooks: IMarkdownIt.IPreParseHook[] = [];
const postRenderHooks: IMarkdownIt.IPostRenderHook[] = [];
for (const provider of pluginProviders) {
if (this.userDisabledPlugins.indexOf(provider.id) !== -1) {
continue;
}
try {
const userOptions = this.userPluginOptions[id] || [];
const userOptions = this.userPluginOptions[provider.id] || [];
const [plugin, ...pluginOptions] = await provider.plugin();
let i = 0;
const maxOptions = Math.max(pluginOptions.length, userOptions.length);
Expand All @@ -145,12 +158,47 @@ export class MarkdownItManager implements IMarkdownIt {
i++;
}
md = md.use(plugin, ...compositeOptions);

// Build table of lifecycle hooks
if (provider?.preParseHook !== undefined) {
preParseHooks.push(await provider.preParseHook());
}
if (provider?.postRenderHook !== undefined) {
postRenderHooks.push(await provider.postRenderHook());
}
} catch (err) {
console.warn(`Failed to load/use markdown-it plugin ${id}`, err);
console.warn(
`Failed to load/use markdown-it plugin ${provider.id}`,
err
);
}
}
// Sort hooks by rank
preParseHooks.sort(rankComparator);
postRenderHooks.sort(rankComparator);

return {
get markdownIt(): MarkdownIt {
return md;
},

render: content => md.render(content),

return md;
// Run hooks serially
preParse: async (content: string) => {
for (const hook of preParseHooks) {
content = await hook.preParse(content);
}
return content;
},

// Run hooks serially
postRender: async (node: HTMLElement) => {
for (const hook of postRenderHooks) {
await hook.postRender(node);
}
}
};
}

/**
Expand Down
22 changes: 8 additions & 14 deletions src/renderers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { removeMath, renderHTML, replaceMath } from '@jupyterlab/rendermime';
import { renderHTML } from '@jupyterlab/rendermime';
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
import { ISanitizer } from '@jupyterlab/apputils';
import MarkdownIt from 'markdown-it';
import { IMarkdownIt } from './tokens';

/**
* Render Markdown into a host node.
Expand All @@ -13,28 +13,22 @@ import MarkdownIt from 'markdown-it';
export async function renderMarkdown(
options: renderMarkdown.IRenderOptions
): Promise<void> {
const { host, source, md, ...others } = options;
const { host, source, renderer, ...others } = options;

// Clear the content if there is no source.
if (!source) {
host.textContent = '';
return;
}

// Separate math from normal markdown text.
const parts = removeMath(source);

let html = md.render(parts['text']);

// Replace math.
html = replaceMath(html, parts['math']);

// Render HTML.
await renderHTML({
host,
source: html,
...others
source: renderer.render(source),
...others,
shouldTypeset: false
});
await renderer.postRender(host);
}

/**
Expand Down Expand Up @@ -83,7 +77,7 @@ export namespace renderMarkdown {
/**
* MarkdownIt renderer
*/
md: MarkdownIt;
renderer: IMarkdownIt.IRenderer;

/**
* The LaTeX typesetter for the application.
Expand Down
Loading

0 comments on commit 29a2e1d

Please sign in to comment.