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

Update to marked 7.0.2, add mermaidjs 10.3.1 #14102

Merged
merged 73 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
b7c543a
Update marked, add mermaidjs
bollwyvl Feb 28, 2023
213eba2
style mermaid errors
bollwyvl Feb 28, 2023
390e9d7
Merge remote-tracking branch 'upstream/master' into add-mermaid-to-ma…
bollwyvl Feb 28, 2023
80b24e8
Automatic application of license header
github-actions[bot] Feb 28, 2023
a30efbc
rework error style, nesting
bollwyvl Mar 1, 2023
9f69978
add mermaidjs to markdown notebook ui tests
bollwyvl Mar 1, 2023
385bdd7
make theme manager optional
bollwyvl Mar 1, 2023
f6890dc
Merge branch 'master' into add-mermaid-to-marked
bollwyvl Mar 2, 2023
5f7eb26
Merge branch 'master' into add-mermaid-to-marked
bollwyvl Mar 2, 2023
2556223
merge upstream, bump mermaid
bollwyvl Mar 3, 2023
79e9bfd
merge upstream
bollwyvl Mar 14, 2023
08f4d6e
use suggested image size
bollwyvl Mar 14, 2023
696ab6e
update markdown screenshots
bollwyvl Mar 15, 2023
0e83b79
merge upstream
bollwyvl Mar 22, 2023
d7cca6a
integrity update
bollwyvl Mar 22, 2023
ce2d8a5
rework, expand parser errors
bollwyvl Mar 22, 2023
7594420
merge upstream
bollwyvl Jun 12, 2023
48f5935
relock, lint
bollwyvl Jun 12, 2023
f86ee27
add dedicated warning classes for style lint
bollwyvl Jun 13, 2023
d7bcf3c
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jun 13, 2023
9f56714
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jun 26, 2023
f64adbc
hoist accessibility features
bollwyvl Jun 26, 2023
bbef4b2
refactor mermaid into core, markdown plugins
bollwyvl Jun 27, 2023
2398eac
tighten up regexen
bollwyvl Jun 28, 2023
193a003
add mime renderer
bollwyvl Jun 30, 2023
7e35112
integrity
bollwyvl Jul 1, 2023
45a327f
reset metapackage to main, re-run integrity
bollwyvl Jul 1, 2023
511b61b
yet more integrity
bollwyvl Jul 1, 2023
24aef8e
mermaid 10.2.4
bollwyvl Jul 1, 2023
1138f5b
use DOM api instead of regexen for width, title, desc
bollwyvl Jul 1, 2023
493286d
add marked plugins to avoid deprecation warnings
bollwyvl Jul 2, 2023
3208afb
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jul 3, 2023
88aecf9
add marked plugins to unused known 'unused' deps
bollwyvl Jul 3, 2023
82fd815
Merge branch 'main' into add-mermaid-to-marked
bollwyvl Jul 4, 2023
10aa3c9
Merge branch 'main' into add-mermaid-to-marked
bollwyvl Jul 11, 2023
094b6a7
start context commands
bollwyvl Jul 13, 2023
23afa70
merge upstream
bollwyvl Jul 13, 2023
277e618
fix context menu commands
bollwyvl Jul 13, 2023
03fd965
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jul 17, 2023
a9e8747
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jul 18, 2023
e0b5113
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jul 19, 2023
5b41806
split languages and custom options
bollwyvl Jul 19, 2023
3e08c39
Merge branch 'main' into add-mermaid-to-marked
bollwyvl Jul 19, 2023
c56ff8b
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jul 20, 2023
1f03b23
add test for embedded svg
bollwyvl Jul 20, 2023
a2a3802
try to fix codeql
bollwyvl Jul 20, 2023
6608008
add snapshot images from CI
bollwyvl Jul 21, 2023
2df6128
Update Playwright Snapshots
github-actions[bot] Jul 21, 2023
5e97b86
yet more snapshots
bollwyvl Jul 21, 2023
94c3f61
more snapshots
bollwyvl Jul 22, 2023
b14dfd9
Merge branch 'main' into add-mermaid-to-marked
bollwyvl Jul 23, 2023
7a3b580
bump to marked 5.1.1
bollwyvl Jul 24, 2023
1918d8d
Merge remote-tracking branch 'upstream/main' into add-mermaid-to-marked
bollwyvl Jul 24, 2023
00006da
Merge remote-tracking branch 'origin/add-mermaid-to-marked' into add-…
bollwyvl Jul 24, 2023
aab0587
mermaid 10.3.0
bollwyvl Jul 26, 2023
fec6706
merge upstream
bollwyvl Jul 26, 2023
27f1677
relock, deduplicate
bollwyvl Jul 26, 2023
09e568c
update marked
bollwyvl Jul 26, 2023
25886f0
merge upstream
bollwyvl Aug 14, 2023
5913b09
--amend
bollwyvl Aug 14, 2023
9de92c7
--amend
bollwyvl Aug 14, 2023
488f607
--amend
bollwyvl Aug 14, 2023
7045626
--amend
bollwyvl Aug 14, 2023
31b4a9b
fix marked types
bollwyvl Aug 14, 2023
33eaef3
move fake marked type packages to missing from unused
bollwyvl Aug 14, 2023
8f8473f
remove marked pre-init
bollwyvl Aug 14, 2023
b25fc76
load marked, plugins in parallel
bollwyvl Aug 14, 2023
5716db7
try pinning binder env, update snapshots
bollwyvl Aug 14, 2023
84186d6
Merge branch 'main' into add-mermaid-to-marked
bollwyvl Aug 16, 2023
2940f84
Merge branch 'main' into add-mermaid-to-marked
bollwyvl Aug 16, 2023
a7bc3b4
Undo unwanted snapshot change
fcollonval Aug 22, 2023
1585e04
Fix UI test
fcollonval Aug 22, 2023
17e21c0
Fix parsing code block with CM
fcollonval Aug 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions galata/test/jupyterlab/notebook-markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,18 @@ test.describe('Notebook Markdown', () => {
imageName
);
});

