@type32/codemirror-rich-obsidian-editor 0.1.20 → 0.1.22
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 +1 -224
- package/dist/module.json +1 -1
- package/dist/runtime/components/CodeEditor.client.vue +1 -6
- package/dist/runtime/components/Editor.client.vue +2 -13
- package/dist/runtime/composables/useEditorFrontmatter.d.ts +0 -4
- package/dist/runtime/composables/useEditorFrontmatter.js +31 -53
- package/dist/runtime/composables/useEditorUtils.d.ts +10 -18
- package/dist/runtime/composables/useEditorUtils.js +21 -79
- package/dist/runtime/utils/frontmatter.d.ts +4 -0
- package/dist/runtime/utils/frontmatter.js +15 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,228 +73,6 @@ 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
|
-
|
|
298
76
|
## Known Issues
|
|
299
77
|
- 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.
|
|
300
78
|
- 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.
|
|
@@ -314,8 +92,7 @@ const extensions = [
|
|
|
314
92
|
- We have a mapping prop that allows developers to add their own link-to-file implementations. (Specific to Vue/Nuxt)
|
|
315
93
|
- ~~Support for code-block mermaid graph rendering & bases is lacking.~~
|
|
316
94
|
- We have a mapping prop that allows developers to add their own custom codeblock widgets. (Specific to Vue/Nuxt)
|
|
317
|
-
-
|
|
318
|
-
- The `CodeEditor.client.vue` component now supports customizable light/dark themes with Catppuccin as the default.
|
|
95
|
+
- Light/Dark themes are not yet supported in code-block syntax highlighting.
|
|
319
96
|
|
|
320
97
|
## Contributions
|
|
321
98
|
- 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,7 +30,6 @@ 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";
|
|
34
33
|
const doc = defineModel({ type: String });
|
|
35
34
|
const props = defineProps({
|
|
36
35
|
class: { type: String, required: false },
|
|
@@ -47,7 +46,6 @@ const props = defineProps({
|
|
|
47
46
|
const emit = defineEmits([]);
|
|
48
47
|
const extensions = shallowRef([]);
|
|
49
48
|
const view = shallowRef();
|
|
50
|
-
const editorInstance = shallowRef();
|
|
51
49
|
const ast = ref([]);
|
|
52
50
|
const languageCompartment = new Compartment();
|
|
53
51
|
const themeCompartment = new Compartment();
|
|
@@ -128,9 +126,7 @@ onMounted(async () => {
|
|
|
128
126
|
// Keys related to the linter system
|
|
129
127
|
...lintKeymap
|
|
130
128
|
]),
|
|
131
|
-
EditorView.editable.of(!props.disabled)
|
|
132
|
-
// Reactivity extension for composables
|
|
133
|
-
createEditorReactivityExtension(editorInstance)
|
|
129
|
+
EditorView.editable.of(!props.disabled)
|
|
134
130
|
];
|
|
135
131
|
});
|
|
136
132
|
watch(
|
|
@@ -156,7 +152,6 @@ watch(
|
|
|
156
152
|
);
|
|
157
153
|
function handleReady(payload) {
|
|
158
154
|
view.value = payload.view;
|
|
159
|
-
editorInstance.value = payload;
|
|
160
155
|
}
|
|
161
156
|
function iterate() {
|
|
162
157
|
ast.value = [];
|
|
@@ -21,7 +21,6 @@ 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";
|
|
25
24
|
import { ref, shallowRef, computed, onMounted, onBeforeUnmount, unref, watch } from "vue";
|
|
26
25
|
const doc = defineModel({ type: String });
|
|
27
26
|
const props = defineProps({
|
|
@@ -37,7 +36,6 @@ const props = defineProps({
|
|
|
37
36
|
const emit = defineEmits(["internal-link-click", "external-link-click"]);
|
|
38
37
|
const extensions = shallowRef([]);
|
|
39
38
|
const view = shallowRef();
|
|
40
|
-
const editorInstance = shallowRef();
|
|
41
39
|
const ast = ref([]);
|
|
42
40
|
const internalLinkCompartment = new Compartment();
|
|
43
41
|
const specialCodeBlockCompartment = new Compartment();
|
|
@@ -54,18 +52,11 @@ async function loadLanguage(info) {
|
|
|
54
52
|
if (lang) {
|
|
55
53
|
return await lang.load();
|
|
56
54
|
}
|
|
57
|
-
return null;
|
|
58
55
|
}
|
|
59
56
|
onMounted(() => {
|
|
60
57
|
const wysiwygPlugin = wysiwyg({
|
|
61
58
|
lezer: {
|
|
62
|
-
codeLanguages:
|
|
63
|
-
const result = await loadLanguage(info);
|
|
64
|
-
if (result === null) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
59
|
+
codeLanguages: loadLanguage
|
|
69
60
|
}
|
|
70
61
|
});
|
|
71
62
|
extensions.value = [
|
|
@@ -85,8 +76,7 @@ onMounted(() => {
|
|
|
85
76
|
editorKeywordSearchPlugin,
|
|
86
77
|
searchCompartment.of(searchOptionsFacet.of(props.searchOptions || { query: "" })),
|
|
87
78
|
wysiwygPlugin,
|
|
88
|
-
EditorView.editable.of(unref(!props.disabled))
|
|
89
|
-
createEditorReactivityExtension(editorInstance)
|
|
79
|
+
EditorView.editable.of(unref(!props.disabled))
|
|
90
80
|
];
|
|
91
81
|
if (editorElement.value) {
|
|
92
82
|
editorElement.value.addEventListener("internal-link-click", handleInternalLinkClick);
|
|
@@ -160,7 +150,6 @@ watch(
|
|
|
160
150
|
);
|
|
161
151
|
function handleReady(payload) {
|
|
162
152
|
view.value = payload.view;
|
|
163
|
-
editorInstance.value = payload;
|
|
164
153
|
}
|
|
165
154
|
function log(...args) {
|
|
166
155
|
}
|
|
@@ -1,39 +1,26 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
2
1
|
import { dump } from "js-yaml";
|
|
3
2
|
import { useEditorUtils } from "./useEditorUtils.js";
|
|
4
3
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
5
4
|
export function useEditorFrontmatter(editor) {
|
|
6
5
|
const editorUtils = useEditorUtils(editor);
|
|
7
|
-
|
|
6
|
+
function getFrontmatter() {
|
|
8
7
|
try {
|
|
9
|
-
const doc = editorUtils.
|
|
8
|
+
const doc = editorUtils.getDoc();
|
|
10
9
|
if (!doc) {
|
|
11
|
-
return {};
|
|
10
|
+
return { error: new Error("No document object found") };
|
|
12
11
|
}
|
|
13
12
|
return parseFrontmatter(doc);
|
|
14
13
|
} catch (e) {
|
|
15
|
-
console.
|
|
14
|
+
console.log(e);
|
|
16
15
|
return { error: e };
|
|
17
16
|
}
|
|
18
|
-
});
|
|
19
|
-
function getFrontmatter() {
|
|
20
|
-
return frontmatter.value;
|
|
21
17
|
}
|
|
22
18
|
function updateFrontmatterProperties(properties) {
|
|
23
19
|
try {
|
|
24
20
|
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
|
-
}
|
|
34
|
-
const firstNode = ast.topNode.firstChild;
|
|
21
|
+
if (!doc) return false;
|
|
35
22
|
let existingData = {};
|
|
36
|
-
if (
|
|
23
|
+
if (doc.startsWith("---\n") || doc.startsWith("---\r\n")) {
|
|
37
24
|
const { data, error } = getFrontmatter();
|
|
38
25
|
if (data && !error) {
|
|
39
26
|
existingData = data;
|
|
@@ -42,17 +29,14 @@ export function useEditorFrontmatter(editor) {
|
|
|
42
29
|
const newData = { ...existingData, ...properties };
|
|
43
30
|
return setFrontmatterProperties(newData);
|
|
44
31
|
} catch (e) {
|
|
45
|
-
console.error("Error updating frontmatter
|
|
32
|
+
console.error("Error updating frontmatter:", e);
|
|
46
33
|
return false;
|
|
47
34
|
}
|
|
48
35
|
}
|
|
49
36
|
function setFrontmatterProperties(properties) {
|
|
50
37
|
try {
|
|
51
38
|
const doc = editorUtils.getDoc();
|
|
52
|
-
if (doc === void 0)
|
|
53
|
-
console.warn("Editor not initialized or document is empty");
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
39
|
+
if (doc === void 0) return false;
|
|
56
40
|
const newData = { ...properties };
|
|
57
41
|
Object.keys(newData).forEach((key) => {
|
|
58
42
|
if (newData[key] === void 0) {
|
|
@@ -64,11 +48,13 @@ export function useEditorFrontmatter(editor) {
|
|
|
64
48
|
let frontmatterEnd = -1;
|
|
65
49
|
if (doc.startsWith("---\n") || doc.startsWith("---\r\n")) {
|
|
66
50
|
frontmatterStart = 0;
|
|
67
|
-
const
|
|
68
|
-
const closingFenceIndex = doc.indexOf("\n---",
|
|
51
|
+
const yamlStart = doc.indexOf("\n", 3) + 1;
|
|
52
|
+
const closingFenceIndex = doc.indexOf("\n---", yamlStart);
|
|
69
53
|
if (closingFenceIndex !== -1) {
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
const afterFence = closingFenceIndex + 4;
|
|
55
|
+
const nextChar = doc[afterFence];
|
|
56
|
+
if (nextChar === void 0 || nextChar === "\n" || nextChar === "\r") {
|
|
57
|
+
frontmatterEnd = afterFence;
|
|
72
58
|
}
|
|
73
59
|
}
|
|
74
60
|
}
|
|
@@ -108,36 +94,32 @@ ${newYamlContent}
|
|
|
108
94
|
}
|
|
109
95
|
return true;
|
|
110
96
|
} catch (e) {
|
|
111
|
-
console.error("Error setting frontmatter
|
|
97
|
+
console.error("Error setting frontmatter:", e);
|
|
112
98
|
return false;
|
|
113
99
|
}
|
|
114
100
|
}
|
|
115
101
|
function clearFrontmatter() {
|
|
116
102
|
try {
|
|
117
103
|
const doc = editorUtils.getDoc();
|
|
118
|
-
if (!doc)
|
|
119
|
-
|
|
120
|
-
return
|
|
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
|
-
}
|
|
104
|
+
if (!doc) return false;
|
|
105
|
+
if (!doc.startsWith("---\n") && !doc.startsWith("---\r\n")) {
|
|
106
|
+
return true;
|
|
131
107
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
const yamlStart = doc.indexOf("\n", 3) + 1;
|
|
109
|
+
if (yamlStart === 0) return true;
|
|
110
|
+
const closingFenceIndex = doc.indexOf("\n---", yamlStart);
|
|
111
|
+
if (closingFenceIndex === -1) return true;
|
|
112
|
+
const afterFence = closingFenceIndex + 4;
|
|
113
|
+
const nextChar = doc[afterFence];
|
|
114
|
+
if (nextChar !== void 0 && nextChar !== "\n" && nextChar !== "\r") {
|
|
139
115
|
return true;
|
|
140
116
|
}
|
|
117
|
+
let removeEnd = afterFence;
|
|
118
|
+
if (doc[removeEnd] === "\n" || doc[removeEnd] === "\r") removeEnd++;
|
|
119
|
+
if (doc[removeEnd] === "\n" || doc[removeEnd] === "\r") removeEnd++;
|
|
120
|
+
editorUtils.dispatch({
|
|
121
|
+
changes: { from: 0, to: removeEnd, insert: "" }
|
|
122
|
+
});
|
|
141
123
|
return true;
|
|
142
124
|
} catch (e) {
|
|
143
125
|
console.error("Error clearing frontmatter:", e);
|
|
@@ -151,10 +133,6 @@ ${newYamlContent}
|
|
|
151
133
|
return updateFrontmatterProperties({ [key]: void 0 });
|
|
152
134
|
}
|
|
153
135
|
return {
|
|
154
|
-
// Reactive properties
|
|
155
|
-
frontmatter,
|
|
156
|
-
// Reactive computed frontmatter data
|
|
157
|
-
// Methods
|
|
158
136
|
getFrontmatter,
|
|
159
137
|
updateFrontmatterProperties,
|
|
160
138
|
setFrontmatterProperties,
|
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|
import type { SyntaxNode, Tree } from '@lezer/common';
|
|
2
2
|
import type { Ref } from 'vue';
|
|
3
|
-
import type { TransactionSpec
|
|
3
|
+
import type { TransactionSpec } 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;
|
|
11
6
|
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;
|
|
23
7
|
getDoc: () => string | undefined;
|
|
24
8
|
setDoc: (content: string) => void;
|
|
25
9
|
getSelection: () => any;
|
|
26
10
|
replaceSelection: (text: string) => void;
|
|
27
11
|
dispatch: (...specs: TransactionSpec[]) => void;
|
|
28
12
|
parseMarkdownToAST: typeof parseMarkdownToAST;
|
|
29
|
-
getDocAst: () => Tree
|
|
13
|
+
getDocAst: () => Tree;
|
|
30
14
|
findNodesByType: (tree: Tree, nodeTypeName: string) => SyntaxNode[];
|
|
31
15
|
getDocNodesByType: (nodeTypeName: string) => SyntaxNode[];
|
|
32
16
|
hasFrontmatter: () => boolean;
|
|
33
17
|
search: (options: SearchOptions) => void;
|
|
34
18
|
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>;
|
|
35
27
|
findNext: () => void;
|
|
36
28
|
findPrevious: () => void;
|
|
37
29
|
replaceCurrent: (replacement: string) => void;
|
|
@@ -1,94 +1,50 @@
|
|
|
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
|
-
}
|
|
15
4
|
export function useEditorUtils(editor) {
|
|
16
5
|
const view = computed(() => {
|
|
17
6
|
const instance = unref(editor);
|
|
18
7
|
if (!instance) return;
|
|
19
8
|
return instance.view ?? instance;
|
|
20
9
|
});
|
|
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
|
-
});
|
|
41
10
|
const searchResults = ref([]);
|
|
42
11
|
const currentMatchIndex = ref(-1);
|
|
43
12
|
const searchQuery = ref(null);
|
|
44
13
|
function createSearchRegex(options) {
|
|
45
14
|
return new RegExp(options.query, options.caseSensitive ? "g" : "gi");
|
|
46
15
|
}
|
|
47
|
-
function findAllMatches(
|
|
16
|
+
function findAllMatches(doc, options) {
|
|
48
17
|
const matches = [];
|
|
49
18
|
const regex = createSearchRegex(options);
|
|
50
19
|
let match;
|
|
51
|
-
while ((match = regex.exec(
|
|
20
|
+
while ((match = regex.exec(doc)) !== null) {
|
|
52
21
|
matches.push({ from: match.index, to: match.index + match[0].length });
|
|
53
22
|
}
|
|
54
23
|
return matches;
|
|
55
24
|
}
|
|
56
25
|
function getDoc() {
|
|
57
|
-
|
|
26
|
+
try {
|
|
27
|
+
return unref(view)?.state.doc.toString();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(e);
|
|
30
|
+
}
|
|
58
31
|
}
|
|
59
32
|
function setDoc(content) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console.warn("Editor not initialized");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
editorView.dispatch({
|
|
66
|
-
changes: { from: 0, to: editorView.state.doc.length, insert: content }
|
|
33
|
+
unref(view)?.dispatch({
|
|
34
|
+
changes: { from: 0, to: unref(view).state.doc.length, insert: content }
|
|
67
35
|
});
|
|
68
36
|
}
|
|
69
37
|
function getSelection() {
|
|
70
38
|
return unref(view)?.state.selection.main;
|
|
71
39
|
}
|
|
72
40
|
function replaceSelection(text) {
|
|
73
|
-
|
|
74
|
-
if (!editorView) {
|
|
75
|
-
console.warn("Editor not initialized");
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
editorView.dispatch(editorView.state.replaceSelection(text));
|
|
41
|
+
unref(view)?.dispatch(unref(view).state.replaceSelection(text));
|
|
79
42
|
}
|
|
80
43
|
function dispatch(...specs) {
|
|
81
|
-
|
|
82
|
-
if (!editorView) {
|
|
83
|
-
console.warn("Editor not initialized");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
editorView.dispatch(...specs);
|
|
44
|
+
unref(view)?.dispatch(...specs);
|
|
87
45
|
}
|
|
88
46
|
function getDocAst() {
|
|
89
|
-
|
|
90
|
-
if (!docContent) return void 0;
|
|
91
|
-
return parseMarkdownToAST(docContent);
|
|
47
|
+
return parseMarkdownToAST(getDoc() || "");
|
|
92
48
|
}
|
|
93
49
|
function findNodesByType(tree, nodeTypeName) {
|
|
94
50
|
const nodes = [];
|
|
@@ -102,9 +58,7 @@ export function useEditorUtils(editor) {
|
|
|
102
58
|
return nodes;
|
|
103
59
|
}
|
|
104
60
|
function getDocNodesByType(nodeTypeName) {
|
|
105
|
-
|
|
106
|
-
if (!ast) return [];
|
|
107
|
-
return findNodesByType(ast, nodeTypeName);
|
|
61
|
+
return findNodesByType(getDocAst(), nodeTypeName);
|
|
108
62
|
}
|
|
109
63
|
function hasFrontmatter() {
|
|
110
64
|
const ast = getDocAst();
|
|
@@ -114,13 +68,13 @@ export function useEditorUtils(editor) {
|
|
|
114
68
|
}
|
|
115
69
|
function search(options) {
|
|
116
70
|
searchQuery.value = options;
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
71
|
+
const doc = getDoc();
|
|
72
|
+
if (!doc || !options.query) {
|
|
119
73
|
searchResults.value = [];
|
|
120
74
|
currentMatchIndex.value = -1;
|
|
121
75
|
return;
|
|
122
76
|
}
|
|
123
|
-
searchResults.value = findAllMatches(
|
|
77
|
+
searchResults.value = findAllMatches(doc, options);
|
|
124
78
|
currentMatchIndex.value = -1;
|
|
125
79
|
}
|
|
126
80
|
function selectAndScrollToMatch(match, verticalScrollStrategy = "start", verticalMargin) {
|
|
@@ -168,9 +122,9 @@ export function useEditorUtils(editor) {
|
|
|
168
122
|
}
|
|
169
123
|
function replaceAll(replacement) {
|
|
170
124
|
if (!searchQuery.value || !searchQuery.value.query) return;
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
173
|
-
const matches = findAllMatches(
|
|
125
|
+
const doc = getDoc();
|
|
126
|
+
if (!doc) return;
|
|
127
|
+
const matches = findAllMatches(doc, searchQuery.value);
|
|
174
128
|
if (matches.length === 0) return;
|
|
175
129
|
const changes = matches.map((m) => ({
|
|
176
130
|
from: m.from,
|
|
@@ -189,36 +143,24 @@ export function useEditorUtils(editor) {
|
|
|
189
143
|
});
|
|
190
144
|
}
|
|
191
145
|
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
|
|
203
146
|
getDoc,
|
|
204
147
|
setDoc,
|
|
205
148
|
getSelection,
|
|
206
149
|
replaceSelection,
|
|
207
150
|
dispatch,
|
|
208
|
-
// AST operations
|
|
209
151
|
parseMarkdownToAST,
|
|
210
152
|
// Re-exported from utils for convenience
|
|
211
153
|
getDocAst,
|
|
212
154
|
findNodesByType,
|
|
213
155
|
getDocNodesByType,
|
|
214
156
|
hasFrontmatter,
|
|
215
|
-
// Search operations
|
|
216
157
|
search,
|
|
217
158
|
replaceAll,
|
|
159
|
+
searchResults,
|
|
160
|
+
currentMatchIndex,
|
|
218
161
|
findNext,
|
|
219
162
|
findPrevious,
|
|
220
163
|
replaceCurrent,
|
|
221
|
-
// Scroll operations
|
|
222
164
|
scrollToNode
|
|
223
165
|
};
|
|
224
166
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Frontmatter } from '../editor/types/editor-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lightning-fast frontmatter parser using string operations instead of AST parsing.
|
|
4
|
+
* Optimized for performance - parses in microseconds instead of milliseconds.
|
|
5
|
+
*/
|
|
2
6
|
export declare function parseFrontmatter(markdownText: string): {
|
|
3
7
|
data?: Frontmatter;
|
|
4
8
|
error?: Error;
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { load } from "js-yaml";
|
|
2
|
-
import { parseMarkdownToAST } from "./markdownParser.js";
|
|
3
2
|
export function parseFrontmatter(markdownText) {
|
|
4
3
|
if (!markdownText) {
|
|
5
4
|
return { error: new Error("No markdown text provided") };
|
|
6
5
|
}
|
|
7
|
-
|
|
8
|
-
const firstNode = tree.topNode.firstChild;
|
|
9
|
-
if (!firstNode || firstNode.name !== "YAMLFrontMatter") {
|
|
6
|
+
if (!markdownText.startsWith("---\n") && !markdownText.startsWith("---\r\n")) {
|
|
10
7
|
return { data: {} };
|
|
11
8
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
9
|
+
const yamlStart = markdownText.indexOf("\n", 3) + 1;
|
|
10
|
+
if (yamlStart === 0) {
|
|
11
|
+
return { data: {} };
|
|
12
|
+
}
|
|
13
|
+
const closingFenceIndex = markdownText.indexOf("\n---", yamlStart);
|
|
14
|
+
if (closingFenceIndex === -1) {
|
|
15
|
+
return { data: {} };
|
|
16
|
+
}
|
|
17
|
+
const afterFence = closingFenceIndex + 4;
|
|
18
|
+
const nextChar = markdownText[afterFence];
|
|
19
|
+
if (nextChar !== void 0 && nextChar !== "\n" && nextChar !== "\r") {
|
|
20
|
+
return { data: {} };
|
|
21
|
+
}
|
|
22
|
+
const yamlContent = markdownText.slice(yamlStart, closingFenceIndex);
|
|
14
23
|
try {
|
|
15
24
|
const data = load(yamlContent);
|
|
16
25
|
if (data === null || data === void 0) {
|
package/package.json
CHANGED