Skip to content

Commit 1faa542

Browse files
feat: add latex feature for crepe (#1613)
* feat: add latex feature for crepe * [autofix.ci] apply automated fixes * feat: scaffold inline latex tooltip * feat: add inline latex editor * [autofix.ci] apply automated fixes * chore: add inline math input rule * feat: add input rule for math block * chore: polish style * chore: fix * chore: fix style * feat: add inline math tooltip button * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 5688083 commit 1faa542

File tree

27 files changed

+735
-74
lines changed

27 files changed

+735
-74
lines changed

Diff for: packages/components/src/code-block/config.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ import { withMeta } from '../__internal__/meta'
77
export interface CodeBlockConfig {
88
extensions: Extension[]
99
languages: LanguageDescription[]
10-
expandIcon: () => ReturnType<typeof html> | string | HTMLElement
11-
searchIcon: () => ReturnType<typeof html> | string | HTMLElement
12-
clearSearchIcon: () => ReturnType<typeof html> | string | HTMLElement
10+
expandIcon: () => ReturnType<typeof html> | string
11+
searchIcon: () => ReturnType<typeof html> | string
12+
clearSearchIcon: () => ReturnType<typeof html> | string
1313
searchPlaceholder: string
1414
noResultText: string
1515
renderLanguage: (
1616
language: string,
1717
selected: boolean
1818
) => ReturnType<typeof html>
19+
renderPreview: (
20+
language: string,
21+
content: string
22+
) => null | string | HTMLElement
23+
previewToggleButton: (previewOnlyMode: boolean) => ReturnType<typeof html>
24+
previewLabel: () => ReturnType<typeof html>
1925
}
2026

2127
export const defaultConfig: CodeBlockConfig = {
@@ -27,6 +33,9 @@ export const defaultConfig: CodeBlockConfig = {
2733
searchPlaceholder: 'Search language',
2834
noResultText: 'No result',
2935
renderLanguage: (language) => html`${language}`,
36+
renderPreview: () => null,
37+
previewToggleButton: (previewOnlyMode) => (previewOnlyMode ? 'Edit' : 'Hide'),
38+
previewLabel: () => 'Preview',
3039
}
3140

3241
export const codeBlockConfig = $ctx(defaultConfig, 'codeBlockConfigCtx')

Diff for: packages/components/src/code-block/view/component.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { CodeBlockConfig } from '../config'
1717
import type { LanguageInfo } from './loader'
1818

1919
export interface CodeComponentProps {
20+
text: string
2021
selected: boolean
2122
codemirror: CodeMirror
2223
language: string
@@ -34,13 +35,16 @@ export const codeComponent: Component<CodeComponentProps> = ({
3435
language,
3536
config,
3637
isEditorReadonly,
38+
text,
3739
}) => {
3840
const host = useHost()
3941
const triggerRef = useRef<HTMLButtonElement>()
4042
const pickerRef = useRef<HTMLDivElement>()
4143
const searchRef = useRef<HTMLInputElement>()
44+
const previewRef = useRef<HTMLDivElement>()
4245
const [filter, setFilter] = useState('')
4346
const [showPicker, setShowPicker] = useState(false)
47+
const [previewOnlyMode, setPreviewOnlyMode] = useState(false)
4448

4549
const root = useMemo(() => host.current.getRootNode() as HTMLElement, [host])
4650

@@ -182,6 +186,26 @@ export const codeComponent: Component<CodeComponentProps> = ({
182186
)
183187
}, [languages])
184188

189+
const preview = useMemo(() => {
190+
const preview = config?.renderPreview?.(language ?? '', text ?? '')
191+
return preview
192+
}, [language, text])
193+
194+
useEffect(() => {
195+
if (!previewRef.current) {
196+
return
197+
}
198+
199+
while (previewRef.current.firstChild) {
200+
previewRef.current.removeChild(previewRef.current.firstChild)
201+
}
202+
if (typeof preview === 'string') {
203+
previewRef.current.innerHTML = preview
204+
} else if (preview instanceof HTMLElement) {
205+
previewRef.current.appendChild(preview)
206+
}
207+
}, [preview])
208+
185209
return html`<host class=${clsx(selected && 'selected')}>
186210
<div class="tools">
187211
<button
@@ -222,8 +246,25 @@ export const codeComponent: Component<CodeComponentProps> = ({
222246
</ul>
223247
</div>
224248
</div>
249+
<button
250+
class=${clsx('preview-toggle-button', !preview && 'hidden')}
251+
onclick=${() => setPreviewOnlyMode(!previewOnlyMode)}
252+
>
253+
${config?.previewToggleButton?.(previewOnlyMode)}
254+
</button>
255+
</div>
256+
<div
257+
class=${clsx('codemirror-host', preview && previewOnlyMode && 'hidden')}
258+
>
259+
${h(codemirror?.dom, {})}
260+
</div>
261+
<div class=${clsx('preview-panel', !preview && 'hidden')}>
262+
<div class=${clsx('preview-divider', previewOnlyMode && 'hidden')}></div>
263+
<div class=${clsx('preview-label', previewOnlyMode && 'hidden')}>
264+
${config?.previewLabel?.()}
265+
</div>
266+
<div ref=${previewRef} class="preview"></div>
225267
</div>
226-
<div class="codemirror-host">${h(codemirror?.dom, {})}</div>
227268
</host>`
228269
}
229270

@@ -235,6 +276,7 @@ codeComponent.props = {
235276
setLanguage: Function,
236277
isEditorReadonly: Function,
237278
config: Object,
279+
text: String,
238280
}
239281

240282
export const CodeElement = c(codeComponent)

Diff for: packages/components/src/code-block/view/node-view.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ export class CodeMirrorBlock implements NodeView {
8484
dom.getAllLanguages = this.getAllLanguages
8585
dom.setLanguage = this.setLanguage
8686
dom.isEditorReadonly = () => !this.view.editable
87-
const { languages, extensions, ...viewConfig } = this.config
87+
dom.text = this.node.textContent
88+
const {
89+
languages: _languages,
90+
extensions: _extensions,
91+
...viewConfig
92+
} = this.config
8893
dom.config = viewConfig
8994
return dom
9095
}
@@ -191,6 +196,7 @@ export class CodeMirrorBlock implements NodeView {
191196
if (this.updating) return true
192197

193198
this.node = node
199+
this.dom.text = node.textContent
194200
this.updateLanguage()
195201
if (this.view.editable === this.cm.state.readOnly) {
196202
this.cm.dispatch({

Diff for: packages/components/src/link-tooltip/preview/preview-view.ts

-13
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ export class LinkPreviewTooltip implements PluginView {
1414

1515
#hovering = false
1616

17-
// get #instance() {
18-
// return this.#provider.getInstance()
19-
// }
20-
2117
constructor(
2218
readonly ctx: Ctx,
2319
view: EditorView
@@ -32,15 +28,6 @@ export class LinkPreviewTooltip implements PluginView {
3228
this.#slice.on(this.#onStateChange)
3329
}
3430

35-
// setRect = (rect: DOMRect) => {
36-
// // this.#provider.getInstance()?.setProps({
37-
// // getReferenceClientRect: () => rect,
38-
// // })
39-
// this.#provider.virtualElement = {
40-
// getBoundingClientRect: () => rect,
41-
// }
42-
// }
43-
4431
#onStateChange = ({ mode }: LinkToolTipState) => {
4532
if (mode === 'edit') this.#hide()
4633
}

Diff for: packages/crepe/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,17 @@
6464
"@codemirror/language": "^6.10.1",
6565
"@codemirror/language-data": "^6.3.1",
6666
"@codemirror/state": "^6.4.1",
67+
"@codemirror/theme-one-dark": "^6.1.2",
6768
"@codemirror/view": "^6.16.0",
6869
"@milkdown/kit": "workspace:*",
69-
"@codemirror/theme-one-dark": "^6.1.2",
7070
"atomico": "^1.75.1",
7171
"clsx": "^2.0.0",
7272
"codemirror": "^6.0.1",
73+
"katex": "^0.16.0",
7374
"nanoid": "^5.0.0",
74-
"tslib": "^2.5.0"
75+
"remark-math": "^6.0.0",
76+
"tslib": "^2.5.0",
77+
"unist-util-visit": "^5.0.0"
7578
},
7679
"nx": {
7780
"targets": {

Diff for: packages/crepe/src/feature/block-edit/handle/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function configureBlockHandle(
8888
ctx.set(blockConfig.key, {
8989
filterNodes: (pos) => {
9090
const filter = findParent((node) =>
91-
['table', 'blockquote'].includes(node.type.name)
91+
['table', 'blockquote', 'math_inline'].includes(node.type.name)
9292
)(pos)
9393
if (filter) return false
9494

Diff for: packages/crepe/src/feature/code-mirror/index.ts

+25-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import type { Extension } from '@codemirror/state'
77
import { basicSetup } from 'codemirror'
88
import { keymap } from '@codemirror/view'
99
import { defaultKeymap, indentWithTab } from '@codemirror/commands'
10-
import type { html } from 'atomico'
10+
import { html } from 'atomico'
1111
import type { DefineFeature, Icon } from '../shared'
12-
import { chevronDownIcon, clearIcon, searchIcon } from '../../icons'
12+
import { chevronDownIcon, clearIcon, editIcon, searchIcon } from '../../icons'
13+
import { visibilityOffIcon } from '../../icons/visibility-off'
1314

1415
interface CodeMirrorConfig {
1516
extensions: Extension[]
@@ -26,7 +27,16 @@ interface CodeMirrorConfig {
2627
renderLanguage: (
2728
language: string,
2829
selected: boolean
29-
) => ReturnType<typeof html> | string | HTMLElement
30+
) => ReturnType<typeof html> | string
31+
32+
renderPreview: (
33+
language: string,
34+
content: string
35+
) => string | HTMLElement | null
36+
37+
previewToggleIcon: (previewOnlyMode: boolean) => ReturnType<Icon>
38+
previewToggleText: (previewOnlyMode: boolean) => ReturnType<typeof html>
39+
previewLabel: () => ReturnType<typeof html>
3040
}
3141
export type CodeMirrorFeatureConfig = Partial<CodeMirrorConfig>
3242

@@ -62,6 +72,18 @@ export const defineFeature: DefineFeature<CodeMirrorFeatureConfig> = (
6272
searchPlaceholder: config.searchPlaceholder || 'Search language',
6373
noResultText: config.noResultText || 'No result',
6474
renderLanguage: config.renderLanguage || defaultConfig.renderLanguage,
75+
renderPreview: config.renderPreview || defaultConfig.renderPreview,
76+
previewToggleButton: (previewOnlyMode) => {
77+
return html`
78+
${config.previewToggleText?.(previewOnlyMode) || previewOnlyMode
79+
? editIcon
80+
: visibilityOffIcon}
81+
${config.previewToggleIcon?.(previewOnlyMode) || previewOnlyMode
82+
? 'Edit'
83+
: 'Hide'}
84+
`
85+
},
86+
previewLabel: config.previewLabel || defaultConfig.previewLabel,
6587
}))
6688
})
6789
.use(codeBlockComponent)

Diff for: packages/crepe/src/feature/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { LinkTooltipFeatureConfig } from './link-tooltip'
88
import type { ListItemFeatureConfig } from './list-item'
99
import type { ToolbarFeatureConfig } from './toolbar'
1010
import type { TableFeatureConfig } from './table'
11+
import type { LatexFeatureConfig } from './latex'
1112

1213
export enum CrepeFeature {
1314
CodeMirror = 'code-mirror',
@@ -19,6 +20,7 @@ export enum CrepeFeature {
1920
Toolbar = 'toolbar',
2021
Placeholder = 'placeholder',
2122
Table = 'table',
23+
Latex = 'latex',
2224
}
2325

2426
export interface CrepeFeatureConfig {
@@ -31,6 +33,7 @@ export interface CrepeFeatureConfig {
3133
[CrepeFeature.Toolbar]?: ToolbarFeatureConfig
3234
[CrepeFeature.CodeMirror]?: CodeMirrorFeatureConfig
3335
[CrepeFeature.Table]?: TableFeatureConfig
36+
[CrepeFeature.Latex]?: LatexFeatureConfig
3437
}
3538

3639
export const defaultFeatures: Record<CrepeFeature, boolean> = {
@@ -43,6 +46,7 @@ export const defaultFeatures: Record<CrepeFeature, boolean> = {
4346
[CrepeFeature.Toolbar]: true,
4447
[CrepeFeature.CodeMirror]: true,
4548
[CrepeFeature.Table]: true,
49+
[CrepeFeature.Latex]: true,
4650
}
4751

4852
export async function loadFeature(
@@ -87,5 +91,9 @@ export async function loadFeature(
8791
const { defineFeature } = await import('./table')
8892
return defineFeature(editor, config)
8993
}
94+
case CrepeFeature.Latex: {
95+
const { defineFeature } = await import('./latex')
96+
return defineFeature(editor, config)
97+
}
9098
}
9199
}

Diff for: packages/crepe/src/feature/latex/index.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { KatexOptions } from 'katex'
2+
import katex from 'katex'
3+
import { codeBlockConfig } from '@milkdown/kit/component/code-block'
4+
import { CrepeFeature } from '../..'
5+
import { FeaturesCtx } from '../../core/slice'
6+
import type { DefineFeature, Icon } from '../shared'
7+
import { remarkMathBlockPlugin, remarkMathPlugin } from './remark'
8+
import { mathInlineSchema } from './inline-latex'
9+
import { defIfNotExists } from '../../utils'
10+
import { LatexInlineEditElement } from './inline-tooltip/component'
11+
import { inlineLatexTooltip } from './inline-tooltip/tooltip'
12+
import { LatexInlineTooltip } from './inline-tooltip/view'
13+
import { confirmIcon } from '../../icons'
14+
import { mathBlockInputRule, mathInlineInputRule } from './input-rule'
15+
16+
export interface LatexConfig {
17+
katexOptions: KatexOptions
18+
inlineEditConfirm: Icon
19+
}
20+
21+
export type LatexFeatureConfig = Partial<LatexConfig>
22+
23+
defIfNotExists('milkdown-latex-inline-edit', LatexInlineEditElement)
24+
export const defineFeature: DefineFeature<LatexFeatureConfig> = (
25+
editor,
26+
config
27+
) => {
28+
editor
29+
.config((ctx) => {
30+
const flags = ctx.get(FeaturesCtx)
31+
const isCodeMirrorEnabled = flags.includes(CrepeFeature.CodeMirror)
32+
if (!isCodeMirrorEnabled) {
33+
throw new Error('You need to enable CodeMirror to use LaTeX feature')
34+
}
35+
36+
ctx.update(codeBlockConfig.key, (prev) => ({
37+
...prev,
38+
renderPreview: (language, content) => {
39+
if (language.toLowerCase() === 'latex' && content.length > 0) {
40+
return renderLatex(content, config?.katexOptions)
41+
}
42+
const renderPreview = prev.renderPreview
43+
return renderPreview(language, content)
44+
},
45+
}))
46+
47+
ctx.set(inlineLatexTooltip.key, {
48+
view: (view) => {
49+
return new LatexInlineTooltip(ctx, view, {
50+
inlineEditConfirm: config?.inlineEditConfirm ?? (() => confirmIcon),
51+
...config,
52+
})
53+
},
54+
})
55+
})
56+
.use(remarkMathPlugin)
57+
.use(remarkMathBlockPlugin)
58+
.use(mathInlineSchema)
59+
.use(inlineLatexTooltip)
60+
.use(mathInlineInputRule)
61+
.use(mathBlockInputRule)
62+
}
63+
64+
function renderLatex(content: string, options?: KatexOptions) {
65+
const html = katex.renderToString(content, {
66+
...options,
67+
throwOnError: false,
68+
displayMode: true,
69+
})
70+
return html
71+
}

0 commit comments

Comments
 (0)