@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 +17 -1
- package/lib/index.js +149 -50
- package/lib/index.js.map +1 -1
- package/package.json +13 -6
- package/src/index.ts +201 -48
- package/style/base.css +4 -0
- package/style/index.css +2 -0
- package/style/index.js +3 -0
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 {
|
|
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) =>
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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":"AAAA;;;+EAG+E;AAC/E;;;GAGG;
|
|
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.
|
|
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.
|
|
39
|
-
"@jupyterlab/codemirror": "^4.0.
|
|
40
|
-
"@jupyterlab/
|
|
41
|
-
"
|
|
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/
|
|
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 {
|
|
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(
|
|
25
|
-
|
|
52
|
+
export function createMarkdownParser(
|
|
53
|
+
languages: IEditorLanguageRegistry,
|
|
54
|
+
options?: IRenderOptions
|
|
55
|
+
) {
|
|
26
56
|
return {
|
|
27
|
-
render: (content: string): Promise<string> =>
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
package/style/index.css
CHANGED