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.
@@ -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'
@@ -149,4 +203,26 @@ describe('React Highlighter', () => {
149
203
  expect(element).toHaveProperty('className', 'ͼp')
150
204
  expect(screen.getByText('123')).toHaveProperty('className', 'ͼu')
151
205
  })
206
+
207
+ it('swaps in a freshly created wrapper once highlighting resolves', async () => {
208
+ const { container } = render(
209
+ <code>
210
+ <Highlighter lang="js" theme={oneDarkHighlightStyle}>
211
+ {'const x = 123'}
212
+ </Highlighter>
213
+ </code>
214
+ )
215
+ const pendingWrapper = container.querySelector('code > span')
216
+ expect(pendingWrapper).not.toBeNull()
217
+
218
+ await sleep(100)
219
+
220
+ // Reusing the pending element would make React insert every highlighted span
221
+ // into the already-attached DOM one at a time, which is ~25x slower on large
222
+ // input. The wrapper must be a genuinely new element.
223
+ const resolvedWrapper = container.querySelector('code > span')
224
+ expect(resolvedWrapper).not.toBe(pendingWrapper)
225
+ expect(resolvedWrapper?.getAttribute('style')).toContain('display: contents')
226
+ expect(container.textContent).toBe('const x = 123')
227
+ })
152
228
  })
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'
5
+ import { createRoot } from 'react-dom/client'
6
+ import { bench, describe } from 'vitest'
7
+
8
+ import { highlightCode, Highlighter } from '../src'
9
+
10
+ const page = readFileSync(resolve(process.cwd(), 'test/fixtures/large.html'), 'utf8')
11
+
12
+ /**
13
+ * Rendering ~90k spans into jsdom costs seconds per iteration, so the defaults
14
+ * (5 warmup + 10 timed runs) would take minutes. Keep the run bounded.
15
+ */
16
+ const RENDER_BENCH_OPTIONS = { time: 0, iterations: 5, warmupTime: 0, warmupIterations: 1 }
17
+
18
+ /**
19
+ * Resolves once the highlighted spans are in the document. The pending state is
20
+ * itself a single wrapper `<span>`, so waiting for *any* span would return
21
+ * immediately and measure nothing — hence the count.
22
+ */
23
+ const waitForHighlight = (host: HTMLElement) =>
24
+ new Promise<void>(resolve => {
25
+ const poll = () => (host.querySelectorAll('span').length > 1 ? resolve() : setTimeout(poll, 5))
26
+ poll()
27
+ })
28
+
29
+ describe(`html highlighting (${(page.length / 1024).toFixed(0)}KB)`, () => {
30
+ bench('highlightCode: parse + tokenize', async () => {
31
+ await highlightCode(
32
+ 'html',
33
+ page,
34
+ oneDarkHighlightStyle,
35
+ undefined,
36
+ undefined,
37
+ (text, style, from, to) => ({ text, style, from, to })
38
+ )
39
+ })
40
+
41
+ // Rendered through createRoot rather than @testing-library/react so the
42
+ // measurement excludes act() overhead and each iteration starts on an empty DOM.
43
+ bench(
44
+ 'Highlighter: parse + tokenize + React render into jsdom',
45
+ async () => {
46
+ const host = document.createElement('div')
47
+ document.body.appendChild(host)
48
+ const root = createRoot(host)
49
+
50
+ root.render(
51
+ <code>
52
+ <Highlighter lang="html" theme={oneDarkHighlightStyle}>
53
+ {page}
54
+ </Highlighter>
55
+ </code>
56
+ )
57
+ await waitForHighlight(host)
58
+
59
+ root.unmount()
60
+ host.remove()
61
+ },
62
+ RENDER_BENCH_OPTIONS
63
+ )
64
+ })
@@ -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/tsconfig.json CHANGED
@@ -15,6 +15,8 @@
15
15
  "outDir": "dist",
16
16
  "declaration": true,
17
17
  "declarationDir": "dist",
18
+ "noUncheckedIndexedAccess": true,
19
+ "exactOptionalPropertyTypes": true,
18
20
  "skipLibCheck": true
19
21
  }
20
22
  }
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
- )