react-codemirror-runmode 2.2.1 → 2.4.0
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/.oxfmtrc.json +9 -0
- package/.oxlintrc.json +31 -0
- package/README.md +40 -6
- package/dist/highlight.d.ts +14 -4
- package/dist/highlight.js +8 -7
- package/dist/react-highlighter.d.ts +16 -3
- package/dist/react-highlighter.js +6 -5
- package/dist/src/highlight.d.ts +16 -0
- package/dist/src/highlight.js +76 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/react-highlighter.d.ts +24 -0
- package/dist/src/react-highlighter.js +14 -0
- package/package.json +41 -43
- package/pnpm-workspace.yaml +43 -0
- package/src/highlight.ts +32 -33
- package/src/react-highlighter.tsx +33 -11
- package/test/fixtures/large.html +17604 -0
- package/test/index.spec.tsx +81 -5
- package/test/perf.bench.tsx +64 -0
- package/tsconfig.build.json +3 -0
- package/tsconfig.json +2 -0
- package/vite.config.ts +1 -2
- package/.prettierignore +0 -137
- package/.prettierrc.json +0 -10
- package/eslint.config.js +0 -45
package/.oxfmtrc.json
ADDED
package/.oxlintrc.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"plugins": ["typescript", "unicorn", "oxc", "react"],
|
|
4
|
+
"categories": {
|
|
5
|
+
"correctness": "error"
|
|
6
|
+
},
|
|
7
|
+
"settings": {
|
|
8
|
+
"react": {
|
|
9
|
+
"version": "19"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"rules": {
|
|
13
|
+
"typescript/no-explicit-any": "off",
|
|
14
|
+
"typescript/ban-ts-comment": "off",
|
|
15
|
+
"typescript/no-this-alias": "off",
|
|
16
|
+
"typescript/no-unused-vars": [
|
|
17
|
+
"warn",
|
|
18
|
+
{
|
|
19
|
+
"argsIgnorePattern": "^_",
|
|
20
|
+
"varsIgnorePattern": "^_",
|
|
21
|
+
"caughtErrorsIgnorePattern": "^_"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"no-useless-escape": "off",
|
|
25
|
+
"no-unused-expressions": "off",
|
|
26
|
+
"prefer-const": "error",
|
|
27
|
+
"no-unused-vars": "off",
|
|
28
|
+
"react-hooks/exhaustive-deps": "off"
|
|
29
|
+
},
|
|
30
|
+
"ignorePatterns": ["dist/**", "node_modules/**"]
|
|
31
|
+
}
|
package/README.md
CHANGED
|
@@ -37,28 +37,62 @@ You can apply custom themes using CodeMirror's theme system. This component uses
|
|
|
37
37
|
Props:
|
|
38
38
|
|
|
39
39
|
- `lang`: `string` - The name of the language
|
|
40
|
-
- `theme`: [`Highlighter`](https://lezer.codemirror.net/docs/ref/#highlight.Highlighter) - The highlight style
|
|
40
|
+
- `theme`: [`Highlighter`](https://lezer.codemirror.net/docs/ref/#highlight.Highlighter)` | readonly Highlighter[]` - The highlight style. Pass an array to combine several highlighters; their emitted classes are merged per token (e.g. a generic token theme plus a markdown-specific one).
|
|
41
41
|
- `children`: `string` - The code to highlight
|
|
42
42
|
- `fallbackLanguage`: `Language` - Optional fallback language to use if the specified language isn't found
|
|
43
43
|
- `languages`: `LanguageDescription[]` - Optional custom list of language descriptions
|
|
44
|
+
- `markdownConfig`: `MarkdownConfig` - Optional markdown parser options, applied only when `lang` is `markdown`/`md`. See [Markdown highlighting](#markdown-highlighting).
|
|
44
45
|
|
|
45
|
-
### `highlightCode<o>(languageName, input,
|
|
46
|
+
### `highlightCode<o>(languageName, input, highlighter, fallbackLanguage?, languages?, callback, markdownConfig?): Promise<Output[]>`
|
|
46
47
|
|
|
47
48
|
Parameters:
|
|
48
49
|
|
|
49
50
|
- `languageName`: `string` - The name of the language
|
|
50
51
|
- `input`: `string` - The code to highlight
|
|
51
|
-
- `highlighter`: [`Highlighter`](https://lezer.codemirror.net/docs/ref/#highlight.Highlighter) - The highlight style
|
|
52
|
+
- `highlighter`: [`Highlighter`](https://lezer.codemirror.net/docs/ref/#highlight.Highlighter)` | readonly Highlighter[]` - The highlight style, or an array of styles whose classes are merged
|
|
52
53
|
- `fallbackLanguage`: `Language` - Optional fallback language to use if the specified language isn't found
|
|
53
54
|
- `languages`: `LanguageDescription[]` - Optional custom list of language descriptions
|
|
54
55
|
- `callback`: `(text: string, style: string | null, from: number, to: number) => Output)` - A callback function that converts the parsed tokens
|
|
56
|
+
- `markdownConfig`: `MarkdownConfig` - Optional markdown parser options, applied only when `languageName` is `markdown`/`md`
|
|
55
57
|
|
|
56
|
-
### `getCodeParser(languageName,
|
|
58
|
+
### `getCodeParser(input, languageName, fallbackLanguage?, languages?, markdownConfig?): Promise<Parser | null>`
|
|
57
59
|
|
|
58
60
|
Parameters:
|
|
59
61
|
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
62
|
+
- `input`: `string` - The code to highlight (used to preload nested code-fence languages for markdown)
|
|
63
|
+
- `languageName`: `string` - The name of the language
|
|
64
|
+
- `fallbackLanguage?`: `Language` - A fallback language (Optional)
|
|
65
|
+
- `languages?`: `LanguageDescription[]` - Optional custom list of language descriptions
|
|
66
|
+
- `markdownConfig?`: `MarkdownConfig` - Optional markdown parser options, applied only when `languageName` is `markdown`/`md`
|
|
67
|
+
|
|
68
|
+
## Markdown highlighting
|
|
69
|
+
|
|
70
|
+
When `lang` is `markdown` (or `md`), the markdown source is parsed with
|
|
71
|
+
[`@codemirror/lang-markdown`](https://github.com/codemirror/lang-markdown). By default this
|
|
72
|
+
uses a **CommonMark** base with no extensions. Pass `markdownConfig` to opt into GFM and/or
|
|
73
|
+
custom [Lezer markdown extensions](https://github.com/lezer-parser/markdown#user-content-markdownextension):
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
import { Highlighter } from 'react-codemirror-runmode'
|
|
77
|
+
import { markdownLanguage } from '@codemirror/lang-markdown'
|
|
78
|
+
|
|
79
|
+
// GFM (tables, strikethrough, task lists, autolinks) + custom extensions,
|
|
80
|
+
// with two highlighters whose classes are merged per token.
|
|
81
|
+
;<Highlighter
|
|
82
|
+
lang="markdown"
|
|
83
|
+
theme={[tokenHighlighter, mdTokenHighlighter]}
|
|
84
|
+
markdownConfig={{
|
|
85
|
+
base: markdownLanguage,
|
|
86
|
+
extensions: [/* your @lezer/markdown extensions */]
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{markdownSource}
|
|
90
|
+
</Highlighter>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`MarkdownConfig` mirrors the `base` and `extensions` options of `markdown()`. Prefer a stable
|
|
94
|
+
reference for `markdownConfig` (e.g. a module-level constant) to avoid re-highlighting on every
|
|
95
|
+
render.
|
|
62
96
|
|
|
63
97
|
## License
|
|
64
98
|
|
package/dist/highlight.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
2
2
|
import { Language, LanguageDescription } from '@codemirror/language';
|
|
3
|
+
import { Parser } from '@lezer/common';
|
|
3
4
|
import { Highlighter } from '@lezer/highlight';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the markdown parser used when highlighting `markdown`/`md`
|
|
7
|
+
* input. Mirrors the relevant subset of `@codemirror/lang-markdown`'s `markdown()`
|
|
8
|
+
* options so callers can opt into GFM (`base: markdownLanguage`) and custom Lezer
|
|
9
|
+
* markdown extensions (e.g. app-specific node props, GFM alerts).
|
|
10
|
+
*
|
|
11
|
+
* Defaults match `@codemirror/lang-markdown`: a CommonMark base with no extensions.
|
|
12
|
+
*/
|
|
13
|
+
export type MarkdownConfig = Pick<NonNullable<Parameters<typeof markdown>[0]>, 'base' | 'extensions'>;
|
|
14
|
+
export declare function getMarkdownParser(input: string, languages?: LanguageDescription[], markdownConfig?: MarkdownConfig): Promise<Parser>;
|
|
15
|
+
export declare function getCodeParser(input: string, languageName: string, fallbackLanguage?: Language, languages?: LanguageDescription[], markdownConfig?: MarkdownConfig): Promise<Parser | null>;
|
|
16
|
+
export declare function highlightCode<Output>(languageName: string, input: string, highlighter: Highlighter | readonly Highlighter[], fallbackLanguage: Language | undefined, languages: LanguageDescription[] | undefined, callback: (text: string, style: string | null, from: number, to: number) => Output, markdownConfig?: MarkdownConfig): Promise<Output[]>;
|
package/dist/highlight.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
1
2
|
import { LanguageDescription } from '@codemirror/language';
|
|
2
|
-
import { highlightTree } from '@lezer/highlight';
|
|
3
3
|
import { languages as builtinLanguages } from '@codemirror/language-data';
|
|
4
|
-
import {
|
|
4
|
+
import { highlightTree } from '@lezer/highlight';
|
|
5
5
|
/**
|
|
6
6
|
* Extract language names from code blocks in markdown text
|
|
7
7
|
*/
|
|
@@ -29,17 +29,18 @@ async function preloadLanguageParsers(languageNames, languages) {
|
|
|
29
29
|
}
|
|
30
30
|
}))).filter((desc) => !!desc);
|
|
31
31
|
}
|
|
32
|
-
export async function getMarkdownParser(input, languages = builtinLanguages) {
|
|
32
|
+
export async function getMarkdownParser(input, languages = builtinLanguages, markdownConfig = {}) {
|
|
33
33
|
const codeBlockLanguages = extractCodeBlockLanguages(input);
|
|
34
34
|
const preloadedLanguages = await preloadLanguageParsers(codeBlockLanguages || [], languages);
|
|
35
35
|
const langSupport = markdown({
|
|
36
|
+
...markdownConfig,
|
|
36
37
|
codeLanguages: preloadedLanguages
|
|
37
38
|
});
|
|
38
39
|
return langSupport.language.parser;
|
|
39
40
|
}
|
|
40
|
-
export async function getCodeParser(input, languageName, fallbackLanguage, languages = builtinLanguages) {
|
|
41
|
+
export async function getCodeParser(input, languageName, fallbackLanguage, languages = builtinLanguages, markdownConfig) {
|
|
41
42
|
if (languageName === 'markdown' || languageName === 'md') {
|
|
42
|
-
return await getMarkdownParser(input, languages);
|
|
43
|
+
return await getMarkdownParser(input, languages, markdownConfig);
|
|
43
44
|
}
|
|
44
45
|
else {
|
|
45
46
|
const found = LanguageDescription.matchLanguageName(languages, languageName, true);
|
|
@@ -53,8 +54,8 @@ export async function getCodeParser(input, languageName, fallbackLanguage, langu
|
|
|
53
54
|
}
|
|
54
55
|
return fallbackLanguage ? fallbackLanguage.parser : null;
|
|
55
56
|
}
|
|
56
|
-
export async function highlightCode(languageName, input, highlighter, fallbackLanguage, languages, callback) {
|
|
57
|
-
const parser = await getCodeParser(input, languageName, fallbackLanguage, languages);
|
|
57
|
+
export async function highlightCode(languageName, input, highlighter, fallbackLanguage, languages, callback, markdownConfig) {
|
|
58
|
+
const parser = await getCodeParser(input, languageName, fallbackLanguage, languages, markdownConfig);
|
|
58
59
|
if (parser) {
|
|
59
60
|
const tree = parser.parse(input);
|
|
60
61
|
const output = [];
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { Highlighter as LezerHighlighter } from '@lezer/highlight';
|
|
3
1
|
import type { Language, LanguageDescription } from '@codemirror/language';
|
|
2
|
+
import type { Highlighter as LezerHighlighter } from '@lezer/highlight';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { type MarkdownConfig } from './highlight.js';
|
|
4
5
|
export type HighlighterProps = {
|
|
5
6
|
lang: string;
|
|
6
7
|
children: string;
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* One highlighter, or several whose emitted classes are merged per token.
|
|
10
|
+
* Pass an array to combine, e.g. a generic token theme with a
|
|
11
|
+
* markdown-specific one.
|
|
12
|
+
*/
|
|
13
|
+
theme: LezerHighlighter | readonly LezerHighlighter[];
|
|
8
14
|
fallbackLanguage?: Language;
|
|
9
15
|
languages?: LanguageDescription[];
|
|
16
|
+
/**
|
|
17
|
+
* Markdown parser options, used only when `lang` is `markdown`/`md`. Enables
|
|
18
|
+
* GFM (`base: markdownLanguage`) and custom Lezer markdown extensions. Pass a
|
|
19
|
+
* stable reference (e.g. a module-level constant) to avoid re-highlighting on
|
|
20
|
+
* every render.
|
|
21
|
+
*/
|
|
22
|
+
markdownConfig?: MarkdownConfig;
|
|
10
23
|
};
|
|
11
24
|
export declare const Highlighter: React.NamedExoticComponent<HighlighterProps>;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useEffect, useState } from 'react';
|
|
3
3
|
import { highlightCode } from './highlight.js';
|
|
4
|
+
const WRAPPER_STYLE = { display: 'contents' };
|
|
4
5
|
export const Highlighter = memo((props) => {
|
|
5
|
-
const { lang, children: code, theme, fallbackLanguage, languages } = props;
|
|
6
|
+
const { lang, children: code, theme, fallbackLanguage, languages, markdownConfig } = props;
|
|
6
7
|
const [highlightedCode, setHighlightedCode] = useState(null);
|
|
7
8
|
useEffect(() => {
|
|
8
9
|
highlightCode(lang, code, theme, fallbackLanguage, languages, (text, style, from) => {
|
|
9
10
|
return (_jsx("span", { className: style || '', children: text }, from));
|
|
10
|
-
}).then(setHighlightedCode);
|
|
11
|
-
}, [lang, code, theme]);
|
|
12
|
-
return _jsx(
|
|
11
|
+
}, markdownConfig).then(setHighlightedCode);
|
|
12
|
+
}, [lang, code, theme, markdownConfig]);
|
|
13
|
+
return highlightedCode ? (_jsx("span", { style: WRAPPER_STYLE, children: highlightedCode }, "highlighted")) : (_jsx("span", { style: WRAPPER_STYLE, children: code }, "pending"));
|
|
13
14
|
});
|
|
14
15
|
Highlighter.displayName = 'Highlighter';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
2
|
+
import { Language, LanguageDescription } from '@codemirror/language';
|
|
3
|
+
import { Parser } from '@lezer/common';
|
|
4
|
+
import { Highlighter } from '@lezer/highlight';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the markdown parser used when highlighting `markdown`/`md`
|
|
7
|
+
* input. Mirrors the relevant subset of `@codemirror/lang-markdown`'s `markdown()`
|
|
8
|
+
* options so callers can opt into GFM (`base: markdownLanguage`) and custom Lezer
|
|
9
|
+
* markdown extensions (e.g. app-specific node props, GFM alerts).
|
|
10
|
+
*
|
|
11
|
+
* Defaults match `@codemirror/lang-markdown`: a CommonMark base with no extensions.
|
|
12
|
+
*/
|
|
13
|
+
export type MarkdownConfig = Pick<NonNullable<Parameters<typeof markdown>[0]>, 'base' | 'extensions'>;
|
|
14
|
+
export declare function getMarkdownParser(input: string, languages?: LanguageDescription[], markdownConfig?: MarkdownConfig): Promise<Parser>;
|
|
15
|
+
export declare function getCodeParser(input: string, languageName: string, fallbackLanguage?: Language, languages?: LanguageDescription[], markdownConfig?: MarkdownConfig): Promise<Parser | null>;
|
|
16
|
+
export declare function highlightCode<Output>(languageName: string, input: string, highlighter: Highlighter | readonly Highlighter[], fallbackLanguage: Language | undefined, languages: LanguageDescription[] | undefined, callback: (text: string, style: string | null, from: number, to: number) => Output, markdownConfig?: MarkdownConfig): Promise<Output[]>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
2
|
+
import { LanguageDescription } from '@codemirror/language';
|
|
3
|
+
import { languages as builtinLanguages } from '@codemirror/language-data';
|
|
4
|
+
import { highlightTree } from '@lezer/highlight';
|
|
5
|
+
/**
|
|
6
|
+
* Extract language names from code blocks in markdown text
|
|
7
|
+
*/
|
|
8
|
+
function extractCodeBlockLanguages(text) {
|
|
9
|
+
const codeBlockRegex = /```(\w+)/g;
|
|
10
|
+
const languages = [];
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
13
|
+
languages.push(match[1]);
|
|
14
|
+
}
|
|
15
|
+
return languages;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pre-load parsers for a list of language names
|
|
19
|
+
*/
|
|
20
|
+
async function preloadLanguageParsers(languageNames, languages) {
|
|
21
|
+
return (await Promise.all(languageNames.map(async (langName) => {
|
|
22
|
+
const found = LanguageDescription.matchLanguageName(languages, langName, true);
|
|
23
|
+
if (found instanceof LanguageDescription) {
|
|
24
|
+
if (!found.support)
|
|
25
|
+
await found.load();
|
|
26
|
+
if (found.support) {
|
|
27
|
+
return found;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}))).filter((desc) => !!desc);
|
|
31
|
+
}
|
|
32
|
+
export async function getMarkdownParser(input, languages = builtinLanguages, markdownConfig = {}) {
|
|
33
|
+
const codeBlockLanguages = extractCodeBlockLanguages(input);
|
|
34
|
+
const preloadedLanguages = await preloadLanguageParsers(codeBlockLanguages || [], languages);
|
|
35
|
+
const langSupport = markdown({
|
|
36
|
+
...markdownConfig,
|
|
37
|
+
codeLanguages: preloadedLanguages
|
|
38
|
+
});
|
|
39
|
+
return langSupport.language.parser;
|
|
40
|
+
}
|
|
41
|
+
export async function getCodeParser(input, languageName, fallbackLanguage, languages = builtinLanguages, markdownConfig) {
|
|
42
|
+
if (languageName === 'markdown' || languageName === 'md') {
|
|
43
|
+
return await getMarkdownParser(input, languages, markdownConfig);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const found = LanguageDescription.matchLanguageName(languages, languageName, true);
|
|
47
|
+
if (found instanceof LanguageDescription) {
|
|
48
|
+
if (!found.support)
|
|
49
|
+
await found.load();
|
|
50
|
+
return found.support ? found.support.language.parser : null;
|
|
51
|
+
}
|
|
52
|
+
else if (found)
|
|
53
|
+
return found.parser;
|
|
54
|
+
}
|
|
55
|
+
return fallbackLanguage ? fallbackLanguage.parser : null;
|
|
56
|
+
}
|
|
57
|
+
export async function highlightCode(languageName, input, highlighter, fallbackLanguage, languages, callback, markdownConfig) {
|
|
58
|
+
const parser = await getCodeParser(input, languageName, fallbackLanguage, languages, markdownConfig);
|
|
59
|
+
if (parser) {
|
|
60
|
+
const tree = parser.parse(input);
|
|
61
|
+
const output = [];
|
|
62
|
+
let pos = 0;
|
|
63
|
+
highlightTree(tree, highlighter, (from, to, classes) => {
|
|
64
|
+
if (from > pos)
|
|
65
|
+
output.push(callback(input.slice(pos, from), null, pos, from));
|
|
66
|
+
output.push(callback(input.slice(from, to), classes, from, to));
|
|
67
|
+
pos = to;
|
|
68
|
+
});
|
|
69
|
+
pos != tree.length &&
|
|
70
|
+
output.push(callback(input.slice(pos, tree.length), null, pos, tree.length));
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return [callback(input, null, 0, input.length)];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Language, LanguageDescription } from '@codemirror/language';
|
|
2
|
+
import type { Highlighter as LezerHighlighter } from '@lezer/highlight';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { type MarkdownConfig } from './highlight.js';
|
|
5
|
+
export type HighlighterProps = {
|
|
6
|
+
lang: string;
|
|
7
|
+
children: string;
|
|
8
|
+
/**
|
|
9
|
+
* One highlighter, or several whose emitted classes are merged per token.
|
|
10
|
+
* Pass an array to combine, e.g. a generic token theme with a
|
|
11
|
+
* markdown-specific one.
|
|
12
|
+
*/
|
|
13
|
+
theme: LezerHighlighter | readonly LezerHighlighter[];
|
|
14
|
+
fallbackLanguage?: Language;
|
|
15
|
+
languages?: LanguageDescription[];
|
|
16
|
+
/**
|
|
17
|
+
* Markdown parser options, used only when `lang` is `markdown`/`md`. Enables
|
|
18
|
+
* GFM (`base: markdownLanguage`) and custom Lezer markdown extensions. Pass a
|
|
19
|
+
* stable reference (e.g. a module-level constant) to avoid re-highlighting on
|
|
20
|
+
* every render.
|
|
21
|
+
*/
|
|
22
|
+
markdownConfig?: MarkdownConfig;
|
|
23
|
+
};
|
|
24
|
+
export declare const Highlighter: React.NamedExoticComponent<HighlighterProps>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useEffect, useState } from 'react';
|
|
3
|
+
import { highlightCode } from './highlight.js';
|
|
4
|
+
export const Highlighter = memo((props) => {
|
|
5
|
+
const { lang, children: code, theme, fallbackLanguage, languages, markdownConfig } = props;
|
|
6
|
+
const [highlightedCode, setHighlightedCode] = useState(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
highlightCode(lang, code, theme, fallbackLanguage, languages, (text, style, from) => {
|
|
9
|
+
return (_jsx("span", { className: style || '', children: text }, from));
|
|
10
|
+
}, markdownConfig).then(setHighlightedCode);
|
|
11
|
+
}, [lang, code, theme, markdownConfig]);
|
|
12
|
+
return _jsx(_Fragment, { children: highlightedCode || code });
|
|
13
|
+
});
|
|
14
|
+
Highlighter.displayName = 'Highlighter';
|
package/package.json
CHANGED
|
@@ -1,62 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-codemirror-runmode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Syntax highlighting for react, utilizing CodeMirror's parser",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"import": "./dist/index.js",
|
|
10
|
-
"types": "./dist/index.d.ts"
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsc --project tsconfig.build.json",
|
|
15
|
-
"test": "vitest",
|
|
16
|
-
"lint": "eslint .",
|
|
17
|
-
"typecheck": "tsc --noEmit",
|
|
18
|
-
"format": "prettier --write .",
|
|
19
|
-
"format:check": "prettier --check .",
|
|
20
|
-
"prepublishOnly": "npm-run-all lint format:check build && npm run test run"
|
|
21
|
-
},
|
|
22
5
|
"keywords": [
|
|
23
|
-
"react",
|
|
24
6
|
"codemirror",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
7
|
+
"highlight",
|
|
8
|
+
"react",
|
|
9
|
+
"syntax"
|
|
27
10
|
],
|
|
28
|
-
"author": "Takuya Matsuyama <hi@craftz.dog>",
|
|
29
11
|
"license": "MIT",
|
|
12
|
+
"author": "Takuya Matsuyama <hi@craftz.dog>",
|
|
30
13
|
"repository": {
|
|
31
14
|
"type": "git",
|
|
32
15
|
"url": "https://github.com/craftzdog/react-codemirror-runmode.git"
|
|
33
16
|
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
34
25
|
"dependencies": {
|
|
35
|
-
"@codemirror/
|
|
26
|
+
"@codemirror/lang-markdown": "^6.5.1",
|
|
27
|
+
"@codemirror/language": "^6.12.4",
|
|
36
28
|
"@codemirror/language-data": "^6.5.2",
|
|
37
|
-
"@lezer/common": "^1.
|
|
29
|
+
"@lezer/common": "^1.5.2",
|
|
38
30
|
"@lezer/highlight": "^1.2.3"
|
|
39
31
|
},
|
|
40
|
-
"peerDependencies": {
|
|
41
|
-
"react": ">= 18"
|
|
42
|
-
},
|
|
43
32
|
"devDependencies": {
|
|
44
33
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
45
|
-
"@testing-library/react": "^16.3.
|
|
46
|
-
"@types/node": "^
|
|
47
|
-
"@types/react": "^19.2.
|
|
48
|
-
"@vitejs/plugin-react": "^
|
|
49
|
-
"
|
|
50
|
-
"eslint-config-prettier": "^10.1.8",
|
|
51
|
-
"eslint-plugin-react": "^7.37.5",
|
|
52
|
-
"jsdom": "^27.3.0",
|
|
34
|
+
"@testing-library/react": "^16.3.2",
|
|
35
|
+
"@types/node": "^26.1.1",
|
|
36
|
+
"@types/react": "^19.2.17",
|
|
37
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
38
|
+
"jsdom": "^29.1.1",
|
|
53
39
|
"npm-run-all": "^4.1.5",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"react
|
|
57
|
-
"
|
|
58
|
-
"typescript
|
|
59
|
-
"vite": "^
|
|
60
|
-
"vitest": "^4.
|
|
40
|
+
"oxfmt": "0.60.0",
|
|
41
|
+
"oxlint": "1.75.0",
|
|
42
|
+
"react": "^19.2.8",
|
|
43
|
+
"react-dom": "^19.2.8",
|
|
44
|
+
"typescript": "^7.0.2",
|
|
45
|
+
"vite": "^8.1.5",
|
|
46
|
+
"vitest": "^4.1.10"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">= 18"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc --project tsconfig.build.json",
|
|
53
|
+
"test": "vitest --run",
|
|
54
|
+
"bench": "vitest bench --run",
|
|
55
|
+
"lint": "oxlint",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"format": "oxfmt",
|
|
58
|
+
"format:check": "oxfmt --check"
|
|
61
59
|
}
|
|
62
|
-
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
allowBuilds:
|
|
2
|
+
esbuild: true
|
|
3
|
+
minimumReleaseAgeExclude:
|
|
4
|
+
- '@oxfmt/binding-android-arm-eabi@0.56.0'
|
|
5
|
+
- '@oxfmt/binding-android-arm64@0.56.0'
|
|
6
|
+
- '@oxfmt/binding-darwin-arm64@0.56.0'
|
|
7
|
+
- '@oxfmt/binding-darwin-x64@0.56.0'
|
|
8
|
+
- '@oxfmt/binding-freebsd-x64@0.56.0'
|
|
9
|
+
- '@oxfmt/binding-linux-arm-gnueabihf@0.56.0'
|
|
10
|
+
- '@oxfmt/binding-linux-arm-musleabihf@0.56.0'
|
|
11
|
+
- '@oxfmt/binding-linux-arm64-gnu@0.56.0'
|
|
12
|
+
- '@oxfmt/binding-linux-arm64-musl@0.56.0'
|
|
13
|
+
- '@oxfmt/binding-linux-ppc64-gnu@0.56.0'
|
|
14
|
+
- '@oxfmt/binding-linux-riscv64-gnu@0.56.0'
|
|
15
|
+
- '@oxfmt/binding-linux-riscv64-musl@0.56.0'
|
|
16
|
+
- '@oxfmt/binding-linux-s390x-gnu@0.56.0'
|
|
17
|
+
- '@oxfmt/binding-linux-x64-gnu@0.56.0'
|
|
18
|
+
- '@oxfmt/binding-linux-x64-musl@0.56.0'
|
|
19
|
+
- '@oxfmt/binding-openharmony-arm64@0.56.0'
|
|
20
|
+
- '@oxfmt/binding-win32-arm64-msvc@0.56.0'
|
|
21
|
+
- '@oxfmt/binding-win32-ia32-msvc@0.56.0'
|
|
22
|
+
- '@oxfmt/binding-win32-x64-msvc@0.56.0'
|
|
23
|
+
- '@oxlint/binding-android-arm-eabi@1.71.0'
|
|
24
|
+
- '@oxlint/binding-android-arm64@1.71.0'
|
|
25
|
+
- '@oxlint/binding-darwin-arm64@1.71.0'
|
|
26
|
+
- '@oxlint/binding-darwin-x64@1.71.0'
|
|
27
|
+
- '@oxlint/binding-freebsd-x64@1.71.0'
|
|
28
|
+
- '@oxlint/binding-linux-arm-gnueabihf@1.71.0'
|
|
29
|
+
- '@oxlint/binding-linux-arm-musleabihf@1.71.0'
|
|
30
|
+
- '@oxlint/binding-linux-arm64-gnu@1.71.0'
|
|
31
|
+
- '@oxlint/binding-linux-arm64-musl@1.71.0'
|
|
32
|
+
- '@oxlint/binding-linux-ppc64-gnu@1.71.0'
|
|
33
|
+
- '@oxlint/binding-linux-riscv64-gnu@1.71.0'
|
|
34
|
+
- '@oxlint/binding-linux-riscv64-musl@1.71.0'
|
|
35
|
+
- '@oxlint/binding-linux-s390x-gnu@1.71.0'
|
|
36
|
+
- '@oxlint/binding-linux-x64-gnu@1.71.0'
|
|
37
|
+
- '@oxlint/binding-linux-x64-musl@1.71.0'
|
|
38
|
+
- '@oxlint/binding-openharmony-arm64@1.71.0'
|
|
39
|
+
- '@oxlint/binding-win32-arm64-msvc@1.71.0'
|
|
40
|
+
- '@oxlint/binding-win32-ia32-msvc@1.71.0'
|
|
41
|
+
- '@oxlint/binding-win32-x64-msvc@1.71.0'
|
|
42
|
+
- oxfmt@0.56.0
|
|
43
|
+
- oxlint@1.71.0
|
package/src/highlight.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { markdown } from '@codemirror/lang-markdown'
|
|
2
2
|
import { Language, LanguageDescription } from '@codemirror/language'
|
|
3
|
-
import { Highlighter, highlightTree } from '@lezer/highlight'
|
|
4
3
|
import { languages as builtinLanguages } from '@codemirror/language-data'
|
|
5
|
-
import {
|
|
4
|
+
import { Parser } from '@lezer/common'
|
|
5
|
+
import { Highlighter, highlightTree } from '@lezer/highlight'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for the markdown parser used when highlighting `markdown`/`md`
|
|
9
|
+
* input. Mirrors the relevant subset of `@codemirror/lang-markdown`'s `markdown()`
|
|
10
|
+
* options so callers can opt into GFM (`base: markdownLanguage`) and custom Lezer
|
|
11
|
+
* markdown extensions (e.g. app-specific node props, GFM alerts).
|
|
12
|
+
*
|
|
13
|
+
* Defaults match `@codemirror/lang-markdown`: a CommonMark base with no extensions.
|
|
14
|
+
*/
|
|
15
|
+
export type MarkdownConfig = Pick<
|
|
16
|
+
NonNullable<Parameters<typeof markdown>[0]>,
|
|
17
|
+
'base' | 'extensions'
|
|
18
|
+
>
|
|
6
19
|
|
|
7
20
|
/**
|
|
8
21
|
* Extract language names from code blocks in markdown text
|
|
@@ -27,11 +40,7 @@ async function preloadLanguageParsers(
|
|
|
27
40
|
return (
|
|
28
41
|
await Promise.all(
|
|
29
42
|
languageNames.map(async langName => {
|
|
30
|
-
const found = LanguageDescription.matchLanguageName(
|
|
31
|
-
languages,
|
|
32
|
-
langName,
|
|
33
|
-
true
|
|
34
|
-
)
|
|
43
|
+
const found = LanguageDescription.matchLanguageName(languages, langName, true)
|
|
35
44
|
if (found instanceof LanguageDescription) {
|
|
36
45
|
if (!found.support) await found.load()
|
|
37
46
|
if (found.support) {
|
|
@@ -45,14 +54,13 @@ async function preloadLanguageParsers(
|
|
|
45
54
|
|
|
46
55
|
export async function getMarkdownParser(
|
|
47
56
|
input: string,
|
|
48
|
-
languages: LanguageDescription[] = builtinLanguages
|
|
57
|
+
languages: LanguageDescription[] = builtinLanguages,
|
|
58
|
+
markdownConfig: MarkdownConfig = {}
|
|
49
59
|
): Promise<Parser> {
|
|
50
60
|
const codeBlockLanguages = extractCodeBlockLanguages(input)
|
|
51
|
-
const preloadedLanguages = await preloadLanguageParsers(
|
|
52
|
-
codeBlockLanguages || [],
|
|
53
|
-
languages
|
|
54
|
-
)
|
|
61
|
+
const preloadedLanguages = await preloadLanguageParsers(codeBlockLanguages || [], languages)
|
|
55
62
|
const langSupport = markdown({
|
|
63
|
+
...markdownConfig,
|
|
56
64
|
codeLanguages: preloadedLanguages
|
|
57
65
|
})
|
|
58
66
|
return langSupport.language.parser
|
|
@@ -62,16 +70,13 @@ export async function getCodeParser(
|
|
|
62
70
|
input: string,
|
|
63
71
|
languageName: string,
|
|
64
72
|
fallbackLanguage?: Language,
|
|
65
|
-
languages: LanguageDescription[] = builtinLanguages
|
|
73
|
+
languages: LanguageDescription[] = builtinLanguages,
|
|
74
|
+
markdownConfig?: MarkdownConfig
|
|
66
75
|
): Promise<Parser | null> {
|
|
67
76
|
if (languageName === 'markdown' || languageName === 'md') {
|
|
68
|
-
return await getMarkdownParser(input, languages)
|
|
77
|
+
return await getMarkdownParser(input, languages, markdownConfig)
|
|
69
78
|
} else {
|
|
70
|
-
const found = LanguageDescription.matchLanguageName(
|
|
71
|
-
languages,
|
|
72
|
-
languageName,
|
|
73
|
-
true
|
|
74
|
-
)
|
|
79
|
+
const found = LanguageDescription.matchLanguageName(languages, languageName, true)
|
|
75
80
|
if (found instanceof LanguageDescription) {
|
|
76
81
|
if (!found.support) await found.load()
|
|
77
82
|
return found.support ? found.support.language.parser : null
|
|
@@ -83,36 +88,30 @@ export async function getCodeParser(
|
|
|
83
88
|
export async function highlightCode<Output>(
|
|
84
89
|
languageName: string,
|
|
85
90
|
input: string,
|
|
86
|
-
highlighter: Highlighter,
|
|
91
|
+
highlighter: Highlighter | readonly Highlighter[],
|
|
87
92
|
fallbackLanguage: Language | undefined,
|
|
88
93
|
languages: LanguageDescription[] | undefined,
|
|
89
|
-
callback: (
|
|
90
|
-
|
|
91
|
-
style: string | null,
|
|
92
|
-
from: number,
|
|
93
|
-
to: number
|
|
94
|
-
) => Output
|
|
94
|
+
callback: (text: string, style: string | null, from: number, to: number) => Output,
|
|
95
|
+
markdownConfig?: MarkdownConfig
|
|
95
96
|
): Promise<Output[]> {
|
|
96
97
|
const parser = await getCodeParser(
|
|
97
98
|
input,
|
|
98
99
|
languageName,
|
|
99
100
|
fallbackLanguage,
|
|
100
|
-
languages
|
|
101
|
+
languages,
|
|
102
|
+
markdownConfig
|
|
101
103
|
)
|
|
102
104
|
if (parser) {
|
|
103
105
|
const tree = parser.parse(input)
|
|
104
106
|
const output: Array<Output> = []
|
|
105
107
|
let pos = 0
|
|
106
108
|
highlightTree(tree, highlighter, (from, to, classes) => {
|
|
107
|
-
if (from > pos)
|
|
108
|
-
output.push(callback(input.slice(pos, from), null, pos, from))
|
|
109
|
+
if (from > pos) output.push(callback(input.slice(pos, from), null, pos, from))
|
|
109
110
|
output.push(callback(input.slice(from, to), classes, from, to))
|
|
110
111
|
pos = to
|
|
111
112
|
})
|
|
112
113
|
pos != tree.length &&
|
|
113
|
-
output.push(
|
|
114
|
-
callback(input.slice(pos, tree.length), null, pos, tree.length)
|
|
115
|
-
)
|
|
114
|
+
output.push(callback(input.slice(pos, tree.length), null, pos, tree.length))
|
|
116
115
|
return output
|
|
117
116
|
} else {
|
|
118
117
|
return [callback(input, null, 0, input.length)]
|