Skip to content

Commit a3070c8

Browse files
authored
Merge pull request #917 from Godefroy/feat/codemirror-language-support
feat: support pre-loaded language support in codeMirrorPlugin
2 parents 057c18c + e9ab593 commit a3070c8

5 files changed

Lines changed: 213 additions & 37 deletions

File tree

docs/code-blocks.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,60 @@ import { languages } from '@codemirror/language-data'
133133
codeMirrorPlugin({ codeBlockLanguages: languages })
134134
```
135135

136+
### Pre-loaded language support
137+
138+
When using the array format, you can provide a `support` property with a pre-loaded `LanguageSupport` instance. This is useful for languages that are not included in `@codemirror/language-data` and cannot be auto-loaded.
139+
140+
When `support` is provided for a language, it takes priority over auto-loading. Languages without a `support` property will still be auto-loaded as usual, unless `autoLoadLanguageSupport` is set to `false` (see below).
141+
142+
```tsx
143+
import { graphql } from 'cm6-graphql'
144+
145+
codeMirrorPlugin({
146+
codeBlockLanguages: [
147+
{ name: 'JavaScript', alias: ['js', 'javascript'] },
148+
{ name: 'CSS', alias: ['css'] },
149+
{ name: 'GraphQL', alias: ['graphql', 'gql'], support: graphql() }
150+
]
151+
})
152+
```
153+
154+
### Custom CodeMirror extensions
155+
156+
The `codeMirrorExtensions` option lets you pass additional CodeMirror extensions that will be applied to all code block editors. This can be used to add custom keymaps, themes, or other CodeMirror plugins.
157+
158+
```tsx
159+
import { keymap, EditorView } from '@codemirror/view'
160+
import { toggleLineComment } from '@codemirror/commands'
161+
162+
codeMirrorPlugin({
163+
codeBlockLanguages: { js: 'JavaScript', css: 'CSS' },
164+
codeMirrorExtensions: [
165+
EditorView.theme({
166+
'&': { backgroundColor: '#f5f5f5' },
167+
'.cm-gutters': { backgroundColor: '#e8e8e8 !important' }
168+
}),
169+
keymap.of([{
170+
key: 'Cmd-:', run: toggleLineComment
171+
}]),
172+
]
173+
})
174+
```
175+
176+
### Auto-loading language support
177+
178+
By default, the plugin dynamically loads language support from `@codemirror/language-data` when a code block's language is recognized. You can disable this by setting `autoLoadLanguageSupport` to `false`. This is useful if you provide all language support manually via the `support` property or through `codeMirrorExtensions`.
179+
180+
```tsx
181+
codeMirrorPlugin({
182+
codeBlockLanguages: [
183+
{ name: 'Python', alias: ['py', 'python'], support: python() },
184+
{ name: 'GraphQL', alias: ['graphql', 'gql'], support: graphql() }
185+
],
186+
autoLoadLanguageSupport: false
187+
})
188+
```
189+
136190
## Configuring the Sandpack editor
137191

138192
Compared to the code mirror editor, the Sandpack one is a bit more complex, as Sandpack needs to know the context of the code block in order to execute it correctly. Before diving in, it's good to [understand Sandpack configuration](https://sandpack.codesandbox.io/) itself. MDXEditor supports multiple Sandpack configurations, based on the meta data of the code block. To configure the supported presets, pass a `sandpackConfig` option in the plugin initialization. For more details, refer to the [SandpackConfig interface](../api/editor.sandpackconfig) and the [SandpackPreset interface](../api/editor.sandpackpreset).

src/examples/codemirror-languages.tsx

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,63 @@
11
import React from 'react'
2-
import { MDXEditor, codeBlockPlugin, codeMirrorPlugin, diffSourcePlugin, toolbarPlugin, DiffSourceToggleWrapper, UndoRedo } from '../'
2+
import { MDXEditor, codeBlockPlugin, codeMirrorPlugin } from '../'
33
import { languages } from '@codemirror/language-data'
4+
import { python } from '@codemirror/lang-python'
5+
import { EditorView, keymap } from '@codemirror/view'
6+
import { toggleLineComment } from '@codemirror/commands'
7+
8+
const simpleSampleMarkdown = `
9+
With \`js\` language:
10+
11+
\`\`\`js
12+
const x = 1
13+
\`\`\`
14+
15+
With \`ts\` language:
16+
17+
\`\`\`ts
18+
const z: number = 3
19+
\`\`\`
20+
`
21+
22+
const aliasSampleMarkdown = `
23+
With \`js\` language:
424
5-
const sampleMarkdown = `
625
\`\`\`js
726
const x = 1
827
\`\`\`
928
29+
With \`javascript\` (alias) language:
30+
1031
\`\`\`javascript
1132
const y = 2
1233
\`\`\`
1334
35+
With \`ts\` language:
36+
1437
\`\`\`ts
1538
const z: number = 3
1639
\`\`\`
1740
18-
\`\`\`css
19-
body { color: red; }
41+
With \`typescript\` (alias) language:
42+
43+
\`\`\`typescript
44+
const z: number = 3
45+
\`\`\`
46+
`
47+
48+
const supportSampleMarkdown = `
49+
JS is **not** loaded:
50+
51+
\`\`\`js
52+
const x = 1
53+
\`\`\`
54+
55+
Python support is loaded:
56+
57+
\`\`\`python
58+
def greet(name):
59+
message = f"Hello, {name}!"
60+
print(message)
2061
\`\`\`
2162
`
2263

