@type32/codemirror-rich-obsidian-editor 0.1.18 → 0.1.20

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/README.md CHANGED
@@ -73,6 +73,228 @@ Customize the editor fonts:
73
73
  }
74
74
  ```
75
75
 
76
+ ## Components
77
+
78
+ ### `Editor.client.vue`
79
+
80
+ The main WYSIWYG Obsidian-Flavored Markdown editor component.
81
+
82
+ **Props:**
83
+ - `v-model`: Document content (string)
84
+ - `internalLinkMap`: Array of internal link mappings for custom link rendering
85
+ - `specialCodeBlockMap`: Array of custom code block component mappings
86
+ - `bracketClosing`: Enable automatic bracket closing (default: true)
87
+ - `foldGutter`: Enable code folding gutter (default: true)
88
+ - `disabled`: Disable editing (default: false)
89
+ - `searchOptions`: Search configuration object
90
+
91
+ **Events:**
92
+ - `@internal-link-click`: Emitted when an internal link is clicked
93
+ - `@external-link-click`: Emitted when an external link is clicked
94
+
95
+ ### `CodeEditor.client.vue`
96
+
97
+ A styled code-only editor with syntax highlighting for multiple languages.
98
+
99
+ **Props:**
100
+ - `v-model`: Code content (string)
101
+ - `language`: Programming language for syntax highlighting (e.g., 'javascript', 'typescript', 'json', 'yaml')
102
+ - `lightTheme`: Custom CodeMirror theme extension for light mode (default: Catppuccin Latte)
103
+ - `darkTheme`: Custom CodeMirror theme extension for dark mode (default: Catppuccin Mocha)
104
+ - `colorMode`: Color mode ('dark' or 'light', default: 'dark')
105
+ - `bracketClosing`: Enable automatic bracket closing (default: true)
106
+ - `foldGutter`: Enable code folding gutter (default: true)
107
+ - `disabled`: Disable editing (default: false)
108
+
109
+ **Example Usage:**
110
+
111
+ ```vue
112
+ <template>
113
+ <CodeEditor
114
+ v-model="code"
115
+ language="typescript"
116
+ :color-mode="colorMode"
117
+ :dark-theme="customDarkTheme"
118
+ :light-theme="customLightTheme"
119
+ />
120
+ </template>
121
+
122
+ <script setup lang="ts">
123
+ import { ref } from 'vue'
124
+ import { oneDark } from '@codemirror/theme-one-dark'
125
+
126
+ const code = ref('console.log("Hello World")')
127
+ const colorMode = ref('dark')
128
+
129
+ // Optional: Use custom themes
130
+ const customDarkTheme = oneDark
131
+ const customLightTheme = undefined // Will use default Catppuccin Latte
132
+ </script>
133
+ ```
134
+
135
+ ## Composables
136
+
137
+ This module provides powerful composables for interacting with the editor programmatically. All composables are **reactive** and **null-safe** during initialization.
138
+
139
+ ### `useEditorUtils(editor: Ref)`
140
+
141
+ Provides utilities for document manipulation, AST operations, and search functionality.
142
+
143
+ #### Reactive Properties
144
+ - **`doc`**: Computed property that automatically updates when the editor content changes
145
+ - **`view`**: Computed property for the CodeMirror EditorView instance
146
+ - **`searchResults`**: Ref containing current search matches
147
+ - **`currentMatchIndex`**: Ref for the currently selected search match
148
+
149
+ #### Document Operations
150
+ - **`getDoc()`**: Get current document content (snapshot)
151
+ - **`setDoc(content: string)`**: Replace entire document
152
+ - **`getSelection()`**: Get current selection
153
+ - **`replaceSelection(text: string)`**: Replace selected text
154
+ - **`dispatch(...specs: TransactionSpec[])`**: Dispatch editor transactions
155
+
156
+ #### AST Operations
157
+ - **`getDocAst()`**: Get parsed markdown AST
158
+ - **`findNodesByType(tree: Tree, nodeTypeName: string)`**: Find nodes by type
159
+ - **`getDocNodesByType(nodeTypeName: string)`**: Find nodes in current document
160
+ - **`hasFrontmatter()`**: Check if document has frontmatter
161
+
162
+ #### Search Operations
163
+ - **`search(options: SearchOptions)`**: Search in document
164
+ - **`findNext()`**, **`findPrevious()`**: Navigate search results
165
+ - **`replaceCurrent(replacement: string)`**: Replace current match
166
+ - **`replaceAll(replacement: string)`**: Replace all matches
167
+
168
+ #### Example Usage
169
+
170
+ ```vue
171
+ <script setup lang="ts">
172
+ import { ref, watch } from 'vue'
173
+
174
+ const editor = ref()
175
+ const { doc, frontmatter, getDoc, setDoc } = useEditorUtils(editor)
176
+
177
+ // Reactive: automatically updates when editor content changes
178
+ watch(doc, (newContent) => {
179
+ console.log('Document changed:', newContent)
180
+ })
181
+
182
+ // Non-reactive: get current snapshot
183
+ function saveDocument() {
184
+ const content = getDoc()
185
+ // Save content...
186
+ }
187
+ </script>
188
+ ```
189
+
190
+ ### `useEditorFrontmatter<T>(editor: Ref)`
191
+
192
+ Provides utilities for managing YAML frontmatter with full reactivity and type safety.
193
+
194
+ #### Reactive Properties
195
+ - **`frontmatter`**: Computed property that automatically parses and updates when frontmatter changes
196
+ - Returns `{ data?: T, error?: Error }`
197
+
198
+ #### Methods
199
+ - **`getFrontmatter()`**: Get current frontmatter (snapshot)
200
+ - **`setFrontmatterProperties(properties: Partial<T>)`**: Replace all frontmatter
201
+ - Returns `boolean` indicating success
202
+ - Removes frontmatter entirely if properties is empty
203
+ - **`updateFrontmatterProperties(properties: Partial<T>)`**: Merge with existing frontmatter
204
+ - Returns `boolean` indicating success
205
+ - Preserves existing properties
206
+ - **`clearFrontmatter()`**: Completely remove frontmatter from document
207
+ - Returns `boolean` indicating success
208
+ - Removes YAML delimiters and trailing newlines
209
+ - **`addFrontmatterProperty(key: string, value: any)`**: Add/update single property
210
+ - Returns `boolean` indicating success
211
+ - **`removeFrontmatterProperty(key: string)`**: Remove single property
212
+ - Returns `boolean` indicating success
213
+
214
+ #### Example Usage
215
+
216
+ ```vue
217
+ <script setup lang="ts">
218
+ import { ref, watch } from 'vue'
219
+
220
+ interface MyFrontmatter {
221
+ title?: string
222
+ tags?: string[]
223
+ date?: string
224
+ }
225
+
226
+ const editor = ref()
227
+ const {
228
+ frontmatter,
229
+ setFrontmatterProperties,
230
+ updateFrontmatterProperties,
231
+ clearFrontmatter
232
+ } = useEditorFrontmatter<MyFrontmatter>(editor)
233
+
234
+ // Reactive: automatically updates when frontmatter changes
235
+ watch(frontmatter, (fm) => {
236
+ if (fm.data) {
237
+ console.log('Title:', fm.data.title)
238
+ console.log('Tags:', fm.data.tags)
239
+ }
240
+ })
241
+
242
+ // Replace all frontmatter
243
+ function setMetadata() {
244
+ setFrontmatterProperties({
245
+ title: 'My Document',
246
+ tags: ['vue', 'nuxt'],
247
+ date: new Date().toISOString()
248
+ })
249
+ }
250
+
251
+ // Update specific properties (preserves other properties)
252
+ function addTag(tag: string) {
253
+ const currentTags = frontmatter.value.data?.tags || []
254
+ updateFrontmatterProperties({
255
+ tags: [...currentTags, tag]
256
+ })
257
+ }
258
+
259
+ // Remove all frontmatter
260
+ function removeFrontmatter() {
261
+ clearFrontmatter()
262
+ }
263
+ </script>
264
+ ```
265
+
266
+ ### Null Safety
267
+
268
+ All composables are safe to use during Vue hydration when the editor ref may be `undefined` or `null`:
269
+
270
+ ```typescript
271
+ // Safe to call even if editor isn't ready yet
272
+ const { doc, frontmatter, setDoc } = useEditorUtils(editor)
273
+
274
+ // Reactive properties will be undefined until editor is initialized
275
+ console.log(doc.value) // undefined initially
276
+
277
+ // Methods return false or empty values if editor isn't ready
278
+ const success = setDoc('New content') // Returns undefined if not ready
279
+ ```
280
+
281
+ ### Enabling Reactivity
282
+
283
+ The reactive features are **automatically enabled** in both `Editor.client.vue` and `CodeEditor.client.vue` components. No additional setup is required.
284
+
285
+ If you're creating a custom editor setup, include the reactivity extension:
286
+
287
+ ```typescript
288
+ import { createEditorReactivityExtension } from '@type32/codemirror-rich-obsidian-editor/composables/useEditorUtils'
289
+
290
+ const editorInstance = shallowRef()
291
+
292
+ const extensions = [
293
+ // ... your other extensions
294
+ createEditorReactivityExtension(editorInstance)
295
+ ]
296
+ ```
297
+
76
298
  ## Known Issues
77
299
  - Same as `segphault/codemirror-rich-markdoc`, the rendered block replacement code is not yet optimized, so it recomputes all of the replaced regions on every operation instead of only updating them as needed.
78
300
  - Progress is being made on this issue: we've optimized the Rich Text Plugin to update based on only the updated ranges instead of the entire document.
@@ -92,7 +314,8 @@ Customize the editor fonts:
92
314
  - We have a mapping prop that allows developers to add their own link-to-file implementations. (Specific to Vue/Nuxt)
93
315
  - ~~Support for code-block mermaid graph rendering & bases is lacking.~~
94
316
  - We have a mapping prop that allows developers to add their own custom codeblock widgets. (Specific to Vue/Nuxt)
95
- - Light/Dark themes are not yet supported in code-block syntax highlighting.
317
+ - ~~Light/Dark themes are not yet supported in code-block syntax highlighting.~~
318
+ - The `CodeEditor.client.vue` component now supports customizable light/dark themes with Catppuccin as the default.
96
319
 
97
320
  ## Contributions
98
321
  - To anyone who wants to fork this, **make sure you preserve the original credits and references to the libraries that are used in this project. It means a lot to them and to us.**
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@type32/codemirror-rich-obsidian-editor",
3
3
  "configKey": "cmOfmEditor",
4
- "version": "0.1.18",
4
+ "version": "0.1.20",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -30,6 +30,7 @@ import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap }
30
30
  import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
31
31
  import { lintKeymap } from "@codemirror/lint";
32
32
  import { catppuccinLatte, catppuccinMocha } from "@catppuccin/codemirror";
33
+ import { createEditorReactivityExtension } from "../composables/useEditorUtils";
33
34
  const doc = defineModel({ type: String });
34
35
  const props = defineProps({
35
36
  class: { type: String, required: false },
@@ -46,6 +47,7 @@ const props = defineProps({
46
47
  const emit = defineEmits([]);
47
48
  const extensions = shallowRef([]);
48
49
  const view = shallowRef();
50
+ const editorInstance = shallowRef();
49
51
  const ast = ref([]);
50
52
  const languageCompartment = new Compartment();
51
53
  const themeCompartment = new Compartment();
@@ -126,7 +128,9 @@ onMounted(async () => {
126
128
  // Keys related to the linter system
127
129
  ...lintKeymap
128
130
  ]),
129
- EditorView.editable.of(!props.disabled)
131
+ EditorView.editable.of(!props.disabled),
132
+ // Reactivity extension for composables
133
+ createEditorReactivityExtension(editorInstance)
130
134
  ];
131
135
  });
132
136
  watch(
@@ -152,6 +156,7 @@ watch(
152
156
  );
153
157
  function handleReady(payload) {
154
158
  view.value = payload.view;
159
+ editorInstance.value = payload;
155
160
  }
156
161
  function iterate() {
157
162
  ast.value = [];
@@ -21,6 +21,7 @@ import { internalLinkMapFacet } from "../editor/plugins/linkMappingConfig";
21
21
  import { specialCodeBlockMapFacet } from "../editor/plugins/specialCodeBlockMappingConfig";
22
22
  import { customBracketClosingConfig } from "../editor/plugins/customBracketClosingConfig";
23
23
  import { editorKeywordSearchPlugin, searchOptionsFacet } from "../editor/plugins/codemirror-editor-plugins/editorKeywordSearchPlugin";
24
+ import { createEditorReactivityExtension } from "../composables/useEditorUtils";
24
25
  import { ref, shallowRef, computed, onMounted, onBeforeUnmount, unref, watch } from "vue";
25
26
  const doc = defineModel({ type: String });
26
27
  const props = defineProps({
@@ -36,6 +37,7 @@ const props = defineProps({
36
37
  const emit = defineEmits(["internal-link-click", "external-link-click"]);
37
38
  const extensions = shallowRef([]);
38
39
  const view = shallowRef();
40
+ const editorInstance = shallowRef();
39
41
  const ast = ref([]);
40
42
  const internalLinkCompartment = new Compartment();
41
43
  const specialCodeBlockCompartment = new Compartment();
@@ -52,11 +54,18 @@ async function loadLanguage(info) {
52
54
  if (lang) {
53
55
  return await lang.load();
54
56
  }
57
+ return null;
55
58
  }
56
59
  onMounted(() => {
57
60
  const wysiwygPlugin = wysiwyg({
58
61
  lezer: {
59
- codeLanguages: loadLanguage
62
+ codeLanguages: async (info) => {
63
+ const result = await loadLanguage(info);
64
+ if (result === null) {
65
+ return null;
66
+ }
67
+ return result;
68
+ }
60
69
  }
61
70
  });
62
71
  extensions.value = [
@@ -76,7 +85,8 @@ onMounted(() => {
76
85
  editorKeywordSearchPlugin,
77
86
  searchCompartment.of(searchOptionsFacet.of(props.searchOptions || { query: "" })),
78
87
  wysiwygPlugin,
79
- EditorView.editable.of(unref(!props.disabled))
88
+ EditorView.editable.of(unref(!props.disabled)),
89
+ createEditorReactivityExtension(editorInstance)
80
90
  ];
81
91
  if (editorElement.value) {
82
92
  editorElement.value.addEventListener("internal-link-click", handleInternalLinkClick);
@@ -150,6 +160,7 @@ watch(
150
160
  );
151
161
  function handleReady(payload) {
152
162
  view.value = payload.view;
163
+ editorInstance.value = payload;
153
164
  }
154
165
  function log(...args) {
155
166
  }
@@ -1,5 +1,9 @@
1
1
  import { type Ref } from 'vue';
2
2
  export declare function useEditorFrontmatter<T extends object = {}>(editor: Ref<any>): {
3
+ frontmatter: import("vue").ComputedRef<{
4
+ data?: T;
5
+ error?: Error;
6
+ }>;
3
7
  getFrontmatter: () => {
4
8
  data?: T;
5
9
  error?: Error;
@@ -7,6 +11,6 @@ export declare function useEditorFrontmatter<T extends object = {}>(editor: Ref<
7
11
  updateFrontmatterProperties: (properties: Partial<T>) => boolean;
8
12
  setFrontmatterProperties: (properties: Partial<T>) => boolean;
9
13
  clearFrontmatter: () => boolean;
10
- addFrontmatterProperty: (key: string, value: any) => void;
11
- removeFrontmatterProperty: (key: string) => void;
14
+ addFrontmatterProperty: (key: string, value: any) => boolean;
15
+ removeFrontmatterProperty: (key: string) => boolean;
12
16
  };
@@ -1,24 +1,36 @@
1
+ import { computed } from "vue";
1
2
  import { dump } from "js-yaml";
2
3
  import { useEditorUtils } from "./useEditorUtils.js";
3
4
  import { parseFrontmatter } from "../utils/frontmatter.js";
4
5
  export function useEditorFrontmatter(editor) {
5
6
  const editorUtils = useEditorUtils(editor);
6
- function getFrontmatter() {
7
+ const frontmatter = computed(() => {
7
8
  try {
8
- const doc = editorUtils.getDoc();
9
+ const doc = editorUtils.doc.value;
9
10
  if (!doc) {
10
- return { error: new Error("No document object found") };
11
+ return {};
11
12
  }
12
13
  return parseFrontmatter(doc);
13
14
  } catch (e) {
14
- console.log(e);
15
+ console.error("Error parsing frontmatter:", e);
15
16
  return { error: e };
16
17
  }
18
+ });
19
+ function getFrontmatter() {
20
+ return frontmatter.value;
17
21
  }
18
22
  function updateFrontmatterProperties(properties) {
19
23
  try {
20
- const doc = editorUtils.getDoc() || "";
21
- const ast = editorUtils.parseMarkdownToAST(doc);
24
+ const doc = editorUtils.getDoc();
25
+ if (!doc) {
26
+ console.warn("Editor not initialized or document is empty");
27
+ return false;
28
+ }
29
+ const ast = editorUtils.getDocAst();
30
+ if (!ast) {
31
+ console.warn("Failed to parse document AST");
32
+ return false;
33
+ }
22
34
  const firstNode = ast.topNode.firstChild;
23
35
  let existingData = {};
24
36
  if (firstNode && (firstNode.name === "Frontmatter" || firstNode.name === "YAMLFrontMatter")) {
@@ -30,20 +42,16 @@ export function useEditorFrontmatter(editor) {
30
42
  const newData = { ...existingData, ...properties };
31
43
  return setFrontmatterProperties(newData);
32
44
  } catch (e) {
33
- console.log(e);
45
+ console.error("Error updating frontmatter properties:", e);
34
46
  return false;
35
47
  }
36
48
  }
37
49
  function setFrontmatterProperties(properties) {
38
50
  try {
39
- const doc = editorUtils.getDoc() || "";
40
- const ast = editorUtils.parseMarkdownToAST(doc);
41
- const firstNode = ast.topNode.firstChild;
42
- let frontmatterNodeRange = { from: -1, to: -1 };
43
- if (firstNode && (firstNode.name === "Frontmatter" || firstNode.name === "YAMLFrontMatter")) {
44
- frontmatterNodeRange = { from: firstNode.from, to: firstNode.to };
45
- const { error } = getFrontmatter();
46
- if (error) return false;
51
+ const doc = editorUtils.getDoc();
52
+ if (doc === void 0) {
53
+ console.warn("Editor not initialized or document is empty");
54
+ return false;
47
55
  }
48
56
  const newData = { ...properties };
49
57
  Object.keys(newData).forEach((key) => {
@@ -52,15 +60,28 @@ export function useEditorFrontmatter(editor) {
52
60
  }
53
61
  });
54
62
  const hasContent = Object.keys(newData).length > 0;
63
+ let frontmatterStart = -1;
64
+ let frontmatterEnd = -1;
65
+ if (doc.startsWith("---\n") || doc.startsWith("---\r\n")) {
66
+ frontmatterStart = 0;
67
+ const searchStart = doc.indexOf("\n", 3) + 1;
68
+ const closingFenceIndex = doc.indexOf("\n---", searchStart);
69
+ if (closingFenceIndex !== -1) {
70
+ frontmatterEnd = closingFenceIndex + 4;
71
+ if (doc[frontmatterEnd] === "\n" || doc[frontmatterEnd] === "\r") {
72
+ }
73
+ }
74
+ }
75
+ const hasFrontmatter = frontmatterStart !== -1 && frontmatterEnd !== -1;
55
76
  if (!hasContent) {
56
- if (frontmatterNodeRange.from !== -1) {
57
- const endPos = frontmatterNodeRange.to;
58
- let removeEnd = endPos;
59
- if (doc[endPos] === "\n") removeEnd++;
60
- if (doc[endPos + 1] === "\n") removeEnd++;
77
+ if (hasFrontmatter) {
78
+ let removeEnd = frontmatterEnd;
79
+ if (doc[removeEnd] === "\n" || doc[removeEnd] === "\r") removeEnd++;
80
+ if (doc[removeEnd] === "\n" || doc[removeEnd] === "\r") removeEnd++;
61
81
  editorUtils.dispatch({
62
- changes: { from: frontmatterNodeRange.from, to: removeEnd, insert: "" }
82
+ changes: { from: frontmatterStart, to: removeEnd, insert: "" }
63
83
  });
84
+ return true;
64
85
  }
65
86
  return false;
66
87
  }
@@ -68,11 +89,11 @@ export function useEditorFrontmatter(editor) {
68
89
  const newFrontmatterBlock = `---
