react-codemirror-runmode 2.2.1 → 2.3.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 ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "arrowParens": "avoid",
4
+ "semi": false,
5
+ "singleQuote": true,
6
+ "trailingComma": "none",
7
+ "sortImports": true,
8
+ "sortPackageJson": true
9
+ }
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,64 @@ 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, highlightStyle, fallbackLanguage?, languages?, callback): Promise<Output[]>`
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, defaultLanguage?): Promise<Parser | null>`
58
+ ### `getCodeParser(input, languageName, fallbackLanguage?, languages?, markdownConfig?): Promise<Parser | null>`
57
59
 
58
60
  Parameters:
59
61
 
60
- - `languageName: string` - The name of the language
61
- - `defaultLanguage?: Language` - A fallback language (Optional)
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: [
87
+ /* your @lezer/markdown extensions */
88
+ ]
89
+ }}
90
+ >
91
+ {markdownSource}
92
+ </Highlighter>
93
+ ```
94
+
95
+ `MarkdownConfig` mirrors the `base` and `extensions` options of `markdown()`. Prefer a stable
96
+ reference for `markdownConfig` (e.g. a module-level constant) to avoid re-highlighting on every
97
+ render.
62
98
 
63
99
  ## License
64
100
 
@@ -1,6 +1,16 @@
1
- import { Parser } from '@lezer/common';
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
- export declare function getMarkdownParser(input: string, languages?: LanguageDescription[]): Promise<Parser>;
5
- export declare function getCodeParser(input: string, languageName: string, fallbackLanguage?: Language, languages?: LanguageDescription[]): Promise<Parser | null>;
6
- export declare function highlightCode<Output>(languageName: string, input: string, highlighter: Highlighter, fallbackLanguage: Language | undefined, languages: LanguageDescription[] | undefined, callback: (text: string, style: string | null, from: number, to: number) => Output): Promise<Output[]>;
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 { markdown } from '@codemirror/lang-markdown';
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
- theme: LezerHighlighter;
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>;
@@ -2,13 +2,13 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { memo, useEffect, useState } from 'react';
3
3
  import { highlightCode } from './highlight.js';
4
4
  export const Highlighter = memo((props) => {
5
- const { lang, children: code, theme, fallbackLanguage, languages } = props;
5
+ const { lang, children: code, theme, fallbackLanguage, languages, markdownConfig } = props;
6
6
  const [highlightedCode, setHighlightedCode] = useState(null);
7
7
  useEffect(() => {
8
8
  highlightCode(lang, code, theme, fallbackLanguage, languages, (text, style, from) => {
9
9
  return (_jsx("span", { className: style || '', children: text }, from));
10
- }).then(setHighlightedCode);
11
- }, [lang, code, theme]);
10
+ }, markdownConfig).then(setHighlightedCode);
11
+ }, [lang, code, theme, markdownConfig]);
12
12
  return _jsx(_Fragment, { children: highlightedCode || code });
13
13
  });
14
14
  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,2 @@
1
+ export * from './highlight.js';
2
+ export * from './react-highlighter.js';
@@ -0,0 +1,2 @@
1
+ export * from './highlight.js';
2
+ export * from './react-highlighter.js';
@@ -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,7 +1,19 @@
1
1
  {
2
2
  "name": "react-codemirror-runmode",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Syntax highlighting for react, utilizing CodeMirror's parser",
5
+ "keywords": [
6
+ "codemirror",
7
+ "highlight",
8
+ "react",
9
+ "syntax"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "Takuya Matsuyama <hi@craftz.dog>",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/craftzdog/react-codemirror-runmode.git"
16
+ },
5
17
  "type": "module",
6
18
  "main": "dist/index.js",
7
19
  "exports": {
@@ -13,50 +25,36 @@
13
25
  "scripts": {
14
26
  "build": "tsc --project tsconfig.build.json",
15
27
  "test": "vitest",
16
- "lint": "eslint .",
28
+ "lint": "oxlint",
17
29
  "typecheck": "tsc --noEmit",
18
- "format": "prettier --write .",
19
- "format:check": "prettier --check .",
30
+ "format": "oxfmt",
31
+ "format:check": "oxfmt --check",
20
32
  "prepublishOnly": "npm-run-all lint format:check build && npm run test run"
21
33
  },
22
- "keywords": [
23
- "react",
24
- "codemirror",
25
- "syntax",
26
- "highlight"
27
- ],
28
- "author": "Takuya Matsuyama <hi@craftz.dog>",
29
- "license": "MIT",
30
- "repository": {
31
- "type": "git",
32
- "url": "https://github.com/craftzdog/react-codemirror-runmode.git"
33
- },
34
34
  "dependencies": {
35
- "@codemirror/language": "^6.11.3",
35
+ "@codemirror/lang-markdown": "^6.5.0",
36
+ "@codemirror/language": "^6.12.3",
36
37
  "@codemirror/language-data": "^6.5.2",
37
- "@lezer/common": "^1.4.0",
38
+ "@lezer/common": "^1.5.2",
38
39
  "@lezer/highlight": "^1.2.3"
39
40
  },
40
- "peerDependencies": {
41
- "react": ">= 18"
42
- },
43
41
  "devDependencies": {
44
42
  "@codemirror/theme-one-dark": "^6.1.3",
45
- "@testing-library/react": "^16.3.0",
46
- "@types/node": "^25.0.1",
47
- "@types/react": "^19.2.7",
48
- "@vitejs/plugin-react": "^5.1.2",
49
- "eslint": "^9.39.1",
50
- "eslint-config-prettier": "^10.1.8",
51
- "eslint-plugin-react": "^7.37.5",
52
- "jsdom": "^27.3.0",
43
+ "@testing-library/react": "^16.3.2",
44
+ "@types/node": "^26.0.0",
45
+ "@types/react": "^19.2.17",
46
+ "@vitejs/plugin-react": "^6.0.2",
47
+ "jsdom": "^29.1.1",
53
48
  "npm-run-all": "^4.1.5",
54
- "prettier": "^3.7.4",
55
- "react": "^19.2.3",
56
- "react-dom": "^19.2.3",
57
- "typescript": "^5.9.3",
58
- "typescript-eslint": "^8.49.0",
59
- "vite": "^7.2.7",
60
- "vitest": "^4.0.15"
49
+ "oxfmt": "0.56.0",
50
+ "oxlint": "1.71.0",
51
+ "react": "^19.2.7",
52
+ "react-dom": "^19.2.7",
53
+ "typescript": "^6.0.3",
54
+ "vite": "^8.0.16",
55
+ "vitest": "^4.1.9"
56
+ },
57
+ "peerDependencies": {
58
+ "react": ">= 18"
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 { Parser } from '@lezer/common'
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 { markdown } from '@codemirror/lang-markdown'
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
- text: string,
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)]
@@ -1,21 +1,32 @@
1
- import React, { memo, useEffect, useState } from 'react'
2
- import { highlightCode } from './highlight.js'
3
- import type { Highlighter as LezerHighlighter } from '@lezer/highlight'
4
1
  import type { Language, LanguageDescription } from '@codemirror/language'
2
+ import type { Highlighter as LezerHighlighter } from '@lezer/highlight'
3
+ import React, { memo, useEffect, useState } from 'react'
4
+
5
+ import { highlightCode, type MarkdownConfig } from './highlight.js'
5
6
 
6
7
  export type HighlighterProps = {
7
8
  lang: string
8
9
  children: string
9
- theme: LezerHighlighter
10
+ /**
11
+ * One highlighter, or several whose emitted classes are merged per token.
12
+ * Pass an array to combine, e.g. a generic token theme with a
13
+ * markdown-specific one.
14
+ */
15
+ theme: LezerHighlighter | readonly LezerHighlighter[]
10
16
  fallbackLanguage?: Language
11
17
  languages?: LanguageDescription[]
18
+ /**
19
+ * Markdown parser options, used only when `lang` is `markdown`/`md`. Enables
20
+ * GFM (`base: markdownLanguage`) and custom Lezer markdown extensions. Pass a
21
+ * stable reference (e.g. a module-level constant) to avoid re-highlighting on
22
+ * every render.
23
+ */
24
+ markdownConfig?: MarkdownConfig
12
25
  }
13
26
 
14
27
  export const Highlighter = memo<HighlighterProps>((props: HighlighterProps) => {
15
- const { lang, children: code, theme, fallbackLanguage, languages } = props
16
- const [highlightedCode, setHighlightedCode] = useState<
17
- React.ReactNode[] | null
18
- >(null)
28
+ const { lang, children: code, theme, fallbackLanguage, languages, markdownConfig } = props
29
+ const [highlightedCode, setHighlightedCode] = useState<React.ReactNode[] | null>(null)
19
30
 
20
31
  useEffect(() => {
21
32
  highlightCode(
@@ -30,9 +41,10 @@ export const Highlighter = memo<HighlighterProps>((props: HighlighterProps) => {
30
41
  {text}
31
42
  </span>
32
43
  )
33
- }
44
+ },
45
+ markdownConfig
34
46
  ).then(setHighlightedCode)
35
- }, [lang, code, theme])
47
+ }, [lang, code, theme, markdownConfig])
36
48
 
37
49
  return <>{highlightedCode || code}</>
38
50
  })
@@ -1,12 +1,14 @@
1
+ import { markdownLanguage } from '@codemirror/lang-markdown'
2
+ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'
3
+ import { Parser } from '@lezer/common'
4
+ import { tagHighlighter, tags } from '@lezer/highlight'
5
+ import { render, screen } from '@testing-library/react'
1
6
  // @vitest-environment jsdom
2
7
  import { describe, expect, it } from 'vitest'
3
- import { Parser } from '@lezer/common'
4
- import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'
8
+
5
9
  import { getCodeParser, highlightCode, Highlighter } from '../src'
6
- import { render, screen } from '@testing-library/react'
7
10
 
8
- const sleep = (msec: number) =>
9
- new Promise(resolve => setTimeout(resolve, msec))
11
+ const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec))
10
12
 
11
13
  describe('getCodeParser', () => {
12
14
  it('loads a JavaScript parser', async () => {
@@ -133,6 +135,58 @@ describe('Highlight codeblocks', () => {
133
135
  })
134
136
  })
135
137
 
138
+ describe('markdownConfig (GFM)', () => {
139
+ const strikeHighlighter = tagHighlighter([{ tag: tags.strikethrough, class: 'strike' }])
140
+
141
+ it('does not tokenize GFM strikethrough with the default CommonMark base', async () => {
142
+ const highlighted = await highlightCode(
143
+ 'markdown',
144
+ '~~gone~~',
145
+ strikeHighlighter,
146
+ undefined,
147
+ undefined,
148
+ (text, style) => ({ text, style })
149
+ )
150
+ expect(highlighted.some(t => t.style === 'strike')).toBe(false)
151
+ })
152
+
153
+ it('tokenizes GFM strikethrough when base is markdownLanguage', async () => {
154
+ const highlighted = await highlightCode(
155
+ 'markdown',
156
+ '~~gone~~',
157
+ strikeHighlighter,
158
+ undefined,
159
+ undefined,
160
+ (text, style) => ({ text, style }),
161
+ { base: markdownLanguage }
162
+ )
163
+ const struck = highlighted.find(t => t.style === 'strike')
164
+ expect(struck).toBeDefined()
165
+ expect(struck?.text).toContain('gone')
166
+ })
167
+ })
168
+
169
+ describe('multiple highlighters', () => {
170
+ const highlighterA = tagHighlighter([{ tag: tags.strikethrough, class: 'a-strike' }])
171
+ const highlighterB = tagHighlighter([{ tag: tags.strikethrough, class: 'b-strike' }])
172
+
173
+ it('merges classes emitted by an array of highlighters', async () => {
174
+ const highlighted = await highlightCode(
175
+ 'markdown',
176
+ '~~gone~~',
177
+ [highlighterA, highlighterB],
178
+ undefined,
179
+ undefined,
180
+ (text, style) => ({ text, style }),
181
+ { base: markdownLanguage }
182
+ )
183
+ const struck = highlighted.find(t => t.style?.includes('a-strike'))
184
+ expect(struck).toBeDefined()
185
+ expect(struck?.style).toContain('a-strike')
186
+ expect(struck?.style).toContain('b-strike')
187
+ })
188
+ })
189
+
136
190
  describe('React Highlighter', () => {
137
191
  it('renders highlighted code', async () => {
138
192
  const code = 'const x = 123'
@@ -1,4 +1,7 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src"
5
+ },
3
6
  "exclude": ["test", "vite.config.ts"]
4
7
  }
package/vite.config.ts CHANGED
@@ -1,6 +1,5 @@
1
- /// <reference types="vitest/config" />
2
- import { defineConfig } from 'vite'
3
1
  import react from '@vitejs/plugin-react'
2
+ import { defineConfig } from 'vitest/config'
4
3
 
5
4
  export default defineConfig({
6
5
  plugins: [react()],
package/.prettierignore DELETED
@@ -1,137 +0,0 @@
1
- # Logs
2
- logs
3
- *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- lerna-debug.log*
8
- .pnpm-debug.log*
9
-
10
- # Diagnostic reports (https://nodejs.org/api/report.html)
11
- report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12
-
13
- # Runtime data
14
- pids
15
- *.pid
16
- *.seed
17
- *.pid.lock
18
-
19
- # Directory for instrumented libs generated by jscoverage/JSCover
20
- lib-cov
21
-
22
- # Coverage directory used by tools like istanbul
23
- coverage
24
- *.lcov
25
-
26
- # nyc test coverage
27
- .nyc_output
28
-
29
- # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30
- .grunt
31
-
32
- # Bower dependency directory (https://bower.io/)
33
- bower_components
34
-
35
- # node-waf configuration
36
- .lock-wscript
37
-
38
- # Compiled binary addons (https://nodejs.org/api/addons.html)
39
- build/Release
40
-
41
- # Dependency directories
42
- node_modules/
43
- jspm_packages/
44
-
45
- # Snowpack dependency directory (https://snowpack.dev/)
46
- web_modules/
47
-
48
- # TypeScript cache
49
- *.tsbuildinfo
50
-
51
- # Optional npm cache directory
52
- .npm
53
-
54
- # Optional eslint cache
55
- .eslintcache
56
-
57
- # Optional stylelint cache
58
- .stylelintcache
59
-
60
- # Microbundle cache
61
- .rpt2_cache/
62
- .rts2_cache_cjs/
63
- .rts2_cache_es/
64
- .rts2_cache_umd/
65
-
66
- # Optional REPL history
67
- .node_repl_history
68
-
69
- # Output of 'npm pack'
70
- *.tgz
71
-
72
- # Yarn Integrity file
73
- .yarn-integrity
74
-
75
- # dotenv environment variable files
76
- .env
77
- .env.development.local
78
- .env.test.local
79
- .env.production.local
80
- .env.local
81
-
82
- # parcel-bundler cache (https://parceljs.org/)
83
- .cache
84
- .parcel-cache
85
-
86
- # Next.js build output
87
- .next
88
- out
89
-
90
- # Nuxt.js build / generate output
91
- .nuxt
92
- dist
93
-
94
- # Gatsby files
95
- .cache/
96
- # Comment in the public line in if your project uses Gatsby and not Next.js
97
- # https://nextjs.org/blog/next-9-1#public-directory-support
98
- # public
99
-
100
- # vuepress build output
101
- .vuepress/dist
102
-
103
- # vuepress v2.x temp and cache directory
104
- .temp
105
- .cache
106
-
107
- # Docusaurus cache and generated files
108
- .docusaurus
109
-
110
- # Serverless directories
111
- .serverless/
112
-
113
- # FuseBox cache
114
- .fusebox/
115
-
116
- # DynamoDB Local files
117
- .dynamodb/
118
-
119
- # TernJS port file
120
- .tern-port
121
-
122
- # Stores VSCode versions used for testing VSCode extensions
123
- .vscode-test
124
-
125
- # yarn v2
126
- .yarn/cache
127
- .yarn/unplugged
128
- .yarn/build-state.yml
129
- .yarn/install-state.gz
130
- .pnp.*
131
-
132
- # IDE
133
- .idea/
134
- .vscode/
135
-
136
- # Lock files
137
- pnpm-lock.yaml
package/.prettierrc.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "arrowParens": "avoid",
3
- "singleQuote": true,
4
- "bracketSpacing": true,
5
- "endOfLine": "lf",
6
- "semi": false,
7
- "tabWidth": 2,
8
- "trailingComma": "none",
9
- "plugins": []
10
- }
package/eslint.config.js DELETED
@@ -1,45 +0,0 @@
1
- import js from '@eslint/js'
2
- import eslintTs from 'typescript-eslint'
3
- import eslintReact from 'eslint-plugin-react'
4
-
5
- export default eslintTs.config(
6
- { ignores: ['dist'] },
7
- js.configs.recommended,
8
- eslintTs.configs.recommended,
9
- eslintReact.configs.flat.recommended,
10
- eslintReact.configs.flat['jsx-runtime'],
11
- {
12
- plugins: {
13
- '@typescript-eslint': eslintTs.plugin
14
- },
15
- rules: {
16
- // TypeScript
17
- '@typescript-eslint/no-explicit-any': 'off',
18
- '@typescript-eslint/no-unused-vars': [
19
- 'warn',
20
- {
21
- argsIgnorePattern: '^_',
22
- varsIgnorePattern: '^_',
23
- caughtErrorsIgnorePattern: '^_'
24
- }
25
- ],
26
- '@typescript-eslint/ban-ts-comment': 'off',
27
- '@typescript-eslint/no-this-alias': 'off',
28
- '@typescript-eslint/no-var-requires': 'off',
29
- '@typescript-eslint/no-unused-expressions': 'off',
30
-
31
- // JavaScript and React rules
32
- 'no-useless-escape': 0,
33
- 'prefer-const': 2,
34
- 'no-unused-vars': 0
35
- }
36
- },
37
-
38
- {
39
- settings: {
40
- react: {
41
- version: '19'
42
- }
43
- }
44
- }
45
- )