Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add post render hook #60

Merged
merged 13 commits into from
Jun 22, 2022
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 = "goosey15@gmail.com" },
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