69
90
  ${newYamlContent}
70
91
  ---`;
71
- if (frontmatterNodeRange.from !== -1) {
92
+ if (hasFrontmatter) {
72
93
  editorUtils.dispatch({
73
94
  changes: {
74
- from: frontmatterNodeRange.from,
75
- to: frontmatterNodeRange.to,
95
+ from: frontmatterStart,
96
+ to: frontmatterEnd,
76
97
  insert: newFrontmatterBlock
77
98
  }
78
99
  });
@@ -87,37 +108,53 @@ ${newYamlContent}
87
108
  }
88
109
  return true;
89
110
  } catch (e) {
90
- console.log(e);
111
+ console.error("Error setting frontmatter properties:", e);
91
112
  return false;
92
113
  }
93
114
  }
94
115
  function clearFrontmatter() {
95
116
  try {
96
- const doc = editorUtils.getDoc() || "";
97
- const ast = editorUtils.parseMarkdownToAST(doc);
98
- const firstNode = ast.topNode.firstChild;
99
- if (firstNode && (firstNode.name === "Frontmatter" || firstNode.name === "YAMLFrontMatter")) {
100
- const endPos = firstNode.to;
101
- let removeEnd = endPos;
102
- if (doc[endPos] === "\n") removeEnd++;
103
- if (doc[endPos + 1] === "\n") removeEnd++;
117
+ const doc = editorUtils.getDoc();
118
+ if (!doc) {
119
+ console.warn("Editor not initialized or document is empty");
120
+ return false;
121
+ }
122
+ let frontmatterStart = -1;
123
+ let frontmatterEnd = -1;
124
+ if (doc.startsWith("---\n") || doc.startsWith("---\r\n")) {
125
+ frontmatterStart = 0;
126
+ const searchStart = doc.indexOf("\n", 3) + 1;
127
+ const closingFenceIndex = doc.indexOf("\n---", searchStart);
128
+ if (closingFenceIndex !== -1) {
129
+ frontmatterEnd = closingFenceIndex + 4;
130
+ }
131
+ }
132
+ if (frontmatterStart !== -1 && frontmatterEnd !== -1) {
133
+ let removeEnd = frontmatterEnd;
134
+ if (doc[removeEnd] === "\n" || doc[removeEnd] === "\r") removeEnd++;
135
+ if (doc[removeEnd] === "\n" || doc[removeEnd] === "\r") removeEnd++;
104
136
  editorUtils.dispatch({
105
- changes: { from: firstNode.from, to: removeEnd, insert: "" }
137
+ changes: { from: frontmatterStart, to: removeEnd, insert: "" }
106
138
  });
139
+ return true;
107
140
  }
108
141
  return true;
109
142
  } catch (e) {
110
- console.log(e);
143
+ console.error("Error clearing frontmatter:", e);
111
144
  return false;
112
145
  }
113
146
  }
114
147
  function addFrontmatterProperty(key, value) {
115
- updateFrontmatterProperties({ [key]: value });
148
+ return updateFrontmatterProperties({ [key]: value });
116
149
  }
117
150
  function removeFrontmatterProperty(key) {
118
- updateFrontmatterProperties({ [key]: void 0 });
151
+ return updateFrontmatterProperties({ [key]: void 0 });
119
152
  }
120
153
  return {
154
+ // Reactive properties
155
+ frontmatter,
156
+ // Reactive computed frontmatter data
157
+ // Methods
121
158
  getFrontmatter,
122
159
  updateFrontmatterProperties,
123
160
  setFrontmatterProperties,
@@ -1,29 +1,37 @@
1
1
  import type { SyntaxNode, Tree } from '@lezer/common';
2
2
  import type { Ref } from 'vue';
3
- import type { TransactionSpec } from '@codemirror/state';
3
+ import type { TransactionSpec, Extension } from '@codemirror/state';
4
4
  import { parseMarkdownToAST } from '../utils/markdownParser.js';
5
5
  import type { SearchMatch, SearchOptions } from '../editor/types/editor-types.js';
6
+ /**
7
+ * Creates a CodeMirror extension that enables reactive composables.
8
+ * This should be added to the editor's extensions array.
9
+ */
10
+ export declare function createEditorReactivityExtension(editorRef: Ref<any>): Extension;
6
11
  export declare function useEditorUtils(editor: Ref<any>): {
12
+ doc: import("vue").ComputedRef<any>;
13
+ view: import("vue").ComputedRef<any>;
14
+ searchResults: Ref<{
15
+ from: number;
16
+ to: number;
17
+ }[], SearchMatch[] | {
18
+ from: number;
19
+ to: number;
20
+ }[]>;
21
+ currentMatchIndex: Ref<number, number>;
22
+ triggerReactivity: () => void;
7
23
  getDoc: () => string | undefined;
8
24
  setDoc: (content: string) => void;
9
25
  getSelection: () => any;
10
26
  replaceSelection: (text: string) => void;
11
27
  dispatch: (...specs: TransactionSpec[]) => void;
12
28
  parseMarkdownToAST: typeof parseMarkdownToAST;
13
- getDocAst: () => Tree;
29
+ getDocAst: () => Tree | undefined;
14
30
  findNodesByType: (tree: Tree, nodeTypeName: string) => SyntaxNode[];
15
31
  getDocNodesByType: (nodeTypeName: string) => SyntaxNode[];
16
32
  hasFrontmatter: () => boolean;
17
33
  search: (options: SearchOptions) => void;
18
34
  replaceAll: (replacement: string) => void;
19
- searchResults: Ref<{
20
- from: number;
21
- to: number;
22
- }[], SearchMatch[] | {
23
- from: number;
24
- to: number;
25
- }[]>;
26
- currentMatchIndex: Ref<number, number>;
27
35
  findNext: () => void;
28
36
  findPrevious: () => void;
29
37
  replaceCurrent: (replacement: string) => void;
@@ -1,50 +1,94 @@
1
1
  import { computed, ref, unref } from "vue";
2
2
  import { EditorView } from "@codemirror/view";
3
3
  import { parseMarkdownToAST } from "../utils/markdownParser.js";
4
+ const reactivityCallbacks = /* @__PURE__ */ new WeakMap();
5
+ export function createEditorReactivityExtension(editorRef) {
6
+ return EditorView.updateListener.of((update) => {
7
+ if (update.docChanged) {
8
+ const callbacks = reactivityCallbacks.get(editorRef);
9
+ if (callbacks) {
10
+ callbacks.forEach((cb) => cb());
11
+ }
12
+ }
13
+ });
14
+ }
4
15
  export function useEditorUtils(editor) {
5
16
  const view = computed(() => {
6
17
  const instance = unref(editor);
7
18
  if (!instance) return;
8
19
  return instance.view ?? instance;
9
20
  });
21
+ const docVersion = ref(0);
22
+ if (!reactivityCallbacks.has(editor)) {
23
+ reactivityCallbacks.set(editor, /* @__PURE__ */ new Set());
24
+ }
25
+ const callbacks = reactivityCallbacks.get(editor);
26
+ const triggerReactivity = () => {
27
+ docVersion.value++;
28
+ };
29
+ callbacks.add(triggerReactivity);
30
+ const doc = computed(() => {
31
+ docVersion.value;
32
+ try {
33
+ const editorView = unref(view);
34
+ if (!editorView) return void 0;
35
+ return editorView.state.doc.toString();
36
+ } catch (e) {
37
+ console.error("Error getting document:", e);
38
+ return void 0;
39
+ }
40
+ });
10
41
  const searchResults = ref([]);
11
42
  const currentMatchIndex = ref(-1);
12
43
  const searchQuery = ref(null);
13
44
  function createSearchRegex(options) {
14
45
  return new RegExp(options.query, options.caseSensitive ? "g" : "gi");
15
46
  }
16
- function findAllMatches(doc, options) {
47
+ function findAllMatches(doc2, options) {
17
48
  const matches = [];
18
49
  const regex = createSearchRegex(options);
19
50
  let match;
20
- while ((match = regex.exec(doc)) !== null) {
51
+ while ((match = regex.exec(doc2)) !== null) {
21
52
  matches.push({ from: match.index, to: match.index + match[0].length });
22
53
  }
23
54
  return matches;
24
55
  }
25
56
  function getDoc() {
26
- try {
27
- return unref(view)?.state.doc.toString();
28
- } catch (e) {
29
- console.error(e);
30
- }
57
+ return doc.value;
31
58
  }
32
59
  function setDoc(content) {
33
- unref(view)?.dispatch({
34
- changes: { from: 0, to: unref(view).state.doc.length, insert: content }
60
+ const editorView = unref(view);
61
+ if (!editorView) {
62
+ console.warn("Editor not initialized");
63
+ return;
64
+ }
65
+ editorView.dispatch({
66
+ changes: { from: 0, to: editorView.state.doc.length, insert: content }
35
67
  });
36
68
  }
37
69
  function getSelection() {
38
70
  return unref(view)?.state.selection.main;
39
71
  }
40
72
  function replaceSelection(text) {
41
- unref(view)?.dispatch(unref(view).state.replaceSelection(text));
73
+ const editorView = unref(view);
74
+ if (!editorView) {
75
+ console.warn("Editor not initialized");
76
+ return;
77
+ }
78
+ editorView.dispatch(editorView.state.replaceSelection(text));
42
79
  }
43
80
  function dispatch(...specs) {
44
- unref(view)?.dispatch(...specs);
81
+ const editorView = unref(view);
82
+ if (!editorView) {
83
+ console.warn("Editor not initialized");
84
+ return;
85
+ }
86
+ editorView.dispatch(...specs);
45
87
  }
46
88
  function getDocAst() {
47
- return parseMarkdownToAST(getDoc() || "");
89
+ const docContent = getDoc();
90
+ if (!docContent) return void 0;
91
+ return parseMarkdownToAST(docContent);
48
92
  }
49
93
  function findNodesByType(tree, nodeTypeName) {
50
94
  const nodes = [];
@@ -58,7 +102,9 @@ export function useEditorUtils(editor) {
58
102
  return nodes;
59
103
  }
60
104
  function getDocNodesByType(nodeTypeName) {
61
- return findNodesByType(getDocAst(), nodeTypeName);
105
+ const ast = getDocAst();
106
+ if (!ast) return [];
107
+ return findNodesByType(ast, nodeTypeName);
62
108
  }
63
109
  function hasFrontmatter() {
64
110
  const ast = getDocAst();
@@ -68,13 +114,13 @@ export function useEditorUtils(editor) {
68
114
  }
69
115
  function search(options) {
70
116
  searchQuery.value = options;
71
- const doc = getDoc();
72
- if (!doc || !options.query) {
117
+ const doc2 = getDoc();
118
+ if (!doc2 || !options.query) {
73
119
  searchResults.value = [];
74
120
  currentMatchIndex.value = -1;
75
121
  return;
76
122
  }
77
- searchResults.value = findAllMatches(doc, options);
123
+ searchResults.value = findAllMatches(doc2, options);
78
124
  currentMatchIndex.value = -1;
79
125
  }
80
126
  function selectAndScrollToMatch(match, verticalScrollStrategy = "start", verticalMargin) {
@@ -122,9 +168,9 @@ export function useEditorUtils(editor) {
122
168
  }
123
169
  function replaceAll(replacement) {
124
170
  if (!searchQuery.value || !searchQuery.value.query) return;
125
- const doc = getDoc();
126
- if (!doc) return;
127
- const matches = findAllMatches(doc, searchQuery.value);
171
+ const doc2 = getDoc();
172
+ if (!doc2) return;
173
+ const matches = findAllMatches(doc2, searchQuery.value);
128
174
  if (matches.length === 0) return;
129
175
  const changes = matches.map((m) => ({
130
176
  from: m.from,
@@ -143,24 +189,36 @@ export function useEditorUtils(editor) {
143
189
  });
144
190
  }
145
191
  return {
192
+ // Reactive properties
193
+ doc,
194
+ // Reactive computed document content
195
+ view,
196
+ // Reactive computed editor view
197
+ searchResults,
198
+ currentMatchIndex,
199
+ // Reactivity helpers
200
+ triggerReactivity,
201
+ // Manual trigger for reactivity
202
+ // Document operations
146
203
  getDoc,
147
204
  setDoc,
148
205
  getSelection,
149
206
  replaceSelection,
150
207
  dispatch,
208
+ // AST operations
151
209
  parseMarkdownToAST,
152
210
  // Re-exported from utils for convenience
153
211
  getDocAst,
154
212
  findNodesByType,
155
213
  getDocNodesByType,
156
214
  hasFrontmatter,
215
+ // Search operations
157
216
  search,
158
217
  replaceAll,
159
- searchResults,
160
- currentMatchIndex,
161
218
  findNext,
162
219
  findPrevious,
163
220
  replaceCurrent,
221
+ // Scroll operations
164
222
  scrollToNode
165
223
  };
166
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@type32/codemirror-rich-obsidian-editor",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "OFM Editor Component for Nuxt.",
5
5
  "repository": "https://github.com/Type-32/codemirror-rich-obsidian",
6
6
  "license": "MIT",