react-codemirror-runmode 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ {
2
+ "enableAllProjectMcpServers": false
3
+ }
@@ -1,5 +1,6 @@
1
1
  import { Parser } from '@lezer/common';
2
2
  import { Language, LanguageDescription } from '@codemirror/language';
3
3
  import { Highlighter } from '@lezer/highlight';
4
- export declare function getCodeParser(languageName: string, fallbackLanguage?: Language, languages?: LanguageDescription[]): Promise<Parser | null>;
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>;
5
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[]>;
package/dist/highlight.js CHANGED
@@ -1,8 +1,47 @@
1
1
  import { LanguageDescription } from '@codemirror/language';
2
2
  import { highlightTree } from '@lezer/highlight';
3
3
  import { languages as builtinLanguages } from '@codemirror/language-data';
4
- export async function getCodeParser(languageName, fallbackLanguage, languages = builtinLanguages) {
5
- if (languageName) {
4
+ import { markdown } from '@codemirror/lang-markdown';
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) {
33
+ const codeBlockLanguages = extractCodeBlockLanguages(input);
34
+ const preloadedLanguages = await preloadLanguageParsers(codeBlockLanguages || [], languages);
35
+ const langSupport = markdown({
36
+ codeLanguages: preloadedLanguages
37
+ });
38
+ return langSupport.language.parser;
39
+ }
40
+ export async function getCodeParser(input, languageName, fallbackLanguage, languages = builtinLanguages) {
41
+ if (languageName === 'markdown' || languageName === 'md') {
42
+ return await getMarkdownParser(input, languages);
43
+ }
44
+ else {
6
45
  const found = LanguageDescription.matchLanguageName(languages, languageName, true);
7
46
  if (found instanceof LanguageDescription) {
8
47
  if (!found.support)
@@ -15,7 +54,7 @@ export async function getCodeParser(languageName, fallbackLanguage, languages =
15
54
  return fallbackLanguage ? fallbackLanguage.parser : null;
16
55
  }
17
56
  export async function highlightCode(languageName, input, highlighter, fallbackLanguage, languages, callback) {
18
- const parser = await getCodeParser(languageName, fallbackLanguage, languages);
57
+ const parser = await getCodeParser(input, languageName, fallbackLanguage, languages);
19
58
  if (parser) {
20
59
  const tree = parser.parse(input);
21
60
  const output = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-codemirror-runmode",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Syntax highlighting for react, utilizing CodeMirror's parser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,6 +14,7 @@
14
14
  "build": "tsc --project tsconfig.build.json",
15
15
  "test": "vitest",
16
16
  "lint": "eslint .",
17
+ "typecheck": "tsc --noEmit",
17
18
  "format": "prettier --write .",
18
19
  "format:check": "prettier --check .",
19
20
  "prepublishOnly": "npm-run-all lint format:check build && npm run test run"
@@ -31,30 +32,31 @@
31
32
  "url": "https://github.com/craftzdog/react-codemirror-runmode.git"
32
33
  },
33
34
  "dependencies": {
34
- "@codemirror/language": "^6.11.0",
35
- "@codemirror/language-data": "^6.5.1",
36
- "@lezer/common": "^1.2.3",
37
- "@lezer/highlight": "^1.2.1"
35
+ "@codemirror/language": "^6.11.3",
36
+ "@codemirror/language-data": "^6.5.2",
37
+ "@lezer/common": "^1.4.0",
38
+ "@lezer/highlight": "^1.2.3"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "react": ">= 18"
41
42
  },
42
43
  "devDependencies": {
43
- "@codemirror/theme-one-dark": "^6.1.2",
44
- "@testing-library/react": "^16.2.0",
45
- "@types/node": "^22.13.10",
46
- "@types/react": "^19.0.12",
47
- "@vitejs/plugin-react": "^4.3.4",
48
- "eslint": "^9.22.0",
49
- "eslint-config-prettier": "^10.1.1",
50
- "eslint-plugin-react": "^7.37.4",
51
- "jsdom": "^26.0.0",
44
+ "@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",
52
53
  "npm-run-all": "^4.1.5",
53
- "prettier": "^3.5.3",
54
- "react": "^19.0.0",
55
- "typescript": "^5.8.2",
56
- "typescript-eslint": "^8.27.0",
57
- "vite": "^6.2.2",
58
- "vitest": "^3.0.9"
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"
59
61
  }
60
62
  }
package/src/highlight.ts CHANGED
@@ -2,13 +2,71 @@ import { Parser } from '@lezer/common'
2
2
  import { Language, LanguageDescription } from '@codemirror/language'
3
3
  import { Highlighter, highlightTree } from '@lezer/highlight'
4
4
  import { languages as builtinLanguages } from '@codemirror/language-data'
5
+ import { markdown } from '@codemirror/lang-markdown'
6
+
7
+ /**
8
+ * Extract language names from code blocks in markdown text
9
+ */
10
+ function extractCodeBlockLanguages(text: string): string[] {
11
+ const codeBlockRegex = /```(\w+)/g
12
+ const languages: string[] = []
13
+ let match
14
+ while ((match = codeBlockRegex.exec(text)) !== null) {
15
+ languages.push(match[1])
16
+ }
17
+ return languages
18
+ }
19
+
20
+ /**
21
+ * Pre-load parsers for a list of language names
22
+ */
23
+ async function preloadLanguageParsers(
24
+ languageNames: string[],
25
+ languages: LanguageDescription[]
26
+ ): Promise<LanguageDescription[]> {
27
+ return (
28
+ await Promise.all(
29
+ languageNames.map(async langName => {
30
+ const found = LanguageDescription.matchLanguageName(
31
+ languages,
32
+ langName,
33
+ true
34
+ )
35
+ if (found instanceof LanguageDescription) {
36
+ if (!found.support) await found.load()
37
+ if (found.support) {
38
+ return found
39
+ }
40
+ }
41
+ })
42
+ )
43
+ ).filter((desc): desc is LanguageDescription => !!desc)
44
+ }
45
+
46
+ export async function getMarkdownParser(
47
+ input: string,
48
+ languages: LanguageDescription[] = builtinLanguages
49
+ ): Promise<Parser> {
50
+ const codeBlockLanguages = extractCodeBlockLanguages(input)
51
+ const preloadedLanguages = await preloadLanguageParsers(
52
+ codeBlockLanguages || [],
53
+ languages
54
+ )
55
+ const langSupport = markdown({
56
+ codeLanguages: preloadedLanguages
57
+ })
58
+ return langSupport.language.parser
59
+ }
5
60
 
6
61
  export async function getCodeParser(
62
+ input: string,
7
63
  languageName: string,
8
64
  fallbackLanguage?: Language,
9
65
  languages: LanguageDescription[] = builtinLanguages
10
66
  ): Promise<Parser | null> {
11
- if (languageName) {
67
+ if (languageName === 'markdown' || languageName === 'md') {
68
+ return await getMarkdownParser(input, languages)
69
+ } else {
12
70
  const found = LanguageDescription.matchLanguageName(
13
71
  languages,
14
72
  languageName,
@@ -35,7 +93,12 @@ export async function highlightCode<Output>(
35
93
  to: number
36
94
  ) => Output
37
95
  ): Promise<Output[]> {
38
- const parser = await getCodeParser(languageName, fallbackLanguage, languages)
96
+ const parser = await getCodeParser(
97
+ input,
98
+ languageName,
99
+ fallbackLanguage,
100
+ languages
101
+ )
39
102
  if (parser) {
40
103
  const tree = parser.parse(input)
41
104
  const output: Array<Output> = []
@@ -10,15 +10,15 @@ const sleep = (msec: number) =>
10
10
 
11
11
  describe('getCodeParser', () => {
12
12
  it('loads a JavaScript parser', async () => {
13
- const parser = await getCodeParser('js')
13
+ const parser = await getCodeParser('', 'js')
14
14
  expect(parser).toBeInstanceOf(Parser)
15
15
  })
16
16
  it('loads a Python parser', async () => {
17
- const parser = await getCodeParser('python')
17
+ const parser = await getCodeParser('', 'python')
18
18
  expect(parser).toBeInstanceOf(Parser)
19
19
  })
20
20
  it('returns null for non-existing languages', async () => {
21
- const parser = await getCodeParser('foobar123')
21
+ const parser = await getCodeParser('', 'foobar123')
22
22
  expect(parser).toBeNull()
23
23
  })
24
24
  })
@@ -67,6 +67,72 @@ describe('highlightCode', () => {
67
67
  })
68
68
  })
69
69
 
70
+ describe('Highlight codeblocks', () => {
71
+ it('highlights codeblocks in markdown', async () => {
72
+ const mdCode = '```js\nconst x = 123\n```'
73
+ const highlighted = await highlightCode(
74
+ 'markdown',
75
+ mdCode,
76
+ oneDarkHighlightStyle,
77
+ undefined,
78
+ undefined,
79
+ (text, style, from, to) => ({ text, style, from, to })
80
+ )
81
+ // Should have tokens for the markdown code block structure and the JS code inside
82
+ expect(highlighted.length).toBeGreaterThan(0)
83
+
84
+ // Find the code fence markers
85
+ const fenceStart = highlighted.find(t => t.text === '```')
86
+ expect(fenceStart).toBeDefined()
87
+
88
+ // Find the language info token
89
+ const langInfo = highlighted.find(t => t.text === 'js')
90
+ expect(langInfo).toBeDefined()
91
+
92
+ // Find the JS keyword 'const' inside the code block
93
+ const constToken = highlighted.find(t => t.text === 'const')
94
+ expect(constToken).toBeDefined()
95
+ expect(constToken?.style).not.toBeNull()
96
+
97
+ // Find the number '123'
98
+ const numberToken = highlighted.find(t => t.text === '123')
99
+ expect(numberToken).toBeDefined()
100
+ expect(numberToken?.style).not.toBeNull()
101
+ })
102
+
103
+ it('highlights typescript codeblocks in markdown', async () => {
104
+ const mdCode = '```typescript\nconst x: number = 123\n```'
105
+ const highlighted = await highlightCode(
106
+ 'markdown',
107
+ mdCode,
108
+ oneDarkHighlightStyle,
109
+ undefined,
110
+ undefined,
111
+ (text, style, from, to) => ({ text, style, from, to })
112
+ )
113
+ expect(highlighted.length).toBeGreaterThan(0)
114
+
115
+ // Find the language info token
116
+ const langInfo = highlighted.find(t => t.text === 'typescript')
117
+ expect(langInfo).toBeDefined()
118
+
119
+ // Find the TS keyword 'const'
120
+ const constToken = highlighted.find(t => t.text === 'const')
121
+ expect(constToken).toBeDefined()
122
+ expect(constToken?.style).not.toBeNull()
123
+
124
+ // Find the type annotation 'number'
125
+ const typeToken = highlighted.find(t => t.text === 'number')
126
+ expect(typeToken).toBeDefined()
127
+ expect(typeToken?.style).not.toBeNull()
128
+
129
+ // Find the number '123'
130
+ const numberToken = highlighted.find(t => t.text === '123')
131
+ expect(numberToken).toBeDefined()
132
+ expect(numberToken?.style).not.toBeNull()
133
+ })
134
+ })
135
+
70
136
  describe('React Highlighter', () => {
71
137
  it('renders highlighted code', async () => {
72
138
  const code = 'const x = 123'
package/tsconfig.json CHANGED
@@ -3,17 +3,18 @@
3
3
  "compilerOptions": {
4
4
  "target": "esnext",
5
5
  "module": "esnext",
6
- "lib": ["es2024", "dom"],
6
+ "lib": ["esnext", "dom"],
7
7
  "jsx": "react-jsx",
8
8
  "esModuleInterop": true,
9
9
  "strict": true,
10
10
  "isolatedModules": true,
11
11
  "sourceMap": false,
12
- "moduleResolution": "node",
12
+ "moduleResolution": "bundler",
13
13
  "resolveJsonModule": true,
14
14
  "noImplicitAny": false,
15
15
  "outDir": "dist",
16
16
  "declaration": true,
17
- "declarationDir": "dist"
17
+ "declarationDir": "dist",
18
+ "skipLibCheck": true
18
19
  }
19
20
  }
package/vite.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- /// <reference types="vitest" />
1
+ /// <reference types="vitest/config" />
2
2
  import { defineConfig } from 'vite'
3
3
  import react from '@vitejs/plugin-react'
4
4