@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 +224 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/CodeEditor.client.vue +6 -1
- package/dist/runtime/components/Editor.client.vue +13 -2
- package/dist/runtime/composables/useEditorFrontmatter.d.ts +6 -2
- package/dist/runtime/composables/useEditorFrontmatter.js +74 -37
- package/dist/runtime/composables/useEditorUtils.d.ts +18 -10
- package/dist/runtime/composables/useEditorUtils.js +79 -21
- package/package.json +1 -1
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
|
@@ -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:
|
|
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) =>
|
|
11
|
-
removeFrontmatterProperty: (key: string) =>
|
|
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
|
-
|
|
7
|
+
const frontmatter = computed(() => {
|
|
7
8
|
try {
|
|
8
|
-
const doc = editorUtils.
|
|
9
|
+
const doc = editorUtils.doc.value;
|
|
9
10
|
if (!doc) {
|
|
10
|
-
return {
|
|
11
|
+
return {};
|
|
11
12
|
}
|
|
12
13
|
return parseFrontmatter(doc);
|
|
13
14
|
} catch (e) {
|
|
14
|
-
console.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (doc[
|
|
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:
|
|
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 (
|
|
92
|
+
if (hasFrontmatter) {
|
|
72
93
|
editorUtils.dispatch({
|
|
73
94
|
changes: {
|
|
74
|
-
from:
|
|
75
|
-
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.
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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:
|
|
137
|
+
changes: { from: frontmatterStart, to: removeEnd, insert: "" }
|
|
106
138
|
});
|
|
139
|
+
return true;
|
|
107
140
|
}
|
|
108
141
|
return true;
|
|
109
142
|
} catch (e) {
|
|
110
|
-
console.
|
|
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(
|
|
47
|
+
function findAllMatches(doc2, options) {
|
|
17
48
|
const matches = [];
|
|
18
49
|
const regex = createSearchRegex(options);
|
|
19
50
|
let match;
|
|
20
|
-
while ((match = regex.exec(
|
|
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
|
-
|
|
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)
|
|
34
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
if (!
|
|
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(
|
|
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
|
|
126
|
-
if (!
|
|
127
|
-
const matches = findAllMatches(
|
|
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