@haklex/rich-renderer-codeblock 0.1.1 → 0.2.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/dist/index.mjs CHANGED
@@ -1,364 +1,330 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
2
- import { history, defaultKeymap, historyKeymap, indentWithTab } from "@codemirror/commands";
3
- import { Compartment, Prec, EditorState, EditorSelection } from "@codemirror/state";
1
+ import { getLanguageDisplayName, languageToColorMap, normalizeLanguage } from "./constants.mjs";
2
+ import { a as bodyBackground, c as card, f as lang, g as scroll, i as body, l as copyButton, n as LanguageIcon, o as bodyBackgroundStatic, p as langInput, r as hasLanguageIcon, v as semanticClassNames } from "./icons-CfRiv2YR.js";
3
+ import { CodeBlockRenderer } from "./static.mjs";
4
+ import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
5
+ import { Compartment, EditorSelection, EditorState, Prec } from "@codemirror/state";
4
6
  import { EditorView, keymap, lineNumbers } from "@codemirror/view";
5
- import { isOnFirstLine, isOnLastLine, getThemeExtensions, loadLanguageExtension } from "@haklex/cm-editor";
6
- import { useVariant, useColorScheme } from "@haklex/rich-editor";
7
- import { useState, useRef, useEffect, useCallback, useMemo } from "react";
8
- import { Check, Copy, ChevronDown } from "lucide-react";
9
- import { normalizeLanguage, getLanguageDisplayName, languageToColorMap } from "./constants.mjs";
10
- import "@iconify/utils";
11
- import "@iconify-json/material-icon-theme";
12
- import { c as card, s as semanticClassNames, l as lang, h as hasLanguageIcon, L as LanguageIcon, a as copyButton, b as bodyBackgroundStatic, d as bodyBackground, e as expandWrap, f as expandButton, g as scroll, i as scrollCollapsed, j as langInput, k as lined, m as linedWithNumbers, n as body, o as bodyReadonly } from "./language-kyYjCKCv.js";
13
- import { Combobox, ComboboxInput, ComboboxContent, ComboboxEmpty, ComboboxList, ComboboxItem } from "@haklex/rich-editor-ui";
7
+ import { getThemeExtensions, isOnFirstLine, isOnLastLine, loadLanguageExtension } from "@haklex/cm-editor";
8
+ import { useColorScheme, useVariant } from "@haklex/rich-editor";
9
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
+ import { Check, ChevronDown, Copy } from "lucide-react";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "@haklex/rich-editor-ui";
14
13
  import { bundledLanguagesInfo } from "shiki/bundle/web";