test('Render a MermaidJS flowchart', async ({ page, tmpPath }) => {
await page.notebook.openByPath(`${tmpPath}/${fileName}`);
const imageName = 'render-mermaid-flowchart.png';
const cell = await page.notebook.getCell(3);
expect(await cell.screenshot()).toMatchSnapshot(imageName);
});

test('Render a MermaidJS error', async ({ page, tmpPath }) => {
await page.notebook.openByPath(`${tmpPath}/${fileName}`);
const imageName = 'render-mermaid-error.png';
const cell = await page.notebook.getCell(4);
expect(await cell.screenshot()).toMatchSnapshot(imageName);
});
});
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 29 additions & 1 deletion galata/test/jupyterlab/notebooks/markdown_notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@
"$ ls\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Demonstration of MermaidJS\n",
"\n",
"Below is a diagram of a chicken and an egg.\n",
"\n",
"```mermaid\n",
"flowchart LR\n",
" chicken --> egg --> chicken\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Broken MermaidJS\n",
"\n",
"Below is a broken diagram.\n",
"\n",
"```mermaid\n",
"flowchart LR\n",
" chicken --> egg --> \n",
"```"
]
}
],
"metadata": {
Expand All @@ -95,7 +123,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.3"
"version": "3.11.0"
}
},
"nbformat": 4,
Expand Down
10 changes: 8 additions & 2 deletions packages/markedparser-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"files": [
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
"style/base.css",
"style/index.css",
"style/index.js",
"src/**/*.{ts,tsx}"
Expand All @@ -36,12 +37,17 @@
},
"dependencies": {
"@jupyterlab/application": "^4.0.0-alpha.19",
"@jupyterlab/apputils": "^4.0.0-alpha.19",
"@jupyterlab/codemirror": "^4.0.0-alpha.19",
"@jupyterlab/rendermime": "^4.0.0-alpha.19",
"marked": "^4.0.17"
"@lumino/coreutils": "^2.0.0-rc.0",
"marked": "^4.2.12",
"mermaid": "^10.0.0"
},
"devDependencies": {
"@types/marked": "^4.0.3",
"@types/d3": "^7.4.0",
"@types/dompurify": "^2.4.0",
"@types/marked": "^4.0.8",
"rimraf": "~3.0.0",
"typescript": "~5.0.0-beta"
},
Expand Down
266 changes: 217 additions & 49 deletions packages/markedparser-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@
* @module markedparser-extension
*/

