jupyterlab_markdown_syntax_rendering_fix 0.6.9 → 1.0.2
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/README.md +89 -12
- package/lib/highlight.d.ts +19 -31
- package/lib/highlight.js +25 -55
- package/lib/index.js +13 -136
- package/package.json +2 -2
- package/src/__tests__/jupyterlab_markdown_syntax_rendering_fix.spec.ts +19 -90
- package/src/highlight.ts +29 -59
- package/src/index.ts +13 -159
package/README.md
CHANGED
|
@@ -8,23 +8,38 @@
|
|
|
8
8
|
[](https://kolomolo.com)
|
|
9
9
|
[](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
JupyterLab 4.x extension that restores syntax-highlight colours in rendered Markdown fenced code blocks when no CodeMirror editor is open.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
> [!WARNING]
|
|
14
|
+
> This extension is a temporary fix for a JupyterLab 4.x behaviour where rendered Markdown code highlighting depends on a CodeMirror editor having been opened. Once JupyterLab core mounts the highlight style for the Markdown renderer independently, this extension will be obsolete and should not be installed.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
- **Language agnostic** - works for any fenced language (bash, python, json, ...), independent of mermaid
|
|
17
|
-
- **Targets the cold-load race** - handles the case where a language chunk imports late or the registry is wired after an early render
|
|
18
|
-
- **Frontend only** - pure TypeScript labextension, no server component
|
|
16
|
+
## The Problem
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
Fenced code blocks in rendered Markdown (the Markdown Preview, README files, `.md` documents) appear in a single flat colour instead of syntax-highlighted, even though the highlighter clearly ran.
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
**Symptoms**:
|
|
23
21
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
22
|
+
- bash, python, json and other fenced blocks render in one uniform grey, not coloured tokens
|
|
23
|
+
- Opening the same file in the editor, then returning to the preview, makes the colours appear - and they stay for the rest of the session
|
|
24
|
+
- Affects any rendered Markdown when the session has not yet opened a CodeMirror editor (notebook cell, file editor, console)
|
|
25
|
+
|
|
26
|
+
**Root cause - the highlight StyleModule is never mounted**:
|
|
27
|
+
|
|
28
|
+
- JupyterLab's Markdown renderer highlights code through `@jupyterlab/codemirror`, producing token `<span>`s with CodeMirror's generated highlight classes (e.g. `ͼs`, `ͼ11`)
|
|
29
|
+
- Those class names are emitted by a CodeMirror `StyleModule`, and the CSS that gives them colour is mounted only when an `EditorView` is instantiated (via `syntaxHighlighting(jupyterHighlightStyle)`)
|
|
30
|
+
- With only Markdown previews open, no `EditorView` exists, so the StyleModule is never mounted - the spans carry the right classes but no colour rule, and inherit the plain code text colour
|
|
31
|
+
- Opening any editor mounts the StyleModule document-wide, which is why the workaround of opening the file in the editor fixes the preview
|
|
32
|
+
|
|
33
|
+
## The Fix
|
|
34
|
+
|
|
35
|
+
The extension mounts the highlight StyleModule's CSS once at startup, achieving the same effect as opening an editor - without one.
|
|
36
|
+
|
|
37
|
+
**How it works**:
|
|
38
|
+
|
|
39
|
+
- On activation, reads the rules from `jupyterHighlightStyle.module` (the same StyleModule the Markdown renderer's spans reference) via `getRules()`
|
|
40
|
+
- Injects them into a single `<style>` element in the document head, so every rendered Markdown code block is coloured immediately
|
|
41
|
+
- Colours are expressed as `--jp-mirror-editor-*-color` CSS variables, so they resolve through whatever theme is active and update on theme change
|
|
42
|
+
- Injection is idempotent (fixed element id) and frontend-only - no server component, no per-render DOM observer
|
|
28
43
|
|
|
29
44
|
## Requirements
|
|
30
45
|
|
|
@@ -45,3 +60,65 @@ To remove the extension, execute:
|
|
|
45
60
|
```bash
|
|
46
61
|
pip uninstall jupyterlab_markdown_syntax_rendering_fix
|
|
47
62
|
```
|
|
63
|
+
|
|
64
|
+
## Contributing
|
|
65
|
+
|
|
66
|
+
### Development install
|
|
67
|
+
|
|
68
|
+
Note: You will need NodeJS to build the extension package.
|
|
69
|
+
|
|
70
|
+
The `jlpm` command is JupyterLab's pinned version of [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use `yarn` or `npm` in lieu of `jlpm` below.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Clone the repo to your local environment
|
|
74
|
+
# Change directory to the jupyterlab_markdown_syntax_rendering_fix directory
|
|
75
|
+
|
|
76
|
+
# Set up a virtual environment and install package in development mode
|
|
77
|
+
python -m venv .venv
|
|
78
|
+
source .venv/bin/activate
|
|
79
|
+
pip install --editable "."
|
|
80
|
+
|
|
81
|
+
# Link your development version of the extension with JupyterLab
|
|
82
|
+
jupyter labextension develop . --overwrite
|
|
83
|
+
|
|
84
|
+
# Rebuild extension TypeScript source after making changes
|
|
85
|
+
# IMPORTANT: Unlike the steps above which are performed only once, do this step
|
|
86
|
+
# every time you make a change.
|
|
87
|
+
jlpm build
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Watch the source directory in one terminal, automatically rebuilding when needed
|
|
94
|
+
jlpm watch
|
|
95
|
+
# Run JupyterLab in another terminal
|
|
96
|
+
jupyter lab
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Development uninstall
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip uninstall jupyterlab_markdown_syntax_rendering_fix
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
In development mode, you will also need to remove the symlink created by `jupyter labextension develop` command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` folder is located. Then you can remove the symlink named `jupyterlab_markdown_syntax_rendering_fix` within that folder.
|
|
106
|
+
|
|
107
|
+
### Testing the extension
|
|
108
|
+
|
|
109
|
+
#### Frontend tests
|
|
110
|
+
|
|
111
|
+
This extension is using [Jest](https://jestjs.io/) for JavaScript code testing.
|
|
112
|
+
|
|
113
|
+
To execute them, execute:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
jlpm
|
|
117
|
+
jlpm test
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Integration tests
|
|
121
|
+
|
|
122
|
+
This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab.
|
|
123
|
+
|
|
124
|
+
More information are provided within the [ui-tests](./ui-tests/README.md) README.
|
package/lib/highlight.d.ts
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* tested directly.
|
|
2
|
+
* Restore syntax highlighting in rendered Markdown code blocks.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of JupyterLab imports so it can be unit tested directly.
|
|
5
5
|
*/
|
|
6
|
+
/** Id of the injected highlight-style element, used to keep injection idempotent. */
|
|
7
|
+
export declare const HIGHLIGHT_STYLE_ID = "jupyterlab-markdown-syntax-rendering-fix-highlight";
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* Inject the CodeMirror highlight StyleModule's CSS into `doc` so rendered
|
|
10
|
+
* Markdown token spans are coloured.
|
|
11
|
+
*
|
|
12
|
+
* Rendered code blocks already carry the generated highlight classes (e.g.
|
|
13
|
+
* `ͼs`), but the StyleModule that gives those classes their colours is only
|
|
14
|
+
* mounted when a CodeMirror `EditorView` is instantiated. With only Markdown
|
|
15
|
+
* previews open, no editor exists, so the spans inherit the plain code colour
|
|
16
|
+
* and look unhighlighted. Mounting the rules once - the same effect as opening
|
|
17
|
+
* the editor - colours every preview. Colours resolve through
|
|
18
|
+
* `--jp-mirror-editor-*-color` CSS variables, so they track the active theme.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent: a fixed element id means repeated calls are a no-op, and an empty
|
|
21
|
+
* rule string is ignored.
|
|
9
22
|
*/
|
|
10
|
-
export declare
|
|
11
|
-
/**
|
|
12
|
-
* Fenced languages rendered by their own renderer (e.g. mermaid -> SVG). They
|
|
13
|
-
* are never plain `pre > code` highlight candidates, so leave them untouched.
|
|
14
|
-
*/
|
|
15
|
-
export declare const SKIP_LANGUAGES: Set<string>;
|
|
16
|
-
/**
|
|
17
|
-
* Extract the CodeMirror language name from a `language-<name>` class token.
|
|
18
|
-
* Returns null when no such class is present.
|
|
19
|
-
*/
|
|
20
|
-
export declare function languageFromClass(className: string): string | null;
|
|
21
|
-
/**
|
|
22
|
-
* A rendered fenced code block has lost its syntax highlighting when it carries
|
|
23
|
-
* a `language-*` class and non-empty text, yet has no child elements - the
|
|
24
|
-
* async highlighter threw and the markdown renderer fell back to a plain
|
|
25
|
-
* `<pre><code>` (cache miss). A correctly highlighted block holds token
|
|
26
|
-
* `<span>` children, so `childElementCount > 0`.
|
|
27
|
-
*/
|
|
28
|
-
export declare function needsHighlight(code: HTMLElement): boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Collect the plain `pre > code.language-*` blocks at or under `root` that need
|
|
31
|
-
* their highlighting recovered. `root` itself is considered, since
|
|
32
|
-
* `querySelectorAll` only matches descendants and an observer may hand us the
|
|
33
|
-
* `<code>` element directly.
|
|
34
|
-
*/
|
|
35
|
-
export declare function collectPlainBlocks(root: ParentNode): HTMLElement[];
|
|
23
|
+
export declare function injectHighlightRules(rules: string, doc?: Document): void;
|
package/lib/highlight.js
CHANGED
|
@@ -1,62 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* tested directly.
|
|
2
|
+
* Restore syntax highlighting in rendered Markdown code blocks.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of JupyterLab imports so it can be unit tested directly.
|
|
5
5
|
*/
|
|
6
|
+
/** Id of the injected highlight-style element, used to keep injection idempotent. */
|
|
7
|
+
export const HIGHLIGHT_STYLE_ID = 'jupyterlab-markdown-syntax-rendering-fix-highlight';
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* Inject the CodeMirror highlight StyleModule's CSS into `doc` so rendered
|
|
10
|
+
* Markdown token spans are coloured.
|
|
11
|
+
*
|
|
12
|
+
* Rendered code blocks already carry the generated highlight classes (e.g.
|
|
13
|
+
* `ͼs`), but the StyleModule that gives those classes their colours is only
|
|
14
|
+
* mounted when a CodeMirror `EditorView` is instantiated. With only Markdown
|
|
15
|
+
* previews open, no editor exists, so the spans inherit the plain code colour
|
|
16
|
+
* and look unhighlighted. Mounting the rules once - the same effect as opening
|
|
17
|
+
* the editor - colours every preview. Colours resolve through
|
|
18
|
+
* `--jp-mirror-editor-*-color` CSS variables, so they track the active theme.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent: a fixed element id means repeated calls are a no-op, and an empty
|
|
21
|
+
* rule string is ignored.
|
|
9
22
|
*/
|
|
10
|
-
export
|
|
11
|
-
/**
|
|
12
|
-
* Fenced languages rendered by their own renderer (e.g. mermaid -> SVG). They
|
|
13
|
-
* are never plain `pre > code` highlight candidates, so leave them untouched.
|
|
14
|
-
*/
|
|
15
|
-
export const SKIP_LANGUAGES = new Set(['mermaid']);
|
|
16
|
-
/**
|
|
17
|
-
* Extract the CodeMirror language name from a `language-<name>` class token.
|
|
18
|
-
* Returns null when no such class is present.
|
|
19
|
-
*/
|
|
20
|
-
export function languageFromClass(className) {
|
|
21
|
-
const match = /(?:^|\s)language-([\w+#-]+)/.exec(className);
|
|
22
|
-
return match ? match[1] : null;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* A rendered fenced code block has lost its syntax highlighting when it carries
|
|
26
|
-
* a `language-*` class and non-empty text, yet has no child elements - the
|
|
27
|
-
* async highlighter threw and the markdown renderer fell back to a plain
|
|
28
|
-
* `<pre><code>` (cache miss). A correctly highlighted block holds token
|
|
29
|
-
* `<span>` children, so `childElementCount > 0`.
|
|
30
|
-
*/
|
|
31
|
-
export function needsHighlight(code) {
|
|
23
|
+
export function injectHighlightRules(rules, doc = document) {
|
|
32
24
|
var _a;
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
const language = languageFromClass(code.className);
|
|
37
|
-
if (!language || SKIP_LANGUAGES.has(language)) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
return ((_a = code.textContent) !== null && _a !== void 0 ? _a : '').trim().length > 0;
|
|
41
|
-
}
|
|
42
|
-
/** CSS selector for a fenced code block carrying a language hint. */
|
|
43
|
-
const CODE_SELECTOR = 'pre > code[class*="language-"]';
|
|
44
|
-
/**
|
|
45
|
-
* Collect the plain `pre > code.language-*` blocks at or under `root` that need
|
|
46
|
-
* their highlighting recovered. `root` itself is considered, since
|
|
47
|
-
* `querySelectorAll` only matches descendants and an observer may hand us the
|
|
48
|
-
* `<code>` element directly.
|
|
49
|
-
*/
|
|
50
|
-
export function collectPlainBlocks(root) {
|
|
51
|
-
const blocks = [];
|
|
52
|
-
const consider = (code) => {
|
|
53
|
-
if (needsHighlight(code)) {
|
|
54
|
-
blocks.push(code);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
if (root instanceof HTMLElement && root.matches(CODE_SELECTOR)) {
|
|
58
|
-
consider(root);
|
|
25
|
+
if (!rules || doc.getElementById(HIGHLIGHT_STYLE_ID)) {
|
|
26
|
+
return;
|
|
59
27
|
}
|
|
60
|
-
|
|
61
|
-
|
|
28
|
+
const style = doc.createElement('style');
|
|
29
|
+
style.id = HIGHLIGHT_STYLE_ID;
|
|
30
|
+
style.textContent = rules;
|
|
31
|
+
((_a = doc.head) !== null && _a !== void 0 ? _a : doc.documentElement).appendChild(style);
|
|
62
32
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,146 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
/** How many times to attempt recovery (only a thrown highlight is retried). */
|
|
4
|
-
const MAX_ATTEMPTS = 4;
|
|
5
|
-
/** Base retry delay; backs off 750/1500/3000ms (~5s total) so a slow chunk import can settle. */
|
|
6
|
-
const BASE_RETRY_DELAY_MS = 750;
|
|
7
|
-
/** Blocks currently being recovered, to dedupe concurrent observer callbacks. */
|
|
8
|
-
const inFlight = new WeakSet();
|
|
9
|
-
const delay = (ms) => new Promise(resolve => {
|
|
10
|
-
window.setTimeout(resolve, ms);
|
|
11
|
-
});
|
|
12
|
-
/**
|
|
13
|
-
* Recover highlighting on a single plain block, and mark it with a terminal
|
|
14
|
-
* `data-msrf` state so it is processed at most once:
|
|
15
|
-
*
|
|
16
|
-
* - `1` success - token spans written
|
|
17
|
-
* - `plain` highlight ran cleanly but produced no tokens (span-less content) -
|
|
18
|
-
* not a failure, the original plain text is left untouched
|
|
19
|
-
* - `skipped` the language is unsupported (`findBest` miss) - deterministic, so
|
|
20
|
-
* no retry and no warning
|
|
21
|
-
* - `failed` the highlight threw on every attempt - left as plain text, no
|
|
22
|
-
* regression vs without this extension
|
|
23
|
-
*
|
|
24
|
-
* Only a thrown highlight is retried, since that is the one transient case (a
|
|
25
|
-
* cold/flaky CodeMirror language-chunk import). Retrying is effective, not
|
|
26
|
-
* theatre: `highlight()` awaits `getLanguage()` internally, which caches the
|
|
27
|
-
* loaded support on the spec only on success (`spec.support = await spec.load()`)
|
|
28
|
-
* - a rejected load throws and leaves `spec.support` unset, and webpack resets a
|
|
29
|
-
* failed chunk load, so each attempt genuinely re-runs the dynamic import.
|
|
30
|
-
* Retries back off over ~5s; a chunk that never loads within that window stays
|
|
31
|
-
* plain until the block is re-rendered (reload / reopen). Concurrent callbacks
|
|
32
|
-
* are deduped via `inFlight`.
|
|
33
|
-
*/
|
|
34
|
-
async function rehighlight(code, languages) {
|
|
35
|
-
var _a;
|
|
36
|
-
const language = languageFromClass(code.className);
|
|
37
|
-
if (!language || inFlight.has(code) || code.hasAttribute(PROCESSED_ATTR)) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const text = (_a = code.textContent) !== null && _a !== void 0 ? _a : '';
|
|
41
|
-
inFlight.add(code);
|
|
42
|
-
try {
|
|
43
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
44
|
-
const spec = languages.findBest(language);
|
|
45
|
-
if (!spec) {
|
|
46
|
-
// Unsupported language - a synchronous registry miss is deterministic,
|
|
47
|
-
// so retrying cannot help. Leave it plain, no warning.
|
|
48
|
-
code.setAttribute(PROCESSED_ATTR, 'skipped');
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
const host = document.createElement('div');
|
|
52
|
-
try {
|
|
53
|
-
await languages.highlight(text, spec, host);
|
|
54
|
-
}
|
|
55
|
-
catch (_b) {
|
|
56
|
-
// The one retryable case: a transient chunk-load flake.
|
|
57
|
-
if (attempt < MAX_ATTEMPTS) {
|
|
58
|
-
await delay(BASE_RETRY_DELAY_MS * 2 ** (attempt - 1));
|
|
59
|
-
// The host may have been re-rendered during the back-off; a fresh node
|
|
60
|
-
// is handled independently, so stop working this detached one.
|
|
61
|
-
if (!code.isConnected) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
code.setAttribute(PROCESSED_ATTR, 'failed');
|
|
67
|
-
console.warn(`[jupyterlab_markdown_syntax_rendering_fix] gave up re-highlighting ${language} after ${MAX_ATTEMPTS} attempts`);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
// The node may have been re-rendered during the await; a fresh node is
|
|
71
|
-
// handled independently, so don't write to a detached one.
|
|
72
|
-
if (!code.isConnected) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
// Highlight ran without throwing. Commit the swap only when the rendered
|
|
76
|
-
// text round-trips exactly - never replace the block with a truncated or
|
|
77
|
-
// divergent fragment (worse than leaving it plain). An empty result is
|
|
78
|
-
// deterministic for the content (a parser that failed to load throws
|
|
79
|
-
// rather than resolving empty), so it is terminal, not retried.
|
|
80
|
-
if (host.childElementCount > 0 && host.textContent === text) {
|
|
81
|
-
// Set the marker in the same synchronous frame as the write so the
|
|
82
|
-
// observer's async callback sees data-msrf and skips the spans we add.
|
|
83
|
-
code.setAttribute(PROCESSED_ATTR, '1');
|
|
84
|
-
code.replaceChildren(...Array.from(host.childNodes));
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
if (host.childElementCount > 0) {
|
|
88
|
-
// Spans were produced but the text did not round-trip - surface this
|
|
89
|
-
// rare case rather than silently suppressing a possibly-correct render.
|
|
90
|
-
console.warn(`[jupyterlab_markdown_syntax_rendering_fix] skipped re-highlight of ${language}: highlighted text did not match source`);
|
|
91
|
-
}
|
|
92
|
-
code.setAttribute(PROCESSED_ATTR, 'plain');
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
inFlight.delete(code);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
1
|
+
import { jupyterHighlightStyle } from '@jupyterlab/codemirror';
|
|
2
|
+
import { injectHighlightRules } from './highlight';
|
|
101
3
|
/**
|
|
102
4
|
* Initialization data for the jupyterlab_markdown_syntax_rendering_fix extension.
|
|
103
5
|
*/
|
|
104
6
|
const plugin = {
|
|
105
7
|
id: 'jupyterlab_markdown_syntax_rendering_fix:plugin',
|
|
106
|
-
description: '
|
|
8
|
+
description: 'Restore syntax highlighting in rendered Markdown code blocks by mounting the CodeMirror highlight StyleModule at startup, so token colours apply even when no editor is open.',
|
|
107
9
|
autoStart: true,
|
|
108
|
-
|
|
109
|
-
|
|
10
|
+
activate: () => {
|
|
11
|
+
var _a, _b;
|
|
110
12
|
console.log('JupyterLab extension jupyterlab_markdown_syntax_rendering_fix is activated!');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for (const mutation of mutations) {
|
|
120
|
-
mutation.addedNodes.forEach(node => {
|
|
121
|
-
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
const element = node;
|
|
125
|
-
// Skip pure observer churn: CodeMirror editors mutate on every
|
|
126
|
-
// keystroke and never hold rendered markdown, and the token spans this
|
|
127
|
-
// extension itself writes land under an already-processed code block.
|
|
128
|
-
if (element.closest('.cm-editor, code[data-msrf]')) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
scan(element);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
// Observe the application shell rather than document.body: rendered markdown
|
|
136
|
-
// lives in the shell (notebook cells, markdown preview). Markdown rendered in
|
|
137
|
-
// body-level overlays (completer / hover docstrings) is intentionally not
|
|
138
|
-
// covered - a deliberate tradeoff to avoid observing the high-churn overlay
|
|
139
|
-
// layer; such code blocks recover on the next in-shell render or a reload.
|
|
140
|
-
const root = app.shell.node;
|
|
141
|
-
observer.observe(root, { childList: true, subtree: true });
|
|
142
|
-
// Recover any markdown already rendered before this extension activated.
|
|
143
|
-
scan(root);
|
|
13
|
+
// Rendered Markdown code blocks receive CodeMirror highlight token classes
|
|
14
|
+
// (e.g. ͼs), but the StyleModule that gives those classes their colours is
|
|
15
|
+
// only mounted when a CodeMirror EditorView is instantiated. With only
|
|
16
|
+
// Markdown previews open, no editor exists, so the spans inherit the plain
|
|
17
|
+
// code colour and look unhighlighted. Mounting the rules once - the same
|
|
18
|
+
// effect as opening the editor - colours every preview, and the colours
|
|
19
|
+
// track the active theme through --jp-mirror-editor-*-color variables.
|
|
20
|
+
injectHighlightRules((_b = (_a = jupyterHighlightStyle.module) === null || _a === void 0 ? void 0 : _a.getRules()) !== null && _b !== void 0 ? _b : '');
|
|
144
21
|
}
|
|
145
22
|
};
|
|
146
23
|
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_markdown_syntax_rendering_fix",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "JupyterLab extension that restores syntax-highlight colours in rendered Markdown fenced code blocks by mounting the CodeMirror highlight StyleModule, so token colours apply even when no editor is open",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
7
7
|
"jupyterlab",
|
|
@@ -1,100 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PROCESSED_ATTR,
|
|
3
|
-
collectPlainBlocks,
|
|
4
|
-
languageFromClass,
|
|
5
|
-
needsHighlight
|
|
6
|
-
} from '../highlight';
|
|
1
|
+
import { HIGHLIGHT_STYLE_ID, injectHighlightRules } from '../highlight';
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const code = document.createElement('code');
|
|
12
|
-
code.className = className;
|
|
13
|
-
code.innerHTML = inner;
|
|
14
|
-
pre.appendChild(code);
|
|
15
|
-
return code;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('languageFromClass', () => {
|
|
19
|
-
it('extracts the language from a language-* class', () => {
|
|
20
|
-
expect(languageFromClass('language-bash')).toBe('bash');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('finds the token among other classes', () => {
|
|
24
|
-
expect(languageFromClass('hljs language-python foo')).toBe('python');
|
|
3
|
+
describe('injectHighlightRules', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
document.getElementById(HIGHLIGHT_STYLE_ID)?.remove();
|
|
25
6
|
});
|
|
26
7
|
|
|
27
|
-
it('
|
|
28
|
-
|
|
29
|
-
|
|
8
|
+
it('appends a style element carrying the rules', () => {
|
|
9
|
+
injectHighlightRules('.ͼs{color:red}');
|
|
10
|
+
const style = document.getElementById(HIGHLIGHT_STYLE_ID);
|
|
11
|
+
expect(style).not.toBeNull();
|
|
12
|
+
expect(style!.tagName).toBe('STYLE');
|
|
13
|
+
expect(style!.textContent).toBe('.ͼs{color:red}');
|
|
30
14
|
});
|
|
31
15
|
|
|
32
|
-
it('
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
describe('needsHighlight', () => {
|
|
39
|
-
it('flags a plain block with a language and text', () => {
|
|
40
|
-
expect(needsHighlight(block('language-bash', 'pip install x'))).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('ignores a block that already has token spans', () => {
|
|
44
|
-
expect(
|
|
45
|
-
needsHighlight(block('language-bash', '<span class="tok">pip</span>'))
|
|
46
|
-
).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('ignores an already-processed block', () => {
|
|
50
|
-
const code = block('language-bash', 'pip install x');
|
|
51
|
-
code.setAttribute(PROCESSED_ATTR, '1');
|
|
52
|
-
expect(needsHighlight(code)).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('ignores blocks without a language class', () => {
|
|
56
|
-
expect(needsHighlight(block('', 'plain text'))).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('ignores empty or whitespace-only blocks', () => {
|
|
60
|
-
expect(needsHighlight(block('language-bash', ' '))).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('skips languages owned by other renderers (mermaid)', () => {
|
|
64
|
-
expect(needsHighlight(block('language-mermaid', 'graph TD; A-->B'))).toBe(
|
|
65
|
-
false
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('collectPlainBlocks', () => {
|
|
71
|
-
it('returns only the plain language blocks under a root', () => {
|
|
72
|
-
const root = document.createElement('div');
|
|
73
|
-
root.appendChild(block('language-bash', 'echo hi').parentElement!);
|
|
74
|
-
root.appendChild(
|
|
75
|
-
block('language-python', '<span class="tok">import os</span>')
|
|
76
|
-
.parentElement!
|
|
77
|
-
);
|
|
78
|
-
root.appendChild(block('language-json', '{"a": 1}').parentElement!);
|
|
79
|
-
root.appendChild(
|
|
80
|
-
block('language-mermaid', 'graph TD; A-->B').parentElement!
|
|
16
|
+
it('is idempotent - a second call does not add another element', () => {
|
|
17
|
+
injectHighlightRules('.ͼs{color:red}');
|
|
18
|
+
injectHighlightRules('.ͼs{color:blue}');
|
|
19
|
+
expect(document.querySelectorAll(`#${HIGHLIGHT_STYLE_ID}`)).toHaveLength(1);
|
|
20
|
+
expect(document.getElementById(HIGHLIGHT_STYLE_ID)!.textContent).toBe(
|
|
21
|
+
'.ͼs{color:red}'
|
|
81
22
|
);
|
|
82
|
-
|
|
83
|
-
const found = collectPlainBlocks(root);
|
|
84
|
-
const langs = found.map(c => languageFromClass(c.className));
|
|
85
|
-
expect(langs).toEqual(['bash', 'json']);
|
|
86
23
|
});
|
|
87
24
|
|
|
88
|
-
it('
|
|
89
|
-
|
|
90
|
-
expect(
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('returns an empty list when everything is already highlighted', () => {
|
|
94
|
-
const root = document.createElement('div');
|
|
95
|
-
root.appendChild(
|
|
96
|
-
block('language-bash', '<span class="tok">echo</span>').parentElement!
|
|
97
|
-
);
|
|
98
|
-
expect(collectPlainBlocks(root)).toHaveLength(0);
|
|
25
|
+
it('ignores an empty rule string', () => {
|
|
26
|
+
injectHighlightRules('');
|
|
27
|
+
expect(document.getElementById(HIGHLIGHT_STYLE_ID)).toBeNull();
|
|
99
28
|
});
|
|
100
29
|
});
|
package/src/highlight.ts
CHANGED
|
@@ -1,67 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* tested directly.
|
|
2
|
+
* Restore syntax highlighting in rendered Markdown code blocks.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of JupyterLab imports so it can be unit tested directly.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
export const PROCESSED_ATTR = 'data-msrf';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Fenced languages rendered by their own renderer (e.g. mermaid -> SVG). They
|
|
15
|
-
* are never plain `pre > code` highlight candidates, so leave them untouched.
|
|
16
|
-
*/
|
|
17
|
-
export const SKIP_LANGUAGES = new Set(['mermaid']);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Extract the CodeMirror language name from a `language-<name>` class token.
|
|
21
|
-
* Returns null when no such class is present.
|
|
22
|
-
*/
|
|
23
|
-
export function languageFromClass(className: string): string | null {
|
|
24
|
-
const match = /(?:^|\s)language-([\w+#-]+)/.exec(className);
|
|
25
|
-
return match ? match[1] : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A rendered fenced code block has lost its syntax highlighting when it carries
|
|
30
|
-
* a `language-*` class and non-empty text, yet has no child elements - the
|
|
31
|
-
* async highlighter threw and the markdown renderer fell back to a plain
|
|
32
|
-
* `<pre><code>` (cache miss). A correctly highlighted block holds token
|
|
33
|
-
* `<span>` children, so `childElementCount > 0`.
|
|
34
|
-
*/
|
|
35
|
-
export function needsHighlight(code: HTMLElement): boolean {
|
|
36
|
-
if (code.hasAttribute(PROCESSED_ATTR) || code.childElementCount > 0) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
const language = languageFromClass(code.className);
|
|
40
|
-
if (!language || SKIP_LANGUAGES.has(language)) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
return (code.textContent ?? '').trim().length > 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** CSS selector for a fenced code block carrying a language hint. */
|
|
47
|
-
const CODE_SELECTOR = 'pre > code[class*="language-"]';
|
|
7
|
+
/** Id of the injected highlight-style element, used to keep injection idempotent. */
|
|
8
|
+
export const HIGHLIGHT_STYLE_ID =
|
|
9
|
+
'jupyterlab-markdown-syntax-rendering-fix-highlight';
|
|
48
10
|
|
|
49
11
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
12
|
+
* Inject the CodeMirror highlight StyleModule's CSS into `doc` so rendered
|
|
13
|
+
* Markdown token spans are coloured.
|
|
14
|
+
*
|
|
15
|
+
* Rendered code blocks already carry the generated highlight classes (e.g.
|
|
16
|
+
* `ͼs`), but the StyleModule that gives those classes their colours is only
|
|
17
|
+
* mounted when a CodeMirror `EditorView` is instantiated. With only Markdown
|
|
18
|
+
* previews open, no editor exists, so the spans inherit the plain code colour
|
|
19
|
+
* and look unhighlighted. Mounting the rules once - the same effect as opening
|
|
20
|
+
* the editor - colours every preview. Colours resolve through
|
|
21
|
+
* `--jp-mirror-editor-*-color` CSS variables, so they track the active theme.
|
|
22
|
+
*
|
|
23
|
+
* Idempotent: a fixed element id means repeated calls are a no-op, and an empty
|
|
24
|
+
* rule string is ignored.
|
|
54
25
|
*/
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
if (root instanceof HTMLElement && root.matches(CODE_SELECTOR)) {
|
|
63
|
-
consider(root);
|
|
26
|
+
export function injectHighlightRules(
|
|
27
|
+
rules: string,
|
|
28
|
+
doc: Document = document
|
|
29
|
+
): void {
|
|
30
|
+
if (!rules || doc.getElementById(HIGHLIGHT_STYLE_ID)) {
|
|
31
|
+
return;
|
|
64
32
|
}
|
|
65
|
-
|
|
66
|
-
|
|
33
|
+
const style = doc.createElement('style');
|
|
34
|
+
style.id = HIGHLIGHT_STYLE_ID;
|
|
35
|
+
style.textContent = rules;
|
|
36
|
+
(doc.head ?? doc.documentElement).appendChild(style);
|
|
67
37
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,120 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from '@jupyterlab/application';
|
|
5
|
-
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
|
|
6
|
-
import {
|
|
7
|
-
PROCESSED_ATTR,
|
|
8
|
-
collectPlainBlocks,
|
|
9
|
-
languageFromClass
|
|
10
|
-
} from './highlight';
|
|
11
|
-
|
|
12
|
-
/** How many times to attempt recovery (only a thrown highlight is retried). */
|
|
13
|
-
const MAX_ATTEMPTS = 4;
|
|
14
|
-
|
|
15
|
-
/** Base retry delay; backs off 750/1500/3000ms (~5s total) so a slow chunk import can settle. */
|
|
16
|
-
const BASE_RETRY_DELAY_MS = 750;
|
|
17
|
-
|
|
18
|
-
/** Blocks currently being recovered, to dedupe concurrent observer callbacks. */
|
|
19
|
-
const inFlight = new WeakSet<HTMLElement>();
|
|
20
|
-
|
|
21
|
-
const delay = (ms: number): Promise<void> =>
|
|
22
|
-
new Promise(resolve => {
|
|
23
|
-
window.setTimeout(resolve, ms);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Recover highlighting on a single plain block, and mark it with a terminal
|
|
28
|
-
* `data-msrf` state so it is processed at most once:
|
|
29
|
-
*
|
|
30
|
-
* - `1` success - token spans written
|
|
31
|
-
* - `plain` highlight ran cleanly but produced no tokens (span-less content) -
|
|
32
|
-
* not a failure, the original plain text is left untouched
|
|
33
|
-
* - `skipped` the language is unsupported (`findBest` miss) - deterministic, so
|
|
34
|
-
* no retry and no warning
|
|
35
|
-
* - `failed` the highlight threw on every attempt - left as plain text, no
|
|
36
|
-
* regression vs without this extension
|
|
37
|
-
*
|
|
38
|
-
* Only a thrown highlight is retried, since that is the one transient case (a
|
|
39
|
-
* cold/flaky CodeMirror language-chunk import). Retrying is effective, not
|
|
40
|
-
* theatre: `highlight()` awaits `getLanguage()` internally, which caches the
|
|
41
|
-
* loaded support on the spec only on success (`spec.support = await spec.load()`)
|
|
42
|
-
* - a rejected load throws and leaves `spec.support` unset, and webpack resets a
|
|
43
|
-
* failed chunk load, so each attempt genuinely re-runs the dynamic import.
|
|
44
|
-
* Retries back off over ~5s; a chunk that never loads within that window stays
|
|
45
|
-
* plain until the block is re-rendered (reload / reopen). Concurrent callbacks
|
|
46
|
-
* are deduped via `inFlight`.
|
|
47
|
-
*/
|
|
48
|
-
async function rehighlight(
|
|
49
|
-
code: HTMLElement,
|
|
50
|
-
languages: IEditorLanguageRegistry
|
|
51
|
-
): Promise<void> {
|
|
52
|
-
const language = languageFromClass(code.className);
|
|
53
|
-
if (!language || inFlight.has(code) || code.hasAttribute(PROCESSED_ATTR)) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const text = code.textContent ?? '';
|
|
57
|
-
inFlight.add(code);
|
|
58
|
-
try {
|
|
59
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
60
|
-
const spec = languages.findBest(language);
|
|
61
|
-
if (!spec) {
|
|
62
|
-
// Unsupported language - a synchronous registry miss is deterministic,
|
|
63
|
-
// so retrying cannot help. Leave it plain, no warning.
|
|
64
|
-
code.setAttribute(PROCESSED_ATTR, 'skipped');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const host = document.createElement('div');
|
|
68
|
-
try {
|
|
69
|
-
await languages.highlight(text, spec, host);
|
|
70
|
-
} catch {
|
|
71
|
-
// The one retryable case: a transient chunk-load flake.
|
|
72
|
-
if (attempt < MAX_ATTEMPTS) {
|
|
73
|
-
await delay(BASE_RETRY_DELAY_MS * 2 ** (attempt - 1));
|
|
74
|
-
// The host may have been re-rendered during the back-off; a fresh node
|
|
75
|
-
// is handled independently, so stop working this detached one.
|
|
76
|
-
if (!code.isConnected) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
code.setAttribute(PROCESSED_ATTR, 'failed');
|
|
82
|
-
console.warn(
|
|
83
|
-
`[jupyterlab_markdown_syntax_rendering_fix] gave up re-highlighting ${language} after ${MAX_ATTEMPTS} attempts`
|
|
84
|
-
);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
// The node may have been re-rendered during the await; a fresh node is
|
|
88
|
-
// handled independently, so don't write to a detached one.
|
|
89
|
-
if (!code.isConnected) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
// Highlight ran without throwing. Commit the swap only when the rendered
|
|
93
|
-
// text round-trips exactly - never replace the block with a truncated or
|
|
94
|
-
// divergent fragment (worse than leaving it plain). An empty result is
|
|
95
|
-
// deterministic for the content (a parser that failed to load throws
|
|
96
|
-
// rather than resolving empty), so it is terminal, not retried.
|
|
97
|
-
if (host.childElementCount > 0 && host.textContent === text) {
|
|
98
|
-
// Set the marker in the same synchronous frame as the write so the
|
|
99
|
-
// observer's async callback sees data-msrf and skips the spans we add.
|
|
100
|
-
code.setAttribute(PROCESSED_ATTR, '1');
|
|
101
|
-
code.replaceChildren(...Array.from(host.childNodes));
|
|
102
|
-
} else {
|
|
103
|
-
if (host.childElementCount > 0) {
|
|
104
|
-
// Spans were produced but the text did not round-trip - surface this
|
|
105
|
-
// rare case rather than silently suppressing a possibly-correct render.
|
|
106
|
-
console.warn(
|
|
107
|
-
`[jupyterlab_markdown_syntax_rendering_fix] skipped re-highlight of ${language}: highlighted text did not match source`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
code.setAttribute(PROCESSED_ATTR, 'plain');
|
|
111
|
-
}
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
} finally {
|
|
115
|
-
inFlight.delete(code);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
import { jupyterHighlightStyle } from '@jupyterlab/codemirror';
|
|
3
|
+
import { injectHighlightRules } from './highlight';
|
|
118
4
|
|
|
119
5
|
/**
|
|
120
6
|
* Initialization data for the jupyterlab_markdown_syntax_rendering_fix extension.
|
|
@@ -122,53 +8,21 @@ async function rehighlight(
|
|
|
122
8
|
const plugin: JupyterFrontEndPlugin<void> = {
|
|
123
9
|
id: 'jupyterlab_markdown_syntax_rendering_fix:plugin',
|
|
124
10
|
description:
|
|
125
|
-
'
|
|
11
|
+
'Restore syntax highlighting in rendered Markdown code blocks by mounting the CodeMirror highlight StyleModule at startup, so token colours apply even when no editor is open.',
|
|
126
12
|
autoStart: true,
|
|
127
|
-
|
|
128
|
-
activate: (app: JupyterFrontEnd, languages: IEditorLanguageRegistry) => {
|
|
13
|
+
activate: (): void => {
|
|
129
14
|
console.log(
|
|
130
15
|
'JupyterLab extension jupyterlab_markdown_syntax_rendering_fix is activated!'
|
|
131
16
|
);
|
|
132
17
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const observer = new MutationObserver(mutations => {
|
|
145
|
-
for (const mutation of mutations) {
|
|
146
|
-
mutation.addedNodes.forEach(node => {
|
|
147
|
-
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const element = node as HTMLElement;
|
|
151
|
-
// Skip pure observer churn: CodeMirror editors mutate on every
|
|
152
|
-
// keystroke and never hold rendered markdown, and the token spans this
|
|
153
|
-
// extension itself writes land under an already-processed code block.
|
|
154
|
-
if (element.closest('.cm-editor, code[data-msrf]')) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
scan(element);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Observe the application shell rather than document.body: rendered markdown
|
|
163
|
-
// lives in the shell (notebook cells, markdown preview). Markdown rendered in
|
|
164
|
-
// body-level overlays (completer / hover docstrings) is intentionally not
|
|
165
|
-
// covered - a deliberate tradeoff to avoid observing the high-churn overlay
|
|
166
|
-
// layer; such code blocks recover on the next in-shell render or a reload.
|
|
167
|
-
const root = app.shell.node;
|
|
168
|
-
observer.observe(root, { childList: true, subtree: true });
|
|
169
|
-
|
|
170
|
-
// Recover any markdown already rendered before this extension activated.
|
|
171
|
-
scan(root);
|
|
18
|
+
// Rendered Markdown code blocks receive CodeMirror highlight token classes
|
|
19
|
+
// (e.g. ͼs), but the StyleModule that gives those classes their colours is
|
|
20
|
+
// only mounted when a CodeMirror EditorView is instantiated. With only
|
|
21
|
+
// Markdown previews open, no editor exists, so the spans inherit the plain
|
|
22
|
+
// code colour and look unhighlighted. Mounting the rules once - the same
|
|
23
|
+
// effect as opening the editor - colours every preview, and the colours
|
|
24
|
+
// track the active theme through --jp-mirror-editor-*-color variables.
|
|
25
|
+
injectHighlightRules(jupyterHighlightStyle.module?.getRules() ?? '');
|
|
172
26
|
}
|
|
173
27
|
};
|
|
174
28
|
|