15
- import { CodeBlockRenderer } from "./static.mjs";
16
- const CopyIcon = /* @__PURE__ */ jsx(Copy, { size: 16 });
17
- const CheckIcon = /* @__PURE__ */ jsx(Check, { size: 16 });
18
- const ExpandIcon = /* @__PURE__ */ jsx(ChevronDown, { size: 14 });
19
- function CodeBlockCard({
20
- code,
21
- language,
22
- collapsible = true,
23
- langSlot,
24
- children,
25
- static: staticMode = false
26
- }) {
27
- const normalizedLanguage = normalizeLanguage(language);
28
- const [copied, setCopied] = useState(false);
29
- const copyTimerRef = useRef(void 0);
30
- const [isCollapsed, setIsCollapsed] = useState(true);
31
- const [isOverflow, setIsOverflow] = useState(false);
32
- const scrollRef = useRef(null);
33
- useEffect(() => {
34
- if (!collapsible) {
35
- setIsOverflow(false);
36
- return;
37
- }
38
- const el = scrollRef.current;
39
- if (!el) return;
40
- const check = () => {
41
- const halfVh = window.innerHeight / 2;
42
- setIsOverflow(el.scrollHeight >= halfVh);
43
- };
44
- const raf = requestAnimationFrame(check);
45
- return () => cancelAnimationFrame(raf);
46
- }, [code, collapsible]);
47
- useEffect(() => {
48
- return () => clearTimeout(copyTimerRef.current);
49
- }, []);
50
- const handleCopy = useCallback(() => {
51
- navigator.clipboard.writeText(code).then(() => {
52
- setCopied(true);
53
- clearTimeout(copyTimerRef.current);
54
- copyTimerRef.current = setTimeout(() => setCopied(false), 2e3);
55
- }).catch(() => {
56
- });
57
- }, [code]);
58
- const languageLabel = getLanguageDisplayName(normalizedLanguage);
59
- const accent = languageToColorMap[normalizedLanguage] || "#737373";
60
- const cardStyle = useMemo(() => ({ "--rr-code-accent": accent }), [accent]);
61
- const scrollClassName = [
62
- scroll,
63
- semanticClassNames.scroll,
64
- collapsible && isCollapsed && isOverflow && scrollCollapsed,
65
- collapsible && isCollapsed && isOverflow && semanticClassNames.scrollCollapsed
66
- ].filter(Boolean).join(" ");
67
- return /* @__PURE__ */ jsxs("div", { className: `${card} ${semanticClassNames.card}`, style: cardStyle, children: [
68
- langSlot ?? (normalizedLanguage !== "text" && /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: `${lang} ${semanticClassNames.lang}`, children: hasLanguageIcon(normalizedLanguage) ? /* @__PURE__ */ jsx(LanguageIcon, { language: normalizedLanguage, size: 14 }) : /* @__PURE__ */ jsx("span", { children: languageLabel }) })),
69
- /* @__PURE__ */ jsx(
70
- "button",
71
- {
72
- "aria-label": copied ? "Copied" : "Copy code",
73
- className: `${copyButton} ${semanticClassNames.copyButton}`,
74
- type: "button",
75
- onClick: handleCopy,
76
- children: copied ? CheckIcon : CopyIcon
77
- }
78
- ),
79
- /* @__PURE__ */ jsxs(
80
- "div",
81
- {
82
- className: `${staticMode ? bodyBackgroundStatic : bodyBackground} ${semanticClassNames.bodyBackground}`,
83
- children: [
84
- /* @__PURE__ */ jsx("div", { className: scrollClassName, ref: scrollRef, children }),
85
- collapsible && isOverflow && isCollapsed && /* @__PURE__ */ jsx("div", { className: `${expandWrap} ${semanticClassNames.expandWrap}`, children: /* @__PURE__ */ jsxs(
86
- "button",
87
- {
88
- className: `${expandButton} ${semanticClassNames.expandButton}`,
89
- type: "button",
90
- onClick: () => setIsCollapsed(false),
91
- children: [
92
- ExpandIcon,
93
- /* @__PURE__ */ jsx("span", { children: "展开" })
94
- ]
95
- }
96
- ) })
97
- ]
98
- }
99
- )
100
- ] });
14
+ //#region src/CodeBlockCard.tsx
15
+ var CopyIcon = /* @__PURE__ */ jsx(Copy, { size: 16 });
16
+ var CheckIcon = /* @__PURE__ */ jsx(Check, { size: 16 });
17
+ var ExpandIcon = /* @__PURE__ */ jsx(ChevronDown, { size: 14 });
18
+ function CodeBlockCard({ code, language, collapsible = true, langSlot, children, static: staticMode = false }) {
19
+ const normalizedLanguage = normalizeLanguage(language);
20
+ const [copied, setCopied] = useState(false);
21
+ const copyTimerRef = useRef(void 0);
22
+ const [isCollapsed, setIsCollapsed] = useState(true);
23
+ const [isOverflow, setIsOverflow] = useState(false);
24
+ const scrollRef = useRef(null);
25
+ useEffect(() => {
26
+ if (!collapsible) {
27
+ setIsOverflow(false);
28
+ return;
29
+ }
30
+ const el = scrollRef.current;
31
+ if (!el) return;
32
+ const check = () => {
33
+ const halfVh = window.innerHeight / 2;
34
+ setIsOverflow(el.scrollHeight >= halfVh);
35
+ };
36
+ const raf = requestAnimationFrame(check);
37
+ return () => cancelAnimationFrame(raf);
38
+ }, [code, collapsible]);
39
+ useEffect(() => {
40
+ return () => clearTimeout(copyTimerRef.current);
41
+ }, []);
42
+ const handleCopy = useCallback(() => {
43
+ navigator.clipboard.writeText(code).then(() => {
44
+ setCopied(true);
45
+ clearTimeout(copyTimerRef.current);
46
+ copyTimerRef.current = setTimeout(() => setCopied(false), 2e3);
47
+ }).catch(() => {});
48
+ }, [code]);
49
+ const languageLabel = getLanguageDisplayName(normalizedLanguage);
50
+ const accent = languageToColorMap[normalizedLanguage] || "#737373";
51
+ const cardStyle = useMemo(() => ({ "--rr-code-accent": accent }), [accent]);
52
+ const scrollClassName = [
53
+ scroll,
54
+ semanticClassNames.scroll,
55
+ collapsible && isCollapsed && isOverflow && "_1pn9r4q8",
56
+ collapsible && isCollapsed && isOverflow && semanticClassNames.scrollCollapsed
57
+ ].filter(Boolean).join(" ");
58
+ return /* @__PURE__ */ jsxs("div", {
59
+ className: `${card} ${semanticClassNames.card}`,
60
+ style: cardStyle,
61
+ children: [
62
+ langSlot ?? (normalizedLanguage !== "text" && /* @__PURE__ */ jsx("div", {
63
+ "aria-hidden": true,
64
+ className: `_1pn9r4q2 ${semanticClassNames.lang}`,
65
+ children: hasLanguageIcon(normalizedLanguage) ? /* @__PURE__ */ jsx(LanguageIcon, {
66
+ language: normalizedLanguage,
67
+ size: 14
68
+ }) : /* @__PURE__ */ jsx("span", { children: languageLabel })
69
+ })),
70
+ /* @__PURE__ */ jsx("button", {
71
+ "aria-label": copied ? "Copied" : "Copy code",
72
+ className: `${copyButton} ${semanticClassNames.copyButton}`,
73
+ type: "button",
74
+ onClick: handleCopy,
75
+ children: copied ? CheckIcon : CopyIcon
76
+ }),
77
+ /* @__PURE__ */ jsxs("div", {
78
+ className: `${staticMode ? bodyBackgroundStatic : bodyBackground} ${semanticClassNames.bodyBackground}`,
79
+ children: [/* @__PURE__ */ jsx("div", {
80
+ className: scrollClassName,
81
+ ref: scrollRef,
82
+ children
83
+ }), collapsible && isOverflow && isCollapsed && /* @__PURE__ */ jsx("div", {
84
+ className: `_1pn9r4qb ${semanticClassNames.expandWrap}`,
85
+ children: /* @__PURE__ */ jsxs("button", {
86
+ className: `_1pn9r4qc ${semanticClassNames.expandButton}`,
87
+ type: "button",
88
+ onClick: () => setIsCollapsed(false),
89
+ children: [ExpandIcon, /* @__PURE__ */ jsx("span", { children: "展开" })]
90
+ })
91
+ })]
92
+ })
93
+ ]
94
+ });
101
95
  }