@@ -28,19 +69,11 @@ export function CodeMirrorLanguageData() {
2869
return (
2970
<MDXEditor
3071
onChange={console.log}
31-
markdown={sampleMarkdown}
72+
markdown={aliasSampleMarkdown}
3273
plugins={[
3374
codeBlockPlugin(),
3475
codeMirrorPlugin({
3576
codeBlockLanguages: languages
36-
}),
37-
diffSourcePlugin(),
38-
toolbarPlugin({
39-
toolbarContents: () => (
40-
<DiffSourceToggleWrapper>
41-
<UndoRedo />
42-
</DiffSourceToggleWrapper>
43-
)
4477
})
4578
]}
4679
/>
@@ -55,23 +88,66 @@ export function CodeMirrorLanguageArray() {
5588
return (
5689
<MDXEditor
5790
onChange={console.log}
58-
markdown={sampleMarkdown}
91+
markdown={aliasSampleMarkdown}
92+
plugins={[
93+
codeBlockPlugin(),
94+
codeMirrorPlugin({
95+
codeBlockLanguages: [
96+
{ name: 'JavaScript', alias: ['js', 'javascript'] },
97+
{ name: 'TypeScript', alias: ['ts', 'typescript'] }
98+
]
99+
})
100+
]}
101+
/>
102+
)
103+
}
104+
105+
/**
106+
* Uses `CodeBlockLanguage[]` with pre-loaded `support` for Python.
107+
* JavaScript and CSS have no `support` and are auto-loaded as usual.
108+
*/
109+
export function CodeMirrorLanguageWithSupport() {
110+
return (
111+
<MDXEditor
112+
onChange={console.log}
113+
markdown={supportSampleMarkdown}
59114
plugins={[
60115
codeBlockPlugin(),
61116
codeMirrorPlugin({
62117
codeBlockLanguages: [
63118
{ name: 'JavaScript', alias: ['js', 'javascript'] },
64-
{ name: 'TypeScript', alias: ['ts', 'typescript'], extensions: ['ts', 'mts'] },
65-
{ name: 'CSS', alias: ['css'] }
119+
{ name: 'Python', alias: ['py', 'python'], support: python() }
120+
],
121+
// Disable auto-load to test support load is working well
122+
autoLoadLanguageSupport: false
123+
})
124+
]}
125+
/>
126+
)
127+
}
128+
129+
/**
130+
* Passes custom CodeMirror extensions to all code block editors.
131+
* Here we add a custom tab size and a custom theme via EditorView.theme.
132+
*/
133+
export function CodeMirrorExtensions() {
134+
return (
135+
<MDXEditor
136+
onChange={console.log}
137+
markdown={simpleSampleMarkdown}
138+
plugins={[
139+
codeBlockPlugin(),
140+
codeMirrorPlugin({
141+
codeBlockLanguages: { js: 'JavaScript', ts: 'TypeScript' },
142+
codeMirrorExtensions: [
143+
keymap.of([{ key: 'Cmd-:', run: toggleLineComment }]),
144+
EditorView.theme({
145+
'&': { backgroundColor: '#f5f5f5' },
146+
'.cm-content': { fontFamily: '"Fira Code", monospace' },
147+
'.cm-gutters': { backgroundColor: 'orange !important' },
148+
'.cm-activeLineGutter': { backgroundColor: 'yellow !important' }
149+
})
66150
]
67-
}),
68-
diffSourcePlugin(),
69-
toolbarPlugin({
70-
toolbarContents: () => (
71-
<DiffSourceToggleWrapper>
72-
<UndoRedo />
73-
</DiffSourceToggleWrapper>
74-
)
75151
})
76152
]}
77153
/>
@@ -85,11 +161,11 @@ export function CodeMirrorLanguageRecord() {
85161
return (
86162
<MDXEditor
87163
onChange={console.log}
88-
markdown={sampleMarkdown}
164+
markdown={aliasSampleMarkdown}
89165
plugins={[
90166
codeBlockPlugin(),
91167
codeMirrorPlugin({
92-
codeBlockLanguages: { jsx: 'JavaScript (React)', js: 'JavaScript', javascript: 'JavaScript', css: 'CSS' }
168+
codeBlockLanguages: { js: 'JavaScript', javascript: 'JavaScript', ts: 'TypeScript', typescript: 'TypeScript' }
93169
})
94170
]}
95171
/>

src/plugins/codemirror/CodeMirrorEditor.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,22 @@ export const CodeMirrorEditor = ({ language, nodeKey, code, focusEmitter }: Code
6464
if (readOnly) {
6565
extensions.push(EditorState.readOnly.of(true))
6666
}
67-
if (language !== '' && autoLoadLanguageSupport) {
68-
const languageData = languages.find((l) => {
69-
return l.name === language || l.alias.includes(language) || l.extensions.includes(language)
70-
})
71-
if (languageData) {
72-
try {
73-
const languageSupport = await languageData.load()
74-
extensions.push(languageSupport.extension)
75-
} catch (_e) {
76-
console.warn('failed to load language support for', language)
67+
if (language !== '') {
68+
const canonical = codeBlockLanguages.keyMap[language] ?? language
69+
const providedSupport = codeBlockLanguages.supportMap[canonical]
70+
if (providedSupport) {
71+
extensions.push(providedSupport.extension)
72+
} else if (autoLoadLanguageSupport) {
73+
const languageData = languages.find((l) => {
74+
return l.name === language || l.alias.includes(language) || l.extensions.includes(language)
75+
})
76+
if (languageData) {
77+
try {
78+
const languageSupport = await languageData.load()
79+
extensions.push(languageSupport.extension)
80+
} catch (_e) {
81+
console.warn('failed to load language support for', language)
82+
}
7783
}
7884
}
7985
}

src/plugins/codemirror/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Cell, Signal, map } from '@mdxeditor/gurx'
33
import { CodeBlockEditorDescriptor, appendCodeBlockEditorDescriptor$, insertCodeBlock$ } from '../codeblock'
44
import { CodeMirrorEditor } from './CodeMirrorEditor'
55
import { Extension } from '@codemirror/state'
6+
import { LanguageSupport } from '@codemirror/language'
67

78
/**
89
* @internal
@@ -21,6 +22,8 @@ export interface CodeBlockLanguage {
2122
alias?: readonly string[]
2223
/** File extensions associated with this language (e.g. `["js", "mjs"]`). */
2324
extensions?: readonly string[]
25+
/** Pre-loaded language support. When provided, this is used directly instead of auto-loading. */
26+
support?: LanguageSupport
2427
}
2528

2629
/**
@@ -32,6 +35,8 @@ export interface NormalizedCodeBlockLanguages {
3235
items: { value: string; label: string }[]
3336
/** Maps any known key (canonical, alias, extension) to the canonical key. */
3437
keyMap: Record<string, string>
38+
/** Maps canonical keys to pre-loaded language support, when provided. */
39+
supportMap: Record<string, LanguageSupport>
3540
}
3641

3742
/**
@@ -41,6 +46,7 @@ export interface NormalizedCodeBlockLanguages {
4146
export function normalizeCodeBlockLanguages(input: Record<string, string> | CodeBlockLanguage[]): NormalizedCodeBlockLanguages {
4247
const items: { value: string; label: string }[] = []
4348
const keyMap: Record<string, string> = {}
49+
const supportMap: Record<string, LanguageSupport> = {}
4450

4551
if (Array.isArray(input)) {
4652
for (const lang of input) {
@@ -60,6 +66,9 @@ export function normalizeCodeBlockLanguages(input: Record<string, string> | Code
6066
}
6167
// Also map the lowercased name
6268
keyMap[lang.name.toLowerCase()] = canonical
69+
if (lang.support) {
70+
supportMap[canonical] = lang.support
71+
}
6372
}
6473
} else {
6574
const firstKeyByLabel: Record<string, string> = {}
@@ -72,7 +81,7 @@ export function normalizeCodeBlockLanguages(input: Record<string, string> | Code
7281
}
7382
}
7483

75-
return { items, keyMap }
84+
return { items, keyMap, supportMap }
7685
}
7786

7887
/**

src/test/codemirror.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22
import { normalizeCodeBlockLanguages, EMPTY_VALUE } from '../plugins/codemirror'
3+
import { LanguageSupport } from '@codemirror/language'
34

45
describe('normalizeCodeBlockLanguages', () => {
56
describe('record format', () => {
@@ -70,6 +71,36 @@ describe('normalizeCodeBlockLanguages', () => {
7071
})
7172
})
7273

74+
describe('supportMap', () => {
75+
it('stores support keyed by canonical key', () => {
76+
const mockSupport = {} as LanguageSupport
77+
const result = normalizeCodeBlockLanguages([
78+
{ name: 'JavaScript', alias: ['js', 'javascript'], support: mockSupport }
79+
])
80+
expect(result.supportMap.js).toBe(mockSupport)
81+
})
82+
83+
it('does not add entry when support is not provided', () => {
84+
const result = normalizeCodeBlockLanguages([
85+
{ name: 'JavaScript', alias: ['js'] }
86+
])
87+
expect(result.supportMap).toEqual({})
88+
})
89+
90+
it('stores support using lowercased name when no aliases', () => {
91+
const mockSupport = {} as LanguageSupport
92+
const result = normalizeCodeBlockLanguages([
93+
{ name: 'Python', support: mockSupport }
94+
])
95+
expect(result.supportMap.python).toBe(mockSupport)
96+
})
97+
98+
it('is empty for record format', () => {
99+
const result = normalizeCodeBlockLanguages({ js: 'JavaScript' })
100+
expect(result.supportMap).toEqual({})
101+
})
102+
})
103+
73104
describe('empty input', () => {
74105
it('handles empty record', () => {
75106
const result = normalizeCodeBlockLanguages({})

0 commit comments

Comments
 (0)