@jupyterlab/markedparser-extension 4.0.4 → 4.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -5,13 +5,29 @@
5
5
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
6
6
  import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
7
7
  import { IMarkdownParser } from '@jupyterlab/rendermime';
8
+ /**
9
+ * An interface for fenced code block renderers.
10
+ */
11
+ export interface IFencedBlockRenderer {
12
+ languages: string[];
13
+ rank: number;
14
+ walk: (text: string) => Promise<void>;
15
+ render: (text: string) => string | null;
16
+ }
17
+ /**
18
+ * Options
19
+ */
20
+ export interface IRenderOptions {
21
+ /** handlers for fenced code blocks */
22
+ blocks?: IFencedBlockRenderer[];
23
+ }
8
24
  /**
9
25
  * Create a markdown parser
10
26
  *
11
27
  * @param languages Editor languages
12
28
  * @returns Markdown parser
13
29
  */
14
- export declare function createMarkdownParser(languages: IEditorLanguageRegistry): {
30
+ export declare function createMarkdownParser(languages: IEditorLanguageRegistry, options?: IRenderOptions): {
15
31
  render: (content: string) => Promise<string>;
16
32
  };
17
33
  /**
package/lib/index.js CHANGED
@@ -6,28 +6,24 @@
6
6
  * @packageDocumentation
7
7
  * @module markedparser-extension
8
8
  */
9
+ import { PromiseDelegate } from '@lumino/coreutils';
10
+ import { LruCache } from '@jupyterlab/coreutils';
9
11
  import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
10
12
  import { IMarkdownParser } from '@jupyterlab/rendermime';
11
- import { marked } from 'marked';
13
+ import { IMermaidMarkdown } from '@jupyterlab/mermaid';
14
+ // highlight cache key separator
15
+ const FENCE = '```~~~';
12
16
  /**
13
17
  * Create a markdown parser
14
18
  *
15
19
  * @param languages Editor languages
16
20
  * @returns Markdown parser
17
21
  */
18
- export function createMarkdownParser(languages) {
19
- Private.initializeMarked(languages);
22
+ export function createMarkdownParser(languages, options) {
20
23
  return {
21
- render: (content) => new Promise((resolve, reject) => {
22
- marked(content, (err, content) => {
23
- if (err) {
24
- reject(err);
25
- }
26
- else {
27
- resolve(content);
28
- }
29
- });
30
- })
24
+ render: (content) => {
25
+ return Private.render(content, languages, options);
26
+ }
31
27
  };
32
28
  }
33
29
  /**
@@ -39,58 +35,161 @@ const plugin = {
39
35
  autoStart: true,
40
36
  provides: IMarkdownParser,
41
37
  requires: [IEditorLanguageRegistry],
42
- activate: (app, languages) => {
43
- return createMarkdownParser(languages);
38
+ optional: [IMermaidMarkdown],
39
+ activate: (app, languages, mermaidMarkdown) => {
40
+ return createMarkdownParser(languages, {
41
+ blocks: mermaidMarkdown ? [mermaidMarkdown] : []
42
+ });
44
43
  }
45
44
  };
46
45
  /**
47
46
  * Export the plugin as default.
48
47
  */
49
48
  export default plugin;
49
+ /**
50
+ * A namespace for private marked functions
51
+ */
50
52
  var Private;
51
53
  (function (Private) {
52
- let markedInitialized = false;
53
- function initializeMarked(languages) {
54
- if (markedInitialized) {
55
- return;
54
+ let _initializing = null;
55
+ let _marked = null;
56
+ let _blocks = [];
57
+ let _languages = null;
58
+ let _markedOptions = {};
59
+ let _highlights = new LruCache();
60
+ async function render(content, languages, options) {
61
+ _languages = languages;
62
+ if (!_marked) {
63
+ _marked = await initializeMarked(options);
56
64
  }
57
- else {
58
- markedInitialized = true;
65
+ return _marked(content, _markedOptions);
66
+ }
67
+ Private.render = render;
68
+ /**
69
+ * Load marked lazily and exactly once.
70
+ */
71
+ async function initializeMarked(options) {
72
+ if (_marked) {
73
+ return _marked;
59
74
  }
60
- marked.setOptions({
75
+ if (_initializing) {
76
+ return await _initializing.promise;
77
+ }
78
+ // order blocks by `rank`
79
+ _blocks = (options === null || options === void 0 ? void 0 : options.blocks) || [];
80
+ _blocks = _blocks.sort((a, b) => { var _a, _b; return ((_a = a.rank) !== null && _a !== void 0 ? _a : Infinity) - ((_b = b.rank) !== null && _b !== void 0 ? _b : Infinity); });
81
+ _initializing = new PromiseDelegate();
82
+ // load marked lazily, and exactly once
83
+ const [{ marked, Renderer }, plugins] = await Promise.all([
84
+ import('marked'),
85
+ loadMarkedPlugins()
86
+ ]);
87
+ // use load marked plugins
88
+ for (const plugin of plugins) {
89
+ marked.use(plugin);
90
+ }
91
+ // finish marked configuration
92
+ _markedOptions = {
93
+ // use the explicit async paradigm for `walkTokens`
94
+ async: true,
95
+ // enable all built-in GitHub-flavored Markdown opinions
61
96
  gfm: true,
97
+ // santizing is applied by the sanitizer
62
98
  sanitize: false,
63
- // breaks: true; We can't use GFM breaks as it causes problems with tables
64
- langPrefix: `language-`,
65
- highlight: (code, lang, callback) => {
66
- const cb = (err, code) => {
67
- if (callback) {
68
- callback(err, code);
99
+ // asynchronously prepare for any special tokens, like highlighting and mermaid
100
+ walkTokens,
101
+ // use custom renderer
102
+ renderer: makeRenderer(Renderer)
103
+ };
104
+ // complete initialization
105
+ _marked = marked;
106
+ _initializing.resolve(_marked);
107
+ return _marked;
108
+ }
109
+ Private.initializeMarked = initializeMarked;
110
+ /**
111
+ * Load and use marked plugins.
112
+ *
113
+ * As of writing, both of these features would work without plugins, but emit
114
+ * deprecation warnings.
115
+ */
116
+ async function loadMarkedPlugins() {
117
+ // use loaded marked plugins
118
+ return Promise.all([
119
+ (async () => (await import('marked-gfm-heading-id')).gfmHeadingId())(),
120
+ (async () => (await import('marked-mangle')).mangle())()
121
+ ]);
122
+ }
123
+ /**
124
+ * Build a custom marked renderer.
125
+ */
126
+ function makeRenderer(Renderer_) {
127
+ const renderer = new Renderer_();
128
+ const originalCode = renderer.code;
129
+ renderer.code = (code, language) => {
130
+ // handle block renderers
131
+ for (const block of _blocks) {
132
+ if (block.languages.includes(language)) {
133
+ const rendered = block.render(code);
134
+ if (rendered != null) {
135
+ return rendered;
69
136
  }
70
- return code;
71
- };
72
- if (!lang) {
73
- // no language, no highlight
74
- return cb(null, code);
75
- }
76
- const el = document.createElement('div');
77
- try {
78
- languages
79
- .highlight(code, languages.findBest(lang), el)
80
- .then(() => {
81
- return cb(null, el.innerHTML);
82
- })
83
- .catch(reason => {
84
- return cb(reason, code);
85
- });
86
- }
87
- catch (err) {
88
- console.error(`Failed to highlight ${lang} code`, err);
89
- return cb(err, code);
90
137
  }
91
138
  }
92
- });
139
+ // handle known highlighting
140
+ const key = `${language}${FENCE}${code}${FENCE}`;
141
+ const highlight = _highlights.get(key);
142
+ if (highlight != null) {
143
+ return highlight;
144
+ }
145
+ // fall back to calling with the renderer as `this`
146
+ return originalCode.call(renderer, code, language);
147
+ };
148
+ return renderer;
149
+ }
150
+ /**
151
+ * Apply and cache syntax highlighting for code blocks.
152
+ */
153
+ async function highlight(token) {
154
+ const { lang, text } = token;
155
+ if (!lang || !_languages) {
156
+ // no language(s), no highlight
157
+ return;
158
+ }
159
+ const key = `${lang}${FENCE}${text}${FENCE}`;
160
+ if (_highlights.get(key)) {
161
+ // already cached, don't make another DOM element
162
+ return;
163
+ }
164
+ const el = document.createElement('div');
165
+ try {
166
+ await _languages.highlight(text, _languages.findBest(lang), el);
167
+ const html = `<pre><code class="language-${lang}">${el.innerHTML}</code></pre>`;
168
+ _highlights.set(key, html);
169
+ }
170
+ catch (err) {
171
+ console.error(`Failed to highlight ${lang} code`, err);
172
+ }
173
+ finally {
174
+ el.remove();
175
+ }
176
+ }
177
+ /**
178
+ * After parsing, lazily load and render or highlight code blocks
179
+ */
180
+ async function walkTokens(token) {
181
+ switch (token.type) {
182
+ case 'code':
183
+ if (token.lang) {
184
+ for (const block of _blocks) {
185
+ if (block.languages.includes(token.lang)) {
186
+ await block.walk(token.text);
187
+ return;
188
+ }
189
+ }
190
+ }
191
+ await highlight(token);
192
+ }
93
193
  }
94
- Private.initializeMarked = initializeMarked;
95
194
  })(Private || (Private = {}));
96
195
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyterlab/markedparser-extension",
3
- "version": "4.0.4",
3
+ "version": "4.1.0-alpha.1",
4
4
  "description": "JupyterLab - Markdown parser provider",
5
5
  "homepage": "https://github.com/jupyterlab/jupyterlab",
6
6
  "bugs": {
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
27
+ "style/base.css",
27
28
  "style/index.css",
28
29
  "style/index.js",
29
30
  "src/**/*.{ts,tsx}"
@@ -35,13 +36,19 @@
35
36
  "watch": "tsc -b --watch"
36
37
  },
37
38
  "dependencies": {
38
- "@jupyterlab/application": "^4.0.4",
39
- "@jupyterlab/codemirror": "^4.0.4",
40
- "@jupyterlab/rendermime": "^4.0.4",
41
- "marked": "^4.0.17"
39
+ "@jupyterlab/application": "^4.1.0-alpha.1",
40
+ "@jupyterlab/codemirror": "^4.1.0-alpha.1",
41
+ "@jupyterlab/coreutils": "^6.1.0-alpha.1",
42
+ "@jupyterlab/mermaid": "^4.1.0-alpha.1",
43
+ "@jupyterlab/rendermime": "^4.1.0-alpha.1",
44
+ "@lumino/coreutils": "^2.1.2",
45
+ "marked": "^7.0.2",
46
+ "marked-gfm-heading-id": "^3.0.6",
47
+ "marked-mangle": "^1.1.2"
42
48
  },
43
49
  "devDependencies": {
44
- "@types/marked": "^4.0.3",
50
+ "@types/d3": "^7.4.0",
51
+ "@types/dompurify": "^2.4.0",
45
52
  "rimraf": "~3.0.0",
46
53
  "typedoc": "~0.24.7",
47
54
  "typescript": "~5.0.4"
package/src/index.ts CHANGED
@@ -7,13 +7,41 @@
7
7
  * @module markedparser-extension
8
8
  */
9
9
 
10
+ import { PromiseDelegate } from '@lumino/coreutils';
11
+
10
12
  import {
11
13
  JupyterFrontEnd,
12
14
  JupyterFrontEndPlugin
13
15
  } from '@jupyterlab/application';
16
+ import { LruCache } from '@jupyterlab/coreutils';
14
17
  import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
15
18
  import { IMarkdownParser } from '@jupyterlab/rendermime';
16
- import { marked } from 'marked';
19
+ import { IMermaidMarkdown } from '@jupyterlab/mermaid';
20
+
21
+ import type { marked, Renderer } from 'marked';
22
+ import type { MarkedExtension, MarkedOptions } from 'MarkedOptions';
23
+ import type { Token, Tokens } from 'Tokens';
24
+
25
+ // highlight cache key separator
26
+ const FENCE = '```~~~';
27
+
28
+ /**
29
+ * An interface for fenced code block renderers.
30
+ */
31
+ export interface IFencedBlockRenderer {
32
+ languages: string[];
33
+ rank: number;
34
+ walk: (text: string) => Promise<void>;
35
+ render: (text: string) => string | null;
36
+ }
37
+
38
+ /**
39
+ * Options
40
+ */
41
+ export interface IRenderOptions {
42
+ /** handlers for fenced code blocks */
43
+ blocks?: IFencedBlockRenderer[];
44
+ }
17
45
 
18
46
  /**
19
47
  * Create a markdown parser
@@ -21,19 +49,14 @@ import { marked } from 'marked';
21
49
  * @param languages Editor languages
22
50
  * @returns Markdown parser
23
51
  */
24
- export function createMarkdownParser(languages: IEditorLanguageRegistry) {
25
- Private.initializeMarked(languages);
52
+ export function createMarkdownParser(
53
+ languages: IEditorLanguageRegistry,
54
+ options?: IRenderOptions
55
+ ) {
26
56
  return {
27
- render: (content: string): Promise<string> =>
28
- new Promise<string>((resolve, reject) => {
29
- marked(content, (err: any, content: string) => {
30
- if (err) {
31
- reject(err);
32
- } else {
33
- resolve(content);
34
- }
35
- });
36
- })
57
+ render: (content: string): Promise<string> => {
58
+ return Private.render(content, languages, options);
59
+ }
37
60
  };
38
61
  }
39
62
 
@@ -46,8 +69,15 @@ const plugin: JupyterFrontEndPlugin<IMarkdownParser> = {
46
69
  autoStart: true,
47
70
  provides: IMarkdownParser,
48
71
  requires: [IEditorLanguageRegistry],
49
- activate: (app: JupyterFrontEnd, languages: IEditorLanguageRegistry) => {
50
- return createMarkdownParser(languages);
72
+ optional: [IMermaidMarkdown],
73
+ activate: (
74
+ app: JupyterFrontEnd,
75
+ languages: IEditorLanguageRegistry,
76
+ mermaidMarkdown: IMermaidMarkdown | null
77
+ ) => {
78
+ return createMarkdownParser(languages, {
79
+ blocks: mermaidMarkdown ? [mermaidMarkdown] : []
80
+ });
51
81
  }
52
82
  };
53
83
 
@@ -56,46 +86,169 @@ const plugin: JupyterFrontEndPlugin<IMarkdownParser> = {
56
86
  */
57
87
  export default plugin;
58
88
 
89
+ /**
90
+ * A namespace for private marked functions
91
+ */
59
92
  namespace Private {
60
- let markedInitialized = false;
61
- export function initializeMarked(languages: IEditorLanguageRegistry): void {
62
- if (markedInitialized) {
63
- return;
64
- } else {
65
- markedInitialized = true;
93
+ let _initializing: PromiseDelegate<typeof marked> | null = null;
94
+ let _marked: typeof marked | null = null;
95
+ let _blocks: IFencedBlockRenderer[] = [];
96
+ let _languages: IEditorLanguageRegistry | null = null;
97
+ let _markedOptions: MarkedOptions = {};
98
+ let _highlights = new LruCache<string, string>();
99
+
100
+ export async function render(
101
+ content: string,
102
+ languages: IEditorLanguageRegistry,
103
+ options?: IRenderOptions
104
+ ): Promise<string> {
105
+ _languages = languages;
106
+ if (!_marked) {
107
+ _marked = await initializeMarked(options);
108
+ }
109
+ return _marked(content, _markedOptions);
110
+ }
111
+
112
+ /**
113
+ * Load marked lazily and exactly once.
114
+ */
115
+ export async function initializeMarked(
116
+ options?: IRenderOptions
117
+ ): Promise<typeof marked> {
118
+ if (_marked) {
119
+ return _marked;
120
+ }
121
+
122
+ if (_initializing) {
123
+ return await _initializing.promise;
124
+ }
125
+
126
+ // order blocks by `rank`
127
+ _blocks = options?.blocks || [];
128
+ _blocks = _blocks.sort(
129
+ (a, b) => (a.rank ?? Infinity) - (b.rank ?? Infinity)
130
+ );
131
+
132
+ _initializing = new PromiseDelegate();
133
+
134
+ // load marked lazily, and exactly once
135
+ const [{ marked, Renderer }, plugins] = await Promise.all([
136
+ import('marked'),
137
+ loadMarkedPlugins()
138
+ ]);
139
+
140
+ // use load marked plugins
141
+ for (const plugin of plugins) {
142
+ marked.use(plugin);
66
143
  }
67
144
 
68
- marked.setOptions({
145
+ // finish marked configuration
146
+ _markedOptions = {
147
+ // use the explicit async paradigm for `walkTokens`
148
+ async: true,
149
+ // enable all built-in GitHub-flavored Markdown opinions
69
150
  gfm: true,
151
+ // santizing is applied by the sanitizer
70
152
  sanitize: false,
71
- // breaks: true; We can't use GFM breaks as it causes problems with tables
72
- langPrefix: `language-`,
73
- highlight: (code, lang, callback) => {
74
- const cb = (err: Error | null, code: string) => {
75
- if (callback) {
76
- callback(err, code);
153
+ // asynchronously prepare for any special tokens, like highlighting and mermaid
154
+ walkTokens,
155
+ // use custom renderer
156
+ renderer: makeRenderer(Renderer)
157
+ };
158
+
159
+ // complete initialization
160
+ _marked = marked;
161
+ _initializing.resolve(_marked);
162
+ return _marked;
163
+ }
164
+
165
+ /**
166
+ * Load and use marked plugins.
167
+ *
168
+ * As of writing, both of these features would work without plugins, but emit
169
+ * deprecation warnings.
170
+ */
171
+ async function loadMarkedPlugins(): Promise<MarkedExtension[]> {
172
+ // use loaded marked plugins
173
+ return Promise.all([
174
+ (async () => (await import('marked-gfm-heading-id')).gfmHeadingId())(),
175
+ (async () => (await import('marked-mangle')).mangle())()
176
+ ]);
177
+ }
178
+
179
+ /**
180
+ * Build a custom marked renderer.
181
+ */
182
+ function makeRenderer(Renderer_: typeof Renderer): Renderer {
183
+ const renderer = new Renderer_();
184
+ const originalCode = renderer.code;
185
+
186
+ renderer.code = (code: string, language: string) => {
187
+ // handle block renderers
188
+ for (const block of _blocks) {
189
+ if (block.languages.includes(language)) {
190
+ const rendered = block.render(code);
191
+ if (rendered != null) {
192
+ return rendered;
77
193
  }
78
- return code;
79
- };
80
- if (!lang) {
81
- // no language, no highlight
82
- return cb(null, code);
83
- }
84
- const el = document.createElement('div');
85
- try {
86
- languages
87
- .highlight(code, languages.findBest(lang), el)
88
- .then(() => {
89
- return cb(null, el.innerHTML);
90
- })
91
- .catch(reason => {
92
- return cb(reason, code);
93
- });
94
- } catch (err) {
95
- console.error(`Failed to highlight ${lang} code`, err);
96
- return cb(err, code);
97
194
  }
98
195
  }
99
- });
196
+
197
+ // handle known highlighting
198
+ const key = `${language}${FENCE}${code}${FENCE}`;
199
+ const highlight = _highlights.get(key);
200
+ if (highlight != null) {
201
+ return highlight;
202
+ }
203
+
204
+ // fall back to calling with the renderer as `this`
205
+ return originalCode.call(renderer, code, language);
206
+ };
207
+
208
+ return renderer;
209
+ }
210
+
211
+ /**
212
+ * Apply and cache syntax highlighting for code blocks.
213
+ */
214
+ async function highlight(token: Tokens.Code): Promise<void> {
215
+ const { lang, text } = token;
216
+ if (!lang || !_languages) {
217
+ // no language(s), no highlight
218
+ return;
219
+ }
220
+ const key = `${lang}${FENCE}${text}${FENCE}`;
221
+ if (_highlights.get(key)) {
222
+ // already cached, don't make another DOM element
223
+ return;
224
+ }
225
+ const el = document.createElement('div');
226
+ try {
227
+ await _languages.highlight(text, _languages.findBest(lang), el);
228
+ const html = `<pre><code class="language-${lang}">${el.innerHTML}</code></pre>`;
229
+ _highlights.set(key, html);
230
+ } catch (err) {
231
+ console.error(`Failed to highlight ${lang} code`, err);
232
+ } finally {
233
+ el.remove();
234
+ }
235
+ }
236
+
237
+ /**
238
+ * After parsing, lazily load and render or highlight code blocks
239
+ */
240
+ async function walkTokens(token: Token): Promise<void> {
241
+ switch (token.type) {
242
+ case 'code':
243
+ if (token.lang) {
244
+ for (const block of _blocks) {
245
+ if (block.languages.includes(token.lang)) {
246
+ await block.walk(token.text);
247
+ return;
248
+ }
249
+ }
250
+ }
251
+ await highlight(token as Tokens.Code);
252
+ }
100
253
  }
101
254
  }
package/style/base.css ADDED
@@ -0,0 +1,4 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
package/style/index.css CHANGED
@@ -7,3 +7,5 @@
7
7
  @import url('~@jupyterlab/rendermime/style/index.css');
8
8
  @import url('~@jupyterlab/application/style/index.css');
9
9
  @import url('~@jupyterlab/codemirror/style/index.css');
10
+ @import url('~@jupyterlab/mermaid/style/index.css');
11
+ @import url('./base.css');
package/style/index.js CHANGED
@@ -7,3 +7,6 @@
7
7
  import '@jupyterlab/rendermime/style/index.js';
8
8
  import '@jupyterlab/application/style/index.js';
9
9
  import '@jupyterlab/codemirror/style/index.js';
10
+ import '@jupyterlab/mermaid/style/index.js';
11
+
12
+ import './base.css';