102
- const languageItems = bundledLanguagesInfo.map((info) => ({
103
- id: info.id,
104
- name: info.name
96
+ //#endregion
97
+ //#region src/LanguageCombobox.tsx
98
+ var languageItems = bundledLanguagesInfo.map((info) => ({
99
+ id: info.id,
100
+ name: info.name
105
101
  }));
106
102
  function LanguageCombobox({ language, onLanguageChange }) {
107
- const normalizedLanguage = normalizeLanguage(language);
108
- const handleValueChange = useCallback(
109
- (value) => {
110
- if (value) {
111
- onLanguageChange?.(value.id);
112
- }
113
- },
114
- [onLanguageChange]
115
- );
116
- const selectedValue = useMemo(
117
- () => languageItems.find((item) => item.id === normalizedLanguage) ?? null,
118
- [normalizedLanguage]
119
- );
120
- return /* @__PURE__ */ jsxs("div", { className: `${lang} ${semanticClassNames.lang}`, children: [
121
- hasLanguageIcon(normalizedLanguage) && /* @__PURE__ */ jsx(LanguageIcon, { language: normalizedLanguage, size: 14 }),
122
- /* @__PURE__ */ jsxs(
123
- Combobox,
124
- {
125
- itemToStringLabel: (item) => item.id,
126
- itemToStringValue: (item) => item.id,
127
- items: languageItems,
128
- value: selectedValue,
129
- onValueChange: handleValueChange,
130
- children: [
131
- /* @__PURE__ */ jsx(
132
- ComboboxInput,
133
- {
134
- className: langInput,
135
- placeholder: "language",
136
- onKeyDown: (e) => {
137
- e.stopPropagation();
138
- }
139
- }
140
- ),
141
- /* @__PURE__ */ jsxs(ComboboxContent, { align: "end", side: "top", sideOffset: 8, children: [
142
- /* @__PURE__ */ jsx(ComboboxEmpty, { children: "No languages found." }),
143
- /* @__PURE__ */ jsx(ComboboxList, { children: (item) => /* @__PURE__ */ jsxs(ComboboxItem, { value: item, children: [
144
- /* @__PURE__ */ jsx(LanguageIcon, { language: item.id, size: 16 }),
145
- item.name
146
- ] }, item.id) })
147
- ] })
148
- ]
149
- }
150
- )
151
- ] });
103
+ const normalizedLanguage = normalizeLanguage(language);
104
+ const handleValueChange = useCallback((value) => {
105
+ if (value) onLanguageChange?.(value.id);
106
+ }, [onLanguageChange]);
107
+ const selectedValue = useMemo(() => languageItems.find((item) => item.id === normalizedLanguage) ?? null, [normalizedLanguage]);
108
+ return /* @__PURE__ */ jsxs("div", {
109
+ className: `${lang} ${semanticClassNames.lang}`,
110
+ children: [hasLanguageIcon(normalizedLanguage) && /* @__PURE__ */ jsx(LanguageIcon, {
111
+ language: normalizedLanguage,
112
+ size: 14
113
+ }), /* @__PURE__ */ jsxs(Combobox, {
114
+ itemToStringLabel: (item) => item.id,
115
+ itemToStringValue: (item) => item.id,
116
+ items: languageItems,
117
+ value: selectedValue,
118
+ onValueChange: handleValueChange,
119
+ children: [/* @__PURE__ */ jsx(ComboboxInput, {
120
+ className: langInput,
121
+ placeholder: "language",
122
+ onKeyDown: (e) => {
123
+ e.stopPropagation();
124
+ }
125
+ }), /* @__PURE__ */ jsxs(ComboboxContent, {
126
+ align: "end",
127
+ side: "top",
128
+ sideOffset: 8,
129
+ children: [/* @__PURE__ */ jsx(ComboboxEmpty, { children: "No languages found." }), /* @__PURE__ */ jsx(ComboboxList, { children: (item) => /* @__PURE__ */ jsxs(ComboboxItem, {
130
+ value: item,
131
+ children: [/* @__PURE__ */ jsx(LanguageIcon, {
132
+ language: item.id,
133
+ size: 16
134
+ }), item.name]
135
+ }, item.id) })]
136
+ })]
137
+ })]
138
+ });
152
139
  }
140
+ //#endregion
141
+ //#region src/CodeBlockEditRenderer.tsx
153
142
  function stopHandledEvent(event) {
154
- event.preventDefault();
155
- event.stopPropagation();
156
- event.stopImmediatePropagation();
143
+ event.preventDefault();
144
+ event.stopPropagation();
145
+ event.stopImmediatePropagation();
157
146
  }
158
- const CodeBlockEditRenderer = ({
159
- code,
160
- language,
161
- showLineNumbers: showLineNumbersProp,
162
- editable = false,
163
- selected = false,
164
- cursorPlacement = "start",
165
- onCodeChange,
166
- onLanguageChange,
167
- onDelete,
168
- onExitBlock
169
- }) => {
170
- const variant = useVariant();
171
- const colorScheme = useColorScheme();
172
- const showLineNumbers = showLineNumbersProp ?? variant !== "comment";
173
- const normalizedLanguage = normalizeLanguage(language);
174
- const [mounted, setMounted] = useState(false);
175
- const containerRef = useRef(null);
176
- const editorRef = useRef(null);
177
- const suppressChangeRef = useRef(false);
178
- const editableRef = useRef(editable);
179
- editableRef.current = editable;
180
- const onCodeChangeRef = useRef(onCodeChange);
181
- onCodeChangeRef.current = onCodeChange;
182
- const onDeleteRef = useRef(onDelete);
183
- onDeleteRef.current = onDelete;
184
- const onExitBlockRef = useRef(onExitBlock);
185
- onExitBlockRef.current = onExitBlock;
186
- const languageCompartmentRef = useRef(null);
187
- const editableCompartmentRef = useRef(null);
188
- const lineNumbersCompartmentRef = useRef(null);
189
- const themeCompartmentRef = useRef(null);
190
- if (!languageCompartmentRef.current) languageCompartmentRef.current = new Compartment();
191
- if (!editableCompartmentRef.current) editableCompartmentRef.current = new Compartment();
192
- if (!lineNumbersCompartmentRef.current) lineNumbersCompartmentRef.current = new Compartment();
193
- if (!themeCompartmentRef.current) themeCompartmentRef.current = new Compartment();
194
- const keyboardBoundaryHandler = useMemo(
195
- () => Prec.high(
196
- EditorView.domEventHandlers({
197
- keydown: (event, view) => {
198
- if (!editableRef.current) return false;
199
- event.stopPropagation();
200
- if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
201
- stopHandledEvent(event);
202
- onExitBlockRef.current?.("after");
203
- return true;
204
- }
205
- if (event.key === "Backspace" && view.state.doc.length === 0) {
206
- stopHandledEvent(event);
207
- onDeleteRef.current?.();
208
- return true;
209
- }
210
- if (event.key === "ArrowUp" && isOnFirstLine(view)) {
211
- stopHandledEvent(event);
212
- onExitBlockRef.current?.("before");
213
- return true;
214
- }
215
- if (event.key === "ArrowDown" && isOnLastLine(view)) {
216
- stopHandledEvent(event);
217
- onExitBlockRef.current?.("after");
218
- return true;
219
- }
220
- return false;
221
- }
222
- })
223
- ),
224
- []
225
- );
226
- useEffect(() => {
227
- const container = containerRef.current;
228
- if (!container) return;
229
- const editor = new EditorView({
230
- parent: container,
231
- state: EditorState.create({
232
- doc: code,
233
- extensions: [
234
- history(),
235
- keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
236
- EditorView.updateListener.of((update) => {
237
- if (!update.docChanged || suppressChangeRef.current) return;
238
- onCodeChangeRef.current?.(update.state.doc.toString());
239
- }),
240
- keyboardBoundaryHandler,
241
- editableCompartmentRef.current.of([
242
- EditorView.editable.of(editable),
243
- EditorState.readOnly.of(!editable)
244
- ]),
245
- lineNumbersCompartmentRef.current.of(showLineNumbers ? lineNumbers() : []),
246
- themeCompartmentRef.current.of(getThemeExtensions(colorScheme)),
247
- languageCompartmentRef.current.of([])
248
- ]
249
- })
250
- });
251
- editorRef.current = editor;
252
- setMounted(true);
253
- return () => {
254
- editor.destroy();
255
- editorRef.current = null;
256
- setMounted(false);
257
- };
258
- }, []);
259
- useEffect(() => {
260
- const editor = editorRef.current;
261
- if (!editor) return;
262
- editor.dispatch({
263
- effects: editableCompartmentRef.current.reconfigure([
264
- EditorView.editable.of(editable),
265
- EditorState.readOnly.of(!editable)
266
- ])
267
- });
268
- }, [editable]);
269
- useEffect(() => {
270
- const editor = editorRef.current;
271
- if (!editor) return;
272
- editor.dispatch({
273
- effects: lineNumbersCompartmentRef.current.reconfigure(showLineNumbers ? lineNumbers() : [])
274
- });
275
- }, [showLineNumbers]);
276
- useEffect(() => {
277
- const editor = editorRef.current;
278
- if (!editor) return;
279
- editor.dispatch({
280
- effects: themeCompartmentRef.current.reconfigure(getThemeExtensions(colorScheme))
281
- });
282
- }, [colorScheme]);
283
- useEffect(() => {
284
- const editor = editorRef.current;
285
- if (!editor) return;
286
- const current = editor.state.doc.toString();
287
- if (current === code) return;
288
- suppressChangeRef.current = true;
289
- editor.dispatch({
290
- changes: {
291
- from: 0,
292
- to: current.length,
293
- insert: code
294
- }
295
- });
296
- suppressChangeRef.current = false;
297
- }, [code]);
298
- useEffect(() => {
299
- const editor = editorRef.current;
300
- if (!editor) return;
301
- let cancelled = false;
302
- (async () => {
303
- const extension = await loadLanguageExtension(normalizedLanguage);
304
- if (cancelled) return;
305
- editor.dispatch({
306
- effects: languageCompartmentRef.current.reconfigure(extension)
307
- });
308
- })();
309
- return () => {
310
- cancelled = true;
311
- };
312
- }, [normalizedLanguage]);
313
- useEffect(() => {
314
- if (!editable || !selected) return;
315
- const editor = editorRef.current;
316
- if (!editor) return;
317
- const raf = requestAnimationFrame(() => {
318
- const nextCursor = cursorPlacement === "end" ? editor.state.doc.length : 0;
319
- editor.focus();
320
- editor.dispatch({
321
- selection: EditorSelection.cursor(nextCursor),
322
- scrollIntoView: true
323
- });
324
- });
325
- return () => cancelAnimationFrame(raf);
326
- }, [cursorPlacement, editable, mounted, selected]);
327
- const fallbackLines = useMemo(() => code.split("\n"), [code]);
328
- const fallbackClassName = [
329
- showLineNumbers && lined,
330
- showLineNumbers && semanticClassNames.lined,
331
- showLineNumbers && linedWithNumbers,
332
- showLineNumbers && semanticClassNames.linedWithNumbers
333
- ].filter(Boolean).join(" ");
334
- const bodyClassName = [
335
- body,
336
- semanticClassNames.body,
337
- !editable && bodyReadonly,
338
- !editable && semanticClassNames.bodyReadonly
339
- ].filter(Boolean).join(" ");
340
- return /* @__PURE__ */ jsxs(
341
- CodeBlockCard,
342
- {
343
- code,
344
- collapsible: !editable,
345
- language,
346
- langSlot: editable ? /* @__PURE__ */ jsx(LanguageCombobox, { language, onLanguageChange }) : void 0,
347
- children: [
348
- !mounted && /* @__PURE__ */ jsx("pre", { className: fallbackClassName, children: /* @__PURE__ */ jsx("code", { children: fallbackLines.map((line, i) => /* @__PURE__ */ jsx("span", { className: "line", children: line }, `${line}-${i}`)) }) }),
349
- /* @__PURE__ */ jsx(
350
- "div",
351
- {
352
- className: bodyClassName,
353
- ref: containerRef,
354
- style: !mounted ? { height: 0, overflow: "hidden", visibility: "hidden" } : void 0
355
- }
356
- )
357
- ]
358
- }
359
- );
360
- };
361
- export {
362
- CodeBlockEditRenderer,
363
- CodeBlockRenderer
147
+ var CodeBlockEditRenderer = ({ code, language, showLineNumbers: showLineNumbersProp, editable = false, selected = false, cursorPlacement = "start", onCodeChange, onLanguageChange, onDelete, onExitBlock }) => {
148
+ const variant = useVariant();
149
+ const colorScheme = useColorScheme();
150
+ const showLineNumbers = showLineNumbersProp ?? variant !== "comment";
151
+ const normalizedLanguage = normalizeLanguage(language);
152
+ const [mounted, setMounted] = useState(false);
153
+ const containerRef = useRef(null);
154
+ const editorRef = useRef(null);
155
+ const suppressChangeRef = useRef(false);
156
+ const editableRef = useRef(editable);
157
+ editableRef.current = editable;
158
+ const onCodeChangeRef = useRef(onCodeChange);
159
+ onCodeChangeRef.current = onCodeChange;
160
+ const onDeleteRef = useRef(onDelete);
161
+ onDeleteRef.current = onDelete;
162
+ const onExitBlockRef = useRef(onExitBlock);
163
+ onExitBlockRef.current = onExitBlock;
164
+ const languageCompartmentRef = useRef(null);
165
+ const editableCompartmentRef = useRef(null);
166
+ const lineNumbersCompartmentRef = useRef(null);
167
+ const themeCompartmentRef = useRef(null);
168
+ if (!languageCompartmentRef.current) languageCompartmentRef.current = new Compartment();
169
+ if (!editableCompartmentRef.current) editableCompartmentRef.current = new Compartment();
170
+ if (!lineNumbersCompartmentRef.current) lineNumbersCompartmentRef.current = new Compartment();
171
+ if (!themeCompartmentRef.current) themeCompartmentRef.current = new Compartment();
172
+ const keyboardBoundaryHandler = useMemo(() => Prec.high(EditorView.domEventHandlers({ keydown: (event, view) => {
173
+ if (!editableRef.current) return false;
174
+ event.stopPropagation();
175
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
176
+ stopHandledEvent(event);
177
+ onExitBlockRef.current?.("after");
178
+ return true;
179
+ }
180
+ if (event.key === "Backspace" && view.state.doc.length === 0) {
181
+ stopHandledEvent(event);
182
+ onDeleteRef.current?.();
183
+ return true;
184
+ }
185
+ if (event.key === "ArrowUp" && isOnFirstLine(view)) {
186
+ stopHandledEvent(event);
187
+ onExitBlockRef.current?.("before");
188
+ return true;
189
+ }
190
+ if (event.key === "ArrowDown" && isOnLastLine(view)) {
191
+ stopHandledEvent(event);
192
+ onExitBlockRef.current?.("after");
193
+ return true;
194
+ }
195
+ return false;
196
+ } })), []);
197
+ useEffect(() => {
198
+ const container = containerRef.current;
199
+ if (!container) return;
200
+ const editor = new EditorView({
201
+ parent: container,
202
+ state: EditorState.create({
203
+ doc: code,
204
+ extensions: [
205
+ history(),
206
+ keymap.of([
207
+ ...defaultKeymap,
208
+ ...historyKeymap,
209
+ indentWithTab
210
+ ]),
211
+ EditorView.updateListener.of((update) => {
212
+ if (!update.docChanged || suppressChangeRef.current) return;
213
+ onCodeChangeRef.current?.(update.state.doc.toString());
214
+ }),
215
+ keyboardBoundaryHandler,
216
+ editableCompartmentRef.current.of([EditorView.editable.of(editable), EditorState.readOnly.of(!editable)]),
217
+ lineNumbersCompartmentRef.current.of(showLineNumbers ? lineNumbers() : []),
218
+ themeCompartmentRef.current.of(getThemeExtensions(colorScheme)),
219
+ languageCompartmentRef.current.of([])
220
+ ]
221
+ })
222
+ });
223
+ editorRef.current = editor;
224
+ setMounted(true);
225
+ return () => {
226
+ editor.destroy();
227
+ editorRef.current = null;
228
+ setMounted(false);
229
+ };
230
+ }, []);
231
+ useEffect(() => {
232
+ const editor = editorRef.current;
233
+ if (!editor) return;
234
+ editor.dispatch({ effects: editableCompartmentRef.current.reconfigure([EditorView.editable.of(editable), EditorState.readOnly.of(!editable)]) });
235
+ }, [editable]);
236
+ useEffect(() => {
237
+ const editor = editorRef.current;
238
+ if (!editor) return;
239
+ editor.dispatch({ effects: lineNumbersCompartmentRef.current.reconfigure(showLineNumbers ? lineNumbers() : []) });
240
+ }, [showLineNumbers]);
241
+ useEffect(() => {
242
+ const editor = editorRef.current;
243
+ if (!editor) return;
244
+ editor.dispatch({ effects: themeCompartmentRef.current.reconfigure(getThemeExtensions(colorScheme)) });
245
+ }, [colorScheme]);
246
+ useEffect(() => {
247
+ const editor = editorRef.current;
248
+ if (!editor) return;
249
+ const current = editor.state.doc.toString();
250
+ if (current === code) return;
251
+ suppressChangeRef.current = true;
252
+ editor.dispatch({ changes: {
253
+ from: 0,
254
+ to: current.length,
255
+ insert: code
256
+ } });
257
+ suppressChangeRef.current = false;
258
+ }, [code]);
259
+ useEffect(() => {
260
+ const editor = editorRef.current;
261
+ if (!editor) return;
262
+ let cancelled = false;
263
+ (async () => {
264
+ const extension = await loadLanguageExtension(normalizedLanguage);
265
+ if (cancelled) return;
266
+ editor.dispatch({ effects: languageCompartmentRef.current.reconfigure(extension) });
267
+ })();
268
+ return () => {
269
+ cancelled = true;
270
+ };
271
+ }, [normalizedLanguage]);
272
+ useEffect(() => {
273
+ if (!editable || !selected) return;
274
+ const editor = editorRef.current;
275
+ if (!editor) return;
276
+ const raf = requestAnimationFrame(() => {
277
+ const nextCursor = cursorPlacement === "end" ? editor.state.doc.length : 0;
278
+ editor.focus();
279
+ editor.dispatch({
280
+ selection: EditorSelection.cursor(nextCursor),
281
+ scrollIntoView: true
282
+ });
283
+ });
284
+ return () => cancelAnimationFrame(raf);
285
+ }, [
286
+ cursorPlacement,
287
+ editable,
288
+ mounted,
289
+ selected
290
+ ]);
291
+ const fallbackLines = useMemo(() => code.split("\n"), [code]);
292
+ const fallbackClassName = [
293
+ showLineNumbers && "_1pn9r4qd",
294
+ showLineNumbers && semanticClassNames.lined,
295
+ showLineNumbers && "_1pn9r4qe",
296
+ showLineNumbers && semanticClassNames.linedWithNumbers
297
+ ].filter(Boolean).join(" ");
298
+ const bodyClassName = [
299
+ body,
300
+ semanticClassNames.body,
301
+ !editable && "_1pn9r4qa",
302
+ !editable && semanticClassNames.bodyReadonly
303
+ ].filter(Boolean).join(" ");
304
+ return /* @__PURE__ */ jsxs(CodeBlockCard, {
305
+ code,
306
+ collapsible: !editable,
307
+ language,
308
+ langSlot: editable ? /* @__PURE__ */ jsx(LanguageCombobox, {
309
+ language,
310
+ onLanguageChange
311
+ }) : void 0,
312
+ children: [!mounted && /* @__PURE__ */ jsx("pre", {
313
+ className: fallbackClassName,
314
+ children: /* @__PURE__ */ jsx("code", { children: fallbackLines.map((line, i) => /* @__PURE__ */ jsx("span", {
315
+ className: "line",
316
+ children: line
317
+ }, `${line}-${i}`)) })
318
+ }), /* @__PURE__ */ jsx("div", {
319
+ className: bodyClassName,
320
+ ref: containerRef,
321
+ style: !mounted ? {
322
+ height: 0,
323
+ overflow: "hidden",
324
+ visibility: "hidden"
325
+ } : void 0
326
+ })]
327
+ });
364
328
  };
329
+ //#endregion
330
+ export { CodeBlockEditRenderer, CodeBlockRenderer };