import { PromiseDelegate } from '@lumino/coreutils';

import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { IThemeManager } from '@jupyterlab/apputils';
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
import { IMarkdownParser } from '@jupyterlab/rendermime';
import { marked } from 'marked';

import type { marked, Renderer } from 'marked';
import type mermaid from 'mermaid';

const FENCE = '```~~~';
const MAX_CACHE = 256;
const MERMAID_CLASS = 'jp-RenderedMermaid';
const ERROR_CLASS = 'jp-mod-error';

/**
* The markdown parser plugin.
Expand All @@ -22,20 +32,16 @@ const plugin: JupyterFrontEndPlugin<IMarkdownParser> = {
id: '@jupyterlab/markedparser-extension:plugin',
autoStart: true,
provides: IMarkdownParser,
requires: [IEditorLanguageRegistry],
activate: (app: JupyterFrontEnd, languages: IEditorLanguageRegistry) => {
Private.initializeMarked(languages);
requires: [IEditorLanguageRegistry, IThemeManager],
bollwyvl marked this conversation as resolved.
Show resolved Hide resolved
activate: (
app: JupyterFrontEnd,
languages: IEditorLanguageRegistry,
themes: IThemeManager
) => {
return {
render: (content: string): Promise<string> =>
new Promise<string>((resolve, reject) => {
marked(content, (err: any, content: string) => {
if (err) {
reject(err);
} else {
resolve(content);
}
});
})
render: async (content: string): Promise<string> => {
return await Private.render(content, languages, themes);
}
};
}
};
Expand All @@ -46,45 +52,207 @@ const plugin: JupyterFrontEndPlugin<IMarkdownParser> = {
export default plugin;

namespace Private {
let markedInitialized = false;
export function initializeMarked(languages: IEditorLanguageRegistry): void {
if (markedInitialized) {
return;
} else {
markedInitialized = true;
let _initializing: PromiseDelegate<typeof marked> | null = null;
let _marked: typeof marked | null = null;
let _mermaid: typeof mermaid | null = null;
let _themes: IThemeManager | null = null;
let _languages: IEditorLanguageRegistry | null = null;
let _markedOptions: marked.MarkedOptions = {};
let _highlights = new Map<string, string>();
let _diagrams = new Map<string, string>();
let _nextMermaidId = 0;

export async function render(
content: string,
languages: IEditorLanguageRegistry,
themes: IThemeManager
): Promise<string> {
if (!_marked) {
_marked = await initializeMarked(languages, themes);
}
return _marked(content, _markedOptions);
}

/**
* Load marked lazily and exactly once.
*/
export async function initializeMarked(
languages: IEditorLanguageRegistry,
themes: IThemeManager
): Promise<typeof marked> {
if (_marked) {
return _marked;
}

if (_initializing) {
return await _initializing.promise;
}

marked.setOptions({
_initializing = new PromiseDelegate();
_themes = themes;
_languages = languages;

// load marked lazily, and exactly once
const { marked, Renderer } = await import('marked');

// finish marked configuration
_markedOptions = {
// use the explicit async paradigm for `walkTokens`
async: true,
// enable all built-in GitHub-flavored Markdown opinions
gfm: true,
// santizing is applied by the sanitizer
sanitize: false,
// breaks: true; We can't use GFM breaks as it causes problems with tables
langPrefix: `language-`,
highlight: (code, lang, callback) => {
const cb = (err: Error | null, code: string) => {
if (callback) {
callback(err, code);
}
return code;
};
if (!lang) {
// no language, no highlight
return cb(null, code);
}
const el = document.createElement('div');
try {
languages
.highlight(code, languages.findBest(lang), el)
.then(() => {
return cb(null, el.innerHTML);
})
.catch(reason => {
return cb(reason, code);
});
} catch (err) {
console.error(`Failed to highlight ${lang} code`, err);
return cb(err, code);
}
// asynchronously prepare for any special tokens, like mermaid and highlighting
walkTokens,
// use custom renderer
renderer: makeRenderer(Renderer)
};

// handle changes to theme (e.g. for mermaid theme)
themes.themeChanged.connect(initTheme);

// complete initialization
_marked = marked;
_initializing.resolve(_marked);
return _marked;
}

/**
* Build a custom marked renderer.
*/
function makeRenderer(Renderer_: typeof Renderer): Renderer {
const renderer = new Renderer_();
const originalCode = renderer.code;

renderer.code = (code: string, language: string) => {
if (language === 'mermaid' && _mermaid) {
return cacheGet(_diagrams, code);
}
});
const key = `${language}${FENCE}${code}${FENCE}`;
const highlight = cacheGet(_highlights, key);
if (highlight != null) {
return highlight;
}
// call with the renderer as `this`
return originalCode.call(renderer, code, language);
};

return renderer;
}

/**
* Apply and cache syntax highlighting for code blocks.
*/
async function highlight(token: marked.Tokens.Code): Promise<void> {
const languages = _languages as IEditorLanguageRegistry;
const { lang, text } = token;
if (!lang) {
// no language, no highlight
return;
}
const key = `${lang}${FENCE}${text}${FENCE}`;
if (cacheGet(_highlights, key)) {
// already cached, don't make another DOM element
return;
}
const el = document.createElement('div');
try {
await languages.highlight(text, languages.findBest(lang), el);
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
const html = `<pre><code class="language-${lang}">${el.innerHTML}</code></pre>`;
cacheSet(_highlights, key, html);
} catch (err) {
console.error(`Failed to highlight ${lang} code`, err);
} finally {
el.remove();
}
}

/**
* After parsing, lazily load and highlight/render code blocks into the cache.
*/
async function walkTokens(token: marked.Token): Promise<void> {
switch (token.type) {
case 'code':
if (token.lang === 'mermaid') {
return await handleMermaid(token);
}
await highlight(token);
}
}

/**
* Load mermaid, and then update the diagram cache.
*/
async function handleMermaid(token: marked.Tokens.Code): Promise<void> {
if (!_mermaid) {
_mermaid = (await import('mermaid')).default;
initTheme();
}

// bail if already cached
if (cacheGet(_diagrams, token.text)) {
return;
}

let html: string;
let className = MERMAID_CLASS;
const id = `jp-mermaid-${_nextMermaidId++}`;

// create temporary element into which to render
const el = document.createElement('div');
document.body.appendChild(el);

try {
const { svg } = await _mermaid.render(id, token.text, el);
html = `<img src="data:image/svg+xml,${encodeURIComponent(svg)}" />`;
} catch (err) {
className = `${className} ${ERROR_CLASS}`;
html = `<code>${err.message}</code>`;
} finally {
// always remove the element
el.remove();
}

// update the cache for use when rendering
cacheSet(_diagrams, token.text, `<div class="${className}">${html}</div>`);
}

/**
* Clear the diagram cache and reconfigure mermaid if loaded.
*/
function initTheme() {
if (_mermaid && _themes && _themes.theme) {
_diagrams.clear();
_mermaid.mermaidAPI.initialize({
theme: _themes.isLight(_themes.theme) ? 'default' : 'dark',
startOnLoad: false,
fontFamily: window
.getComputedStyle(document.body)
.getPropertyValue('--jp-ui-font-family')
});
}
}

/**
* Restore from cache, and move to the front of the queue.
*/
function cacheGet<K, V>(cache: Map<K, V>, key: K): V | undefined {
const item = cache.get(key);
if (item != null) {
cache.delete(key);
cache.set(key, item);
}
return item;
}

/**
* Set to the front of the cache queue, potentially evicting the oldest key.
*/
function cacheSet<K, V>(cache: Map<K, V>, key: K, item: V): void {
if (cache.size >= MAX_CACHE) {
cache.delete(cache.keys().next().value);
}
cache.set(key, item);
}
}