@verbb/plugin-kit-codemirror-core 2.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 2.0.0 - 2026-07-19
6
+
7
+ ### Added
8
+ - Initial public release of `@verbb/plugin-kit-codemirror-core` — shared CodeMirror setup for Plugin Kit code-editor surfaces.
9
+
10
+ ### Changed
11
+ - Package versions lockstep with `@verbb/plugin-kit-web` and related `@verbb/plugin-kit-*` packages at `2.0.0`.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Verbb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # `@verbb/plugin-kit-codemirror-core`
2
+
3
+ Shared CodeMirror 6 setup for Plugin Kit code-editor surfaces — languages, theme chrome, and a small host helper.
4
+
5
+ You normally consume this through `<pk-code-editor>` / React `CodeEditor` / Vue `CodeEditor`. Use this package when you need the extensions or host without the web component.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @verbb/plugin-kit-codemirror-core
11
+ ```
12
+
13
+ ## What you get
14
+
15
+ | Export | Purpose |
16
+ |--------|---------|
17
+ | `createCodeEditorExtensions` | Language + chrome extensions for a given mode |
18
+ | `createLanguageExtension` | Single-language helper |
19
+ | `codeEditorTheme` / colour constants | Shared CP-looking editor chrome |
20
+ | `CodeMirrorHost` | Mount / teardown helper for a CodeMirror view |
21
+ | `computeCodeEditorMinHeight` | Layout helper from line count |
22
+
23
+ Supported languages include HTML, CSS, JavaScript, and JSON (see `CodeEditorLanguage`).
24
+
25
+ ## UI entry points
26
+
27
+ | Surface | Import |
28
+ |---------|--------|
29
+ | Web | `@verbb/plugin-kit-web/components/code-editor.js` |
30
+ | React | `CodeEditor` from `@verbb/plugin-kit-react/components` |
31
+ | Vue | `CodeEditor` from `@verbb/plugin-kit-vue/components` |
32
+
33
+ SchemaForm field key: `codeEditor` (via `@verbb/plugin-kit-react/forms` or `@verbb/plugin-kit-vue/forms`).
34
+
35
+ ## Docs
36
+
37
+ [Code Editor (Web)](https://docs.verbb.io/plugin-kit/web/components/code-editor) · [React](https://docs.verbb.io/plugin-kit/react/components/code-editor) · [Vue](https://docs.verbb.io/plugin-kit/vue/components/code-editor)
@@ -0,0 +1,4 @@
1
+ export type CodeEditorLanguage = 'html' | 'text' | 'javascript' | 'css' | 'json';
2
+ export declare const CODE_EDITOR_LINE_HEIGHT_PX = 18;
3
+ export declare function computeCodeEditorMinHeight(rows?: number): string;
4
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,KAAK,GAAG,MAAM,CAAC;AAEjF,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,wBAAgB,0BAA0B,CAAC,IAAI,SAAK,GAAG,MAAM,CAI5D"}
@@ -0,0 +1,9 @@
1
+ //#region src/constants.ts
2
+ var CODE_EDITOR_LINE_HEIGHT_PX = 18;
3
+ function computeCodeEditorMinHeight(rows = 12) {
4
+ return `${Math.max(Number(rows) || 12, 4) * 18 + 12}px`;
5
+ }
6
+ //#endregion
7
+ export { CODE_EDITOR_LINE_HEIGHT_PX, computeCodeEditorMinHeight };
8
+
9
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export type CodeEditorLanguage = 'html' | 'text' | 'javascript' | 'css' | 'json';\n\nexport const CODE_EDITOR_LINE_HEIGHT_PX = 18;\n\nexport function computeCodeEditorMinHeight(rows = 12): string {\n const normalizedRows = Math.max(Number(rows) || 12, 4);\n\n return `${normalizedRows * CODE_EDITOR_LINE_HEIGHT_PX + 12}px`;\n}\n"],"mappings":";AAEA,IAAa,6BAA6B;AAE1C,SAAgB,2BAA2B,OAAO,IAAY;AAG1D,QAAO,GAFgB,KAAK,IAAI,OAAO,KAAK,IAAI,IAAI,EAE1C,GAAA,KAA8C,GAAG"}
@@ -0,0 +1,13 @@
1
+ import { Compartment, Extension } from '@codemirror/state';
2
+ import { CodeEditorLanguage } from './constants.js';
3
+ export type CreateCodeEditorExtensionsOptions = {
4
+ language?: CodeEditorLanguage;
5
+ tabSize?: number;
6
+ lineNumbers?: boolean;
7
+ editable?: boolean;
8
+ editableCompartment?: Compartment;
9
+ onUpdate?: (value: string) => void;
10
+ onBlur?: () => void;
11
+ };
12
+ export declare function createCodeEditorExtensions({ language, tabSize, lineNumbers: showLineNumbers, editable, editableCompartment, onUpdate, onBlur, }: CreateCodeEditorExtensionsOptions): Extension[];
13
+ //# sourceMappingURL=extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAe,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAW7E,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAIzD,MAAM,MAAM,iCAAiC,GAAG;IAC5C,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mBAAmB,CAAC,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAkCF,wBAAgB,0BAA0B,CAAC,EACvC,QAAiB,EACjB,OAAW,EACX,WAAW,EAAE,eAAsB,EACnC,QAAe,EACf,mBAAmB,EACnB,QAAQ,EACR,MAAM,GACT,EAAE,iCAAiC,GAAG,SAAS,EAAE,CAwCjD"}
@@ -0,0 +1,57 @@
1
+ import { createLanguageExtension, languageUsesSyntaxHelpers } from "./languages.js";
2
+ import { codeEditorTheme } from "./theme.js";
3
+ import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
4
+ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
5
+ import { bracketMatching, defaultHighlightStyle, indentOnInput, indentUnit, syntaxHighlighting } from "@codemirror/language";
6
+ import { EditorState } from "@codemirror/state";
7
+ import { EditorView, drawSelection, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers } from "@codemirror/view";
8
+ //#region src/extensions.ts
9
+ function createEditorSetupExtensions(language, showLineNumbers) {
10
+ const usesSyntaxHelpers = languageUsesSyntaxHelpers(language);
11
+ const extensions = [
12
+ highlightSpecialChars(),
13
+ history(),
14
+ drawSelection(),
15
+ EditorState.allowMultipleSelections.of(false),
16
+ indentOnInput(),
17
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
18
+ highlightActiveLine(),
19
+ keymap.of([
20
+ ...usesSyntaxHelpers ? closeBracketsKeymap : [],
21
+ ...defaultKeymap,
22
+ ...historyKeymap
23
+ ])
24
+ ];
25
+ if (showLineNumbers) extensions.unshift(lineNumbers(), highlightActiveLineGutter());
26
+ if (usesSyntaxHelpers) {
27
+ extensions.push(bracketMatching());
28
+ if (language === "html" || language === "javascript" || language === "json") extensions.push(closeBrackets());
29
+ }
30
+ return extensions;
31
+ }
32
+ function createCodeEditorExtensions({ language = "html", tabSize = 4, lineNumbers: showLineNumbers = true, editable = true, editableCompartment, onUpdate, onBlur }) {
33
+ const normalizedTabSize = Math.max(1, tabSize);
34
+ const extensions = [
35
+ codeEditorTheme,
36
+ EditorState.tabSize.of(normalizedTabSize),
37
+ indentUnit.of(" ".repeat(normalizedTabSize)),
38
+ EditorView.lineWrapping,
39
+ ...createEditorSetupExtensions(language, showLineNumbers)
40
+ ];
41
+ const languageExtension = createLanguageExtension(language);
42
+ if (languageExtension) extensions.push(languageExtension);
43
+ if (editableCompartment) extensions.push(editableCompartment.of(EditorView.editable.of(editable)));
44
+ else extensions.push(EditorView.editable.of(editable));
45
+ if (onUpdate) extensions.push(EditorView.updateListener.of((update) => {
46
+ if (update.docChanged) onUpdate(update.state.doc.toString());
47
+ }));
48
+ if (onBlur) extensions.push(EditorView.domEventHandlers({ blur: () => {
49
+ onBlur();
50
+ return false;
51
+ } }));
52
+ return extensions;
53
+ }
54
+ //#endregion
55
+ export { createCodeEditorExtensions };
56
+
57
+ //# sourceMappingURL=extensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.js","names":[],"sources":["../src/extensions.ts"],"sourcesContent":["import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';\nimport { defaultKeymap, history, historyKeymap } from '@codemirror/commands';\nimport {\n bracketMatching,\n defaultHighlightStyle,\n indentOnInput,\n indentUnit,\n syntaxHighlighting,\n} from '@codemirror/language';\nimport { Compartment, EditorState, type Extension } from '@codemirror/state';\nimport {\n drawSelection,\n highlightActiveLine,\n highlightActiveLineGutter,\n highlightSpecialChars,\n keymap,\n lineNumbers,\n EditorView,\n} from '@codemirror/view';\n\nimport { type CodeEditorLanguage } from './constants.js';\nimport { createLanguageExtension, languageUsesSyntaxHelpers } from './languages.js';\nimport { codeEditorTheme } from './theme.js';\n\nexport type CreateCodeEditorExtensionsOptions = {\n language?: CodeEditorLanguage;\n tabSize?: number;\n lineNumbers?: boolean;\n editable?: boolean;\n editableCompartment?: Compartment;\n onUpdate?: (value: string) => void;\n onBlur?: () => void;\n};\n\nfunction createEditorSetupExtensions(language: CodeEditorLanguage, showLineNumbers: boolean): Extension[] {\n const usesSyntaxHelpers = languageUsesSyntaxHelpers(language);\n const extensions: Extension[] = [\n highlightSpecialChars(),\n history(),\n drawSelection(),\n EditorState.allowMultipleSelections.of(false),\n indentOnInput(),\n syntaxHighlighting(defaultHighlightStyle, { fallback: true }),\n highlightActiveLine(),\n keymap.of([\n ...(usesSyntaxHelpers ? closeBracketsKeymap : []),\n ...defaultKeymap,\n ...historyKeymap,\n ]),\n ];\n\n if (showLineNumbers) {\n extensions.unshift(lineNumbers(), highlightActiveLineGutter());\n }\n\n if (usesSyntaxHelpers) {\n extensions.push(bracketMatching());\n\n if (language === 'html' || language === 'javascript' || language === 'json') {\n extensions.push(closeBrackets());\n }\n }\n\n return extensions;\n}\n\nexport function createCodeEditorExtensions({\n language = 'html',\n tabSize = 4,\n lineNumbers: showLineNumbers = true,\n editable = true,\n editableCompartment,\n onUpdate,\n onBlur,\n}: CreateCodeEditorExtensionsOptions): Extension[] {\n const normalizedTabSize = Math.max(1, tabSize);\n const extensions: Extension[] = [\n codeEditorTheme,\n EditorState.tabSize.of(normalizedTabSize),\n indentUnit.of(' '.repeat(normalizedTabSize)),\n EditorView.lineWrapping,\n ...createEditorSetupExtensions(language, showLineNumbers),\n ];\n\n const languageExtension = createLanguageExtension(language);\n\n if (languageExtension) {\n extensions.push(languageExtension);\n }\n\n if (editableCompartment) {\n extensions.push(editableCompartment.of(EditorView.editable.of(editable)));\n } else {\n extensions.push(EditorView.editable.of(editable));\n }\n\n if (onUpdate) {\n extensions.push(EditorView.updateListener.of((update) => {\n if (update.docChanged) {\n onUpdate(update.state.doc.toString());\n }\n }));\n }\n\n if (onBlur) {\n extensions.push(EditorView.domEventHandlers({\n blur: () => {\n onBlur();\n return false;\n },\n }));\n }\n\n return extensions;\n}\n"],"mappings":";;;;;;;;AAkCA,SAAS,4BAA4B,UAA8B,iBAAuC;CACtG,MAAM,oBAAoB,0BAA0B,SAAS;CAC7D,MAAM,aAA0B;EAC5B,uBAAuB;EACvB,SAAS;EACT,eAAe;EACf,YAAY,wBAAwB,GAAG,MAAM;EAC7C,eAAe;EACf,mBAAmB,uBAAuB,EAAE,UAAU,MAAM,CAAC;EAC7D,qBAAqB;EACrB,OAAO,GAAG;GACN,GAAI,oBAAoB,sBAAsB,EAAE;GAChD,GAAG;GACH,GAAG;GACN,CAAC;EACL;AAED,KAAI,gBACA,YAAW,QAAQ,aAAa,EAAE,2BAA2B,CAAC;AAGlE,KAAI,mBAAmB;AACnB,aAAW,KAAK,iBAAiB,CAAC;AAElC,MAAI,aAAa,UAAU,aAAa,gBAAgB,aAAa,OACjE,YAAW,KAAK,eAAe,CAAC;;AAIxC,QAAO;;AAGX,SAAgB,2BAA2B,EACvC,WAAW,QACX,UAAU,GACV,aAAa,kBAAkB,MAC/B,WAAW,MACX,qBACA,UACA,UAC+C;CAC/C,MAAM,oBAAoB,KAAK,IAAI,GAAG,QAAQ;CAC9C,MAAM,aAA0B;EAC5B;EACA,YAAY,QAAQ,GAAG,kBAAkB;EACzC,WAAW,GAAG,IAAI,OAAO,kBAAkB,CAAC;EAC5C,WAAW;EACX,GAAG,4BAA4B,UAAU,gBAAgB;EAC5D;CAED,MAAM,oBAAoB,wBAAwB,SAAS;AAE3D,KAAI,kBACA,YAAW,KAAK,kBAAkB;AAGtC,KAAI,oBACA,YAAW,KAAK,oBAAoB,GAAG,WAAW,SAAS,GAAG,SAAS,CAAC,CAAC;KAEzE,YAAW,KAAK,WAAW,SAAS,GAAG,SAAS,CAAC;AAGrD,KAAI,SACA,YAAW,KAAK,WAAW,eAAe,IAAI,WAAW;AACrD,MAAI,OAAO,WACP,UAAS,OAAO,MAAM,IAAI,UAAU,CAAC;GAE3C,CAAC;AAGP,KAAI,OACA,YAAW,KAAK,WAAW,iBAAiB,EACxC,YAAY;AACR,UAAQ;AACR,SAAO;IAEd,CAAC,CAAC;AAGP,QAAO"}
package/dist/host.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { EditorView } from '@codemirror/view';
2
+ import { CodeEditorLanguage } from './constants.js';
3
+ export type CodeMirrorHostMountOptions = {
4
+ parent: HTMLElement;
5
+ value?: string;
6
+ language?: CodeEditorLanguage;
7
+ tabSize?: number;
8
+ lineNumbers?: boolean;
9
+ rows?: number;
10
+ editable?: boolean;
11
+ onUpdate?: (value: string) => void;
12
+ onBlur?: () => void;
13
+ };
14
+ export declare class CodeMirrorHost {
15
+ view: EditorView | null;
16
+ private readonly editableCompartment;
17
+ private lastEmitted;
18
+ mount({ parent, value, language, tabSize, lineNumbers, rows, editable, onUpdate, onBlur, }: CodeMirrorHostMountOptions): void;
19
+ getValue(): string;
20
+ setValue(value: string, options?: {
21
+ respectFocus?: boolean;
22
+ }): void;
23
+ setEditable(editable: boolean): void;
24
+ destroy(): void;
25
+ }
26
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAA8B,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGrF,MAAM,MAAM,0BAA0B,GAAG;IACrC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF,qBAAa,cAAc;IACvB,IAAI,EAAE,UAAU,GAAG,IAAI,CAAQ;IAE/B,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,WAAW,CAAuB;IAE1C,KAAK,CAAC,EACF,MAAM,EACN,KAAU,EACV,QAAiB,EACjB,OAAW,EACX,WAAkB,EAClB,IAAS,EACT,QAAe,EACf,QAAQ,EACR,MAAM,GACT,EAAE,0BAA0B,GAAG,IAAI;IAwBpC,QAAQ,IAAI,MAAM;IAIlB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,IAAI;IA0BvE,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAUpC,OAAO,IAAI,IAAI;CAKlB"}
package/dist/host.js ADDED
@@ -0,0 +1,59 @@
1
+ import { computeCodeEditorMinHeight } from "./constants.js";
2
+ import { createCodeEditorExtensions } from "./extensions.js";
3
+ import { Compartment, EditorState } from "@codemirror/state";
4
+ import { EditorView } from "@codemirror/view";
5
+ //#region src/host.ts
6
+ var CodeMirrorHost = class {
7
+ view = null;
8
+ editableCompartment = new Compartment();
9
+ lastEmitted = null;
10
+ mount({ parent, value = "", language = "html", tabSize = 4, lineNumbers = true, rows = 12, editable = true, onUpdate, onBlur }) {
11
+ this.view = new EditorView({
12
+ state: EditorState.create({
13
+ doc: value,
14
+ extensions: createCodeEditorExtensions({
15
+ language,
16
+ tabSize,
17
+ lineNumbers,
18
+ editable,
19
+ editableCompartment: this.editableCompartment,
20
+ onUpdate: (nextValue) => {
21
+ this.lastEmitted = nextValue;
22
+ onUpdate?.(nextValue);
23
+ },
24
+ onBlur
25
+ })
26
+ }),
27
+ parent
28
+ });
29
+ this.view.dom.style.minHeight = computeCodeEditorMinHeight(rows);
30
+ this.view.dom.style.width = "100%";
31
+ }
32
+ getValue() {
33
+ return this.view?.state.doc.toString() ?? "";
34
+ }
35
+ setValue(value, options = {}) {
36
+ if (!this.view) return;
37
+ const { respectFocus = true } = options;
38
+ if (respectFocus && this.view.hasFocus) return;
39
+ if (this.view.state.doc.toString() === value || value === this.lastEmitted) return;
40
+ this.view.dispatch({ changes: {
41
+ from: 0,
42
+ to: this.view.state.doc.length,
43
+ insert: value
44
+ } });
45
+ }
46
+ setEditable(editable) {
47
+ if (!this.view) return;
48
+ this.view.dispatch({ effects: this.editableCompartment.reconfigure(EditorView.editable.of(editable)) });
49
+ }
50
+ destroy() {
51
+ this.view?.destroy();
52
+ this.view = null;
53
+ this.lastEmitted = null;
54
+ }
55
+ };
56
+ //#endregion
57
+ export { CodeMirrorHost };
58
+
59
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","names":[],"sources":["../src/host.ts"],"sourcesContent":["import { Compartment, EditorState } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\n\nimport { computeCodeEditorMinHeight, type CodeEditorLanguage } from './constants.js';\nimport { createCodeEditorExtensions } from './extensions.js';\n\nexport type CodeMirrorHostMountOptions = {\n parent: HTMLElement;\n value?: string;\n language?: CodeEditorLanguage;\n tabSize?: number;\n lineNumbers?: boolean;\n rows?: number;\n editable?: boolean;\n onUpdate?: (value: string) => void;\n onBlur?: () => void;\n};\n\nexport class CodeMirrorHost {\n view: EditorView | null = null;\n\n private readonly editableCompartment = new Compartment();\n\n private lastEmitted: string | null = null;\n\n mount({\n parent,\n value = '',\n language = 'html',\n tabSize = 4,\n lineNumbers = true,\n rows = 12,\n editable = true,\n onUpdate,\n onBlur,\n }: CodeMirrorHostMountOptions): void {\n this.view = new EditorView({\n state: EditorState.create({\n doc: value,\n extensions: createCodeEditorExtensions({\n language,\n tabSize,\n lineNumbers,\n editable,\n editableCompartment: this.editableCompartment,\n onUpdate: (nextValue) => {\n this.lastEmitted = nextValue;\n onUpdate?.(nextValue);\n },\n onBlur,\n }),\n }),\n parent,\n });\n\n this.view.dom.style.minHeight = computeCodeEditorMinHeight(rows);\n this.view.dom.style.width = '100%';\n }\n\n getValue(): string {\n return this.view?.state.doc.toString() ?? '';\n }\n\n setValue(value: string, options: { respectFocus?: boolean } = {}): void {\n if (!this.view) {\n return;\n }\n\n const { respectFocus = true } = options;\n\n if (respectFocus && this.view.hasFocus) {\n return;\n }\n\n const current = this.view.state.doc.toString();\n\n if (current === value || value === this.lastEmitted) {\n return;\n }\n\n this.view.dispatch({\n changes: {\n from: 0,\n to: this.view.state.doc.length,\n insert: value,\n },\n });\n }\n\n setEditable(editable: boolean): void {\n if (!this.view) {\n return;\n }\n\n this.view.dispatch({\n effects: this.editableCompartment.reconfigure(EditorView.editable.of(editable)),\n });\n }\n\n destroy(): void {\n this.view?.destroy();\n this.view = null;\n this.lastEmitted = null;\n }\n}\n"],"mappings":";;;;;AAkBA,IAAa,iBAAb,MAA4B;CACxB,OAA0B;CAE1B,sBAAuC,IAAI,aAAa;CAExD,cAAqC;CAErC,MAAM,EACF,QACA,QAAQ,IACR,WAAW,QACX,UAAU,GACV,cAAc,MACd,OAAO,IACP,WAAW,MACX,UACA,UACiC;AACjC,OAAK,OAAO,IAAI,WAAW;GACvB,OAAO,YAAY,OAAO;IACtB,KAAK;IACL,YAAY,2BAA2B;KACnC;KACA;KACA;KACA;KACA,qBAAqB,KAAK;KAC1B,WAAW,cAAc;AACrB,WAAK,cAAc;AACnB,iBAAW,UAAU;;KAEzB;KACH,CAAC;IACL,CAAC;GACF;GACH,CAAC;AAEF,OAAK,KAAK,IAAI,MAAM,YAAY,2BAA2B,KAAK;AAChE,OAAK,KAAK,IAAI,MAAM,QAAQ;;CAGhC,WAAmB;AACf,SAAO,KAAK,MAAM,MAAM,IAAI,UAAU,IAAI;;CAG9C,SAAS,OAAe,UAAsC,EAAE,EAAQ;AACpE,MAAI,CAAC,KAAK,KACN;EAGJ,MAAM,EAAE,eAAe,SAAS;AAEhC,MAAI,gBAAgB,KAAK,KAAK,SAC1B;AAKJ,MAFgB,KAAK,KAAK,MAAM,IAAI,UAEhC,KAAY,SAAS,UAAU,KAAK,YACpC;AAGJ,OAAK,KAAK,SAAS,EACf,SAAS;GACL,MAAM;GACN,IAAI,KAAK,KAAK,MAAM,IAAI;GACxB,QAAQ;GACX,EACJ,CAAC;;CAGN,YAAY,UAAyB;AACjC,MAAI,CAAC,KAAK,KACN;AAGJ,OAAK,KAAK,SAAS,EACf,SAAS,KAAK,oBAAoB,YAAY,WAAW,SAAS,GAAG,SAAS,CAAC,EAClF,CAAC;;CAGN,UAAgB;AACZ,OAAK,MAAM,SAAS;AACpB,OAAK,OAAO;AACZ,OAAK,cAAc"}
@@ -0,0 +1,9 @@
1
+ export { CODE_EDITOR_LINE_HEIGHT_PX, computeCodeEditorMinHeight, } from './constants.js';
2
+ export type { CodeEditorLanguage } from './constants.js';
3
+ export { createCodeEditorExtensions, } from './extensions.js';
4
+ export type { CreateCodeEditorExtensionsOptions } from './extensions.js';
5
+ export { createLanguageExtension, languageUsesSyntaxHelpers, } from './languages.js';
6
+ export { CODE_EDITOR_ACTIVE_LINE_BG, CODE_EDITOR_CHROME_BG, CODE_EDITOR_GUTTER_BG, CODE_EDITOR_GUTTER_BORDER, CODE_EDITOR_GUTTER_TEXT_COLOR, CODE_EDITOR_SELECTION_BG, CODE_EDITOR_SURFACE_BG, CODE_EDITOR_TEXT_COLOR, codeEditorTheme, } from './theme.js';
7
+ export { CodeMirrorHost } from './host.js';
8
+ export type { CodeMirrorHostMountOptions } from './host.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,0BAA0B,EAC1B,0BAA0B,GAC7B,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,OAAO,EACH,0BAA0B,GAC7B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,iCAAiC,EAAE,MAAM,iBAAiB,CAAC;AAEzE,OAAO,EACH,uBAAuB,EACvB,yBAAyB,GAC5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACH,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACrB,yBAAyB,EACzB,6BAA6B,EAC7B,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,EACtB,eAAe,GAClB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { CODE_EDITOR_LINE_HEIGHT_PX, computeCodeEditorMinHeight } from "./constants.js";
2
+ import { createLanguageExtension, languageUsesSyntaxHelpers } from "./languages.js";
3
+ import { CODE_EDITOR_ACTIVE_LINE_BG, CODE_EDITOR_CHROME_BG, CODE_EDITOR_GUTTER_BG, CODE_EDITOR_GUTTER_BORDER, CODE_EDITOR_GUTTER_TEXT_COLOR, CODE_EDITOR_SELECTION_BG, CODE_EDITOR_SURFACE_BG, CODE_EDITOR_TEXT_COLOR, codeEditorTheme } from "./theme.js";
4
+ import { createCodeEditorExtensions } from "./extensions.js";
5
+ import { CodeMirrorHost } from "./host.js";
6
+ export { CODE_EDITOR_ACTIVE_LINE_BG, CODE_EDITOR_CHROME_BG, CODE_EDITOR_GUTTER_BG, CODE_EDITOR_GUTTER_BORDER, CODE_EDITOR_GUTTER_TEXT_COLOR, CODE_EDITOR_LINE_HEIGHT_PX, CODE_EDITOR_SELECTION_BG, CODE_EDITOR_SURFACE_BG, CODE_EDITOR_TEXT_COLOR, CodeMirrorHost, codeEditorTheme, computeCodeEditorMinHeight, createCodeEditorExtensions, createLanguageExtension, languageUsesSyntaxHelpers };
@@ -0,0 +1,5 @@
1
+ import { Extension } from '@codemirror/state';
2
+ import { CodeEditorLanguage } from './constants.js';
3
+ export declare function languageUsesSyntaxHelpers(language: CodeEditorLanguage): boolean;
4
+ export declare function createLanguageExtension(language: CodeEditorLanguage): Extension | null;
5
+ //# sourceMappingURL=languages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"languages.d.ts","sourceRoot":"","sources":["../src/languages.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAE/E;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAatF"}
@@ -0,0 +1,21 @@
1
+ import { css } from "@codemirror/lang-css";
2
+ import { html } from "@codemirror/lang-html";
3
+ import { javascript } from "@codemirror/lang-javascript";
4
+ import { json } from "@codemirror/lang-json";
5
+ //#region src/languages.ts
6
+ function languageUsesSyntaxHelpers(language) {
7
+ return language !== "text";
8
+ }
9
+ function createLanguageExtension(language) {
10
+ switch (language) {
11
+ case "html": return html();
12
+ case "javascript": return javascript();
13
+ case "css": return css();
14
+ case "json": return json();
15
+ default: return null;
16
+ }
17
+ }
18
+ //#endregion
19
+ export { createLanguageExtension, languageUsesSyntaxHelpers };
20
+
21
+ //# sourceMappingURL=languages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"languages.js","names":[],"sources":["../src/languages.ts"],"sourcesContent":["import { css } from '@codemirror/lang-css';\nimport { html } from '@codemirror/lang-html';\nimport { javascript } from '@codemirror/lang-javascript';\nimport { json } from '@codemirror/lang-json';\nimport type { Extension } from '@codemirror/state';\n\nimport type { CodeEditorLanguage } from './constants.js';\n\nexport function languageUsesSyntaxHelpers(language: CodeEditorLanguage): boolean {\n return language !== 'text';\n}\n\nexport function createLanguageExtension(language: CodeEditorLanguage): Extension | null {\n switch (language) {\n case 'html':\n return html();\n case 'javascript':\n return javascript();\n case 'css':\n return css();\n case 'json':\n return json();\n default:\n return null;\n }\n}\n"],"mappings":";;;;;AAQA,SAAgB,0BAA0B,UAAuC;AAC7E,QAAO,aAAa;;AAGxB,SAAgB,wBAAwB,UAAgD;AACpF,SAAQ,UAAR;EACI,KAAK,OACD,QAAO,MAAM;EACjB,KAAK,aACD,QAAO,YAAY;EACvB,KAAK,MACD,QAAO,KAAK;EAChB,KAAK,OACD,QAAO,MAAM;EACjB,QACI,QAAO"}
@@ -0,0 +1,18 @@
1
+ /** Editor content surface — matches @uiw/react-codemirror default light theme. */
2
+ export declare const CODE_EDITOR_SURFACE_BG = "#ffffff";
3
+ /** Gutter strip beside line numbers. */
4
+ export declare const CODE_EDITOR_GUTTER_BG = "rgb(245, 247, 250)";
5
+ export declare const CODE_EDITOR_TEXT_COLOR = "rgb(31, 41, 51)";
6
+ export declare const CODE_EDITOR_GUTTER_TEXT_COLOR = "#7c8793";
7
+ export declare const CODE_EDITOR_GUTTER_BORDER = "1px solid rgba(96, 125, 159, 0.2)";
8
+ export declare const CODE_EDITOR_ACTIVE_LINE_BG = "rgba(96, 125, 159, 0.06)";
9
+ export declare const CODE_EDITOR_SELECTION_BG = "rgba(59, 130, 246, 0.35)";
10
+ /** Outer chrome behind the editor — Craft input shell tone. */
11
+ export declare const CODE_EDITOR_CHROME_BG = "rgb(251, 252, 254)";
12
+ /**
13
+ * Matches the legacy React CodeEditor theme. The interior surface is white
14
+ * (via @uiw/react-codemirror's default light theme); only the gutter uses
15
+ * the cooler gray strip.
16
+ */
17
+ export declare const codeEditorTheme: import('@codemirror/state').Extension;
18
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAIA,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,YAAY,CAAC;AAEhD,wCAAwC;AACxC,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D,eAAO,MAAM,sBAAsB,oBAAoB,CAAC;AACxD,eAAO,MAAM,6BAA6B,YAAY,CAAC;AACvD,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAC7E,eAAO,MAAM,0BAA0B,6BAA6B,CAAC;AACrE,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAEnE,+DAA+D;AAC/D,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,eAAe,uCA6C1B,CAAC"}
package/dist/theme.js ADDED
@@ -0,0 +1,58 @@
1
+ import "./constants.js";
2
+ import { EditorView } from "@codemirror/view";
3
+ //#region src/theme.ts
4
+ /** Editor content surface — matches @uiw/react-codemirror default light theme. */
5
+ var CODE_EDITOR_SURFACE_BG = "#ffffff";
6
+ /** Gutter strip beside line numbers. */
7
+ var CODE_EDITOR_GUTTER_BG = "rgb(245, 247, 250)";
8
+ var CODE_EDITOR_TEXT_COLOR = "rgb(31, 41, 51)";
9
+ var CODE_EDITOR_GUTTER_TEXT_COLOR = "#7c8793";
10
+ var CODE_EDITOR_GUTTER_BORDER = "1px solid rgba(96, 125, 159, 0.2)";
11
+ var CODE_EDITOR_ACTIVE_LINE_BG = "rgba(96, 125, 159, 0.06)";
12
+ var CODE_EDITOR_SELECTION_BG = "rgba(59, 130, 246, 0.35)";
13
+ /** Outer chrome behind the editor — Craft input shell tone. */
14
+ var CODE_EDITOR_CHROME_BG = "rgb(251, 252, 254)";
15
+ /**
16
+ * Matches the legacy React CodeEditor theme. The interior surface is white
17
+ * (via @uiw/react-codemirror's default light theme); only the gutter uses
18
+ * the cooler gray strip.
19
+ */
20
+ var codeEditorTheme = EditorView.theme({
21
+ "&": {
22
+ backgroundColor: CODE_EDITOR_SURFACE_BG,
23
+ color: CODE_EDITOR_TEXT_COLOR,
24
+ fontSize: "0.9em"
25
+ },
26
+ "&.cm-focused": { outline: "none" },
27
+ ".cm-scroller": {
28
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
29
+ lineHeight: `18px`
30
+ },
31
+ ".cm-content": {
32
+ padding: "6px 8px",
33
+ caretColor: CODE_EDITOR_TEXT_COLOR
34
+ },
35
+ ".cm-gutters": {
36
+ backgroundColor: CODE_EDITOR_GUTTER_BG,
37
+ borderRight: CODE_EDITOR_GUTTER_BORDER,
38
+ color: CODE_EDITOR_GUTTER_TEXT_COLOR
39
+ },
40
+ ".cm-gutter.cm-lineNumbers .cm-gutterElement": {
41
+ padding: "0 8px",
42
+ minWidth: "3rem"
43
+ },
44
+ ".cm-activeLine": { backgroundColor: CODE_EDITOR_ACTIVE_LINE_BG },
45
+ "&.cm-light .cm-activeLine": { backgroundColor: CODE_EDITOR_ACTIVE_LINE_BG },
46
+ "&.cm-light .cm-activeLineGutter": { backgroundColor: CODE_EDITOR_GUTTER_BG },
47
+ "&.cm-light .cm-gutters": {
48
+ backgroundColor: CODE_EDITOR_GUTTER_BG,
49
+ color: CODE_EDITOR_GUTTER_TEXT_COLOR,
50
+ border: "0 solid transparent",
51
+ borderRight: CODE_EDITOR_GUTTER_BORDER
52
+ },
53
+ ".cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection": { backgroundColor: `${CODE_EDITOR_SELECTION_BG} !important` }
54
+ });
55
+ //#endregion
56
+ export { CODE_EDITOR_ACTIVE_LINE_BG, CODE_EDITOR_CHROME_BG, CODE_EDITOR_GUTTER_BG, CODE_EDITOR_GUTTER_BORDER, CODE_EDITOR_GUTTER_TEXT_COLOR, CODE_EDITOR_SELECTION_BG, CODE_EDITOR_SURFACE_BG, CODE_EDITOR_TEXT_COLOR, codeEditorTheme };
57
+
58
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["import { EditorView } from '@codemirror/view';\n\nimport { CODE_EDITOR_LINE_HEIGHT_PX } from './constants.js';\n\n/** Editor content surface — matches @uiw/react-codemirror default light theme. */\nexport const CODE_EDITOR_SURFACE_BG = '#ffffff';\n\n/** Gutter strip beside line numbers. */\nexport const CODE_EDITOR_GUTTER_BG = 'rgb(245, 247, 250)';\n\nexport const CODE_EDITOR_TEXT_COLOR = 'rgb(31, 41, 51)';\nexport const CODE_EDITOR_GUTTER_TEXT_COLOR = '#7c8793';\nexport const CODE_EDITOR_GUTTER_BORDER = '1px solid rgba(96, 125, 159, 0.2)';\nexport const CODE_EDITOR_ACTIVE_LINE_BG = 'rgba(96, 125, 159, 0.06)';\nexport const CODE_EDITOR_SELECTION_BG = 'rgba(59, 130, 246, 0.35)';\n\n/** Outer chrome behind the editor — Craft input shell tone. */\nexport const CODE_EDITOR_CHROME_BG = 'rgb(251, 252, 254)';\n\n/**\n * Matches the legacy React CodeEditor theme. The interior surface is white\n * (via @uiw/react-codemirror's default light theme); only the gutter uses\n * the cooler gray strip.\n */\nexport const codeEditorTheme = EditorView.theme({\n '&': {\n backgroundColor: CODE_EDITOR_SURFACE_BG,\n color: CODE_EDITOR_TEXT_COLOR,\n // Match kit mono optical size (text-[0.9em] / --pk-font-size-mono).\n fontSize: '0.9em',\n },\n '&.cm-focused': {\n outline: 'none',\n },\n '.cm-scroller': {\n fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',\n lineHeight: `${CODE_EDITOR_LINE_HEIGHT_PX}px`,\n },\n '.cm-content': {\n padding: '6px 8px',\n caretColor: CODE_EDITOR_TEXT_COLOR,\n },\n '.cm-gutters': {\n backgroundColor: CODE_EDITOR_GUTTER_BG,\n borderRight: CODE_EDITOR_GUTTER_BORDER,\n color: CODE_EDITOR_GUTTER_TEXT_COLOR,\n },\n '.cm-gutter.cm-lineNumbers .cm-gutterElement': {\n padding: '0 8px',\n minWidth: '3rem',\n },\n '.cm-activeLine': {\n backgroundColor: CODE_EDITOR_ACTIVE_LINE_BG,\n },\n '&.cm-light .cm-activeLine': {\n backgroundColor: CODE_EDITOR_ACTIVE_LINE_BG,\n },\n '&.cm-light .cm-activeLineGutter': {\n backgroundColor: CODE_EDITOR_GUTTER_BG,\n },\n '&.cm-light .cm-gutters': {\n backgroundColor: CODE_EDITOR_GUTTER_BG,\n color: CODE_EDITOR_GUTTER_TEXT_COLOR,\n border: '0 solid transparent',\n borderRight: CODE_EDITOR_GUTTER_BORDER,\n },\n '.cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection': {\n backgroundColor: `${CODE_EDITOR_SELECTION_BG} !important`,\n },\n});\n"],"mappings":";;;;AAKA,IAAa,yBAAyB;;AAGtC,IAAa,wBAAwB;AAErC,IAAa,yBAAyB;AACtC,IAAa,gCAAgC;AAC7C,IAAa,4BAA4B;AACzC,IAAa,6BAA6B;AAC1C,IAAa,2BAA2B;;AAGxC,IAAa,wBAAwB;;;;;;AAOrC,IAAa,kBAAkB,WAAW,MAAM;CAC5C,KAAK;EACD,iBAAiB;EACjB,OAAO;EAEP,UAAU;EACb;CACD,gBAAgB,EACZ,SAAS,QACZ;CACD,gBAAgB;EACZ,YAAY;EACZ,YAAY;EACf;CACD,eAAe;EACX,SAAS;EACT,YAAY;EACf;CACD,eAAe;EACX,iBAAiB;EACjB,aAAa;EACb,OAAO;EACV;CACD,+CAA+C;EAC3C,SAAS;EACT,UAAU;EACb;CACD,kBAAkB,EACd,iBAAiB,4BACpB;CACD,6BAA6B,EACzB,iBAAiB,4BACpB;CACD,mCAAmC,EAC/B,iBAAiB,uBACpB;CACD,0BAA0B;EACtB,iBAAiB;EACjB,OAAO;EACP,QAAQ;EACR,aAAa;EAChB;CACD,8EAA8E,EAC1E,iBAAiB,GAAG,yBAAyB,cAChD;CACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@verbb/plugin-kit-codemirror-core",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "LICENSE.md",
11
+ "CHANGELOG.md"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/verbb/plugin-kit.git",
16
+ "directory": "plugin-kit-codemirror-core"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/verbb/plugin-kit/issues"
20
+ },
21
+ "homepage": "https://docs.verbb.io/plugin-kit",
22
+ "engines": {
23
+ "node": ">=20.0.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "scripts": {
36
+ "build": "vite build",
37
+ "dev": "vite build --watch",
38
+ "test": "vitest run"
39
+ },
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "@codemirror/autocomplete": "^6.20.0",
43
+ "@codemirror/commands": "^6.10.2",
44
+ "@codemirror/lang-css": "^6.3.1",
45
+ "@codemirror/lang-html": "^6.4.11",
46
+ "@codemirror/lang-javascript": "^6.2.4",
47
+ "@codemirror/lang-json": "^6.0.2",
48
+ "@codemirror/language": "^6.12.1",
49
+ "@codemirror/state": "^6.6.0",
50
+ "@codemirror/view": "^6.43.1"
51
+ },
52
+ "devDependencies": {
53
+ "jsdom": "^26.1.0",
54
+ "typescript": "^6.0.3",
55
+ "vite": "^8.0.10",
56
+ "vite-plugin-dts": "^4.5.4",
57
+ "vitest": "^4.1.5"
58
+ }
59
+ }