@type32/codemirror-rich-obsidian-editor 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/Editor/ImageEmbedComponent.vue.d.ts +2 -1
- package/dist/runtime/components/Editor/TestCustomCodeBlock.vue.d.ts +2 -1
- package/dist/runtime/components/Editor.client.vue.d.ts +4 -2
- package/dist/runtime/composables/useDocumentUtils.js +3 -9
- package/dist/runtime/composables/useEditorFrontmatter.js +3 -20
- package/dist/runtime/composables/useEditorUtils.d.ts +2 -1
- package/dist/runtime/composables/useEditorUtils.js +16 -25
- package/dist/runtime/editor/plugins/codemirror-editor-plugins/editorInternalLinkAutocompletePlugin.js +11 -10
- package/dist/runtime/editor/plugins/codemirror-editor-plugins/editorLinkClickPlugin.js +3 -3
- package/dist/runtime/editor/plugins/codemirror-plugin-proses/createProsePlugin.d.ts +53 -0
- package/dist/runtime/editor/plugins/codemirror-plugin-proses/createProsePlugin.js +23 -0
- package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseHashtagCodemirrorViewPlugin.d.ts +1 -3
- package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseHashtagCodemirrorViewPlugin.js +5 -13
- package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseInternalLinkCodemirrorViewPlugin.d.ts +0 -3
- package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseInternalLinkCodemirrorViewPlugin.js +5 -12
- package/dist/runtime/editor/plugins/richTextPlugin.js +37 -35
- package/dist/runtime/editor/types/editor-types.d.ts +49 -9
- package/dist/runtime/utils/frontmatter.js +2 -6
- package/dist/runtime/utils/internalLinks.js +2 -6
- package/dist/runtime/utils/markdownParser.d.ts +12 -0
- package/dist/runtime/utils/markdownParser.js +11 -0
- package/package.json +13 -14
package/dist/module.json
CHANGED
|
@@ -2,5 +2,6 @@ type __VLS_Props = {
|
|
|
2
2
|
filePath: string;
|
|
3
3
|
display?: string;
|
|
4
4
|
};
|
|
5
|
-
declare const
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
6
7
|
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
codeContent?: string;
|
|
3
3
|
};
|
|
4
|
-
declare const
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
5
|
+
declare const _default: typeof __VLS_export;
|
|
5
6
|
export default _default;
|
|
@@ -10,10 +10,11 @@ type __VLS_Props = {
|
|
|
10
10
|
debug?: boolean;
|
|
11
11
|
searchOptions?: SearchOptions;
|
|
12
12
|
};
|
|
13
|
-
type
|
|
13
|
+
type __VLS_ModelProps = {
|
|
14
14
|
modelValue?: string;
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
17
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
17
18
|
view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
|
|
18
19
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
20
|
"internal-link-click": (detail: InternalLinkClickDetail) => any;
|
|
@@ -24,4 +25,5 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
24
25
|
"onExternal-link-click"?: ((detail: ExternalLinkClickDetail) => any) | undefined;
|
|
25
26
|
"onUpdate:modelValue"?: ((value: string | undefined) => any) | undefined;
|
|
26
27
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
|
+
declare const _default: typeof __VLS_export;
|
|
27
29
|
export default _default;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { useAlfaaz } from "./useAlfaaz.js";
|
|
2
|
-
import {
|
|
3
|
-
import { GFM } from "@lezer/markdown";
|
|
4
|
-
import { CustomOFM } from "../editor/lezer-parsers/customOFMParsers.js";
|
|
2
|
+
import { parseMarkdownToAST } from "../utils/markdownParser.js";
|
|
5
3
|
export function useDocumentUtils() {
|
|
6
4
|
const alfaaz = useAlfaaz();
|
|
7
5
|
function getWordCount(text) {
|
|
@@ -32,9 +30,7 @@ export function useDocumentUtils() {
|
|
|
32
30
|
function getTableOfContents(text) {
|
|
33
31
|
const toc = [];
|
|
34
32
|
if (!text) return toc;
|
|
35
|
-
const tree =
|
|
36
|
-
extensions: [GFM, CustomOFM, { remove: ["SetextHeading"] }]
|
|
37
|
-
}).language.parser.parse(text);
|
|
33
|
+
const tree = parseMarkdownToAST(text);
|
|
38
34
|
tree.iterate({
|
|
39
35
|
enter: (node) => {
|
|
40
36
|
if (node.name.startsWith("ATXHeading")) {
|
|
@@ -61,9 +57,7 @@ export function useDocumentUtils() {
|
|
|
61
57
|
function getAllTags(text) {
|
|
62
58
|
const tags = [];
|
|
63
59
|
if (!text) return tags;
|
|
64
|
-
const tree =
|
|
65
|
-
extensions: [GFM, CustomOFM, { remove: ["SetextHeading"] }]
|
|
66
|
-
}).language.parser.parse(text);
|
|
60
|
+
const tree = parseMarkdownToAST(text);
|
|
67
61
|
let frontmatterEnd = 0;
|
|
68
62
|
const frontmatterNode = tree.topNode.firstChild;
|
|
69
63
|
if (frontmatterNode && frontmatterNode.name === "Frontmatter") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dump } from "js-yaml";
|
|
2
2
|
import { useEditorUtils } from "./useEditorUtils.js";
|
|
3
|
+
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
3
4
|
export function useEditorFrontmatter(editor) {
|
|
4
5
|
const editorUtils = useEditorUtils(editor);
|
|
5
6
|
function getFrontmatter() {
|
|
@@ -7,25 +8,7 @@ export function useEditorFrontmatter(editor) {
|
|
|
7
8
|
if (!doc) {
|
|
8
9
|
return {};
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
const firstNode = ast.topNode.firstChild;
|
|
12
|
-
if (!firstNode || firstNode.name !== "YAMLFrontMatter") {
|
|
13
|
-
return {};
|
|
14
|
-
}
|
|
15
|
-
const contentNode = firstNode.getChild("YAMLContent");
|
|
16
|
-
const yamlContent = contentNode ? doc.slice(contentNode.from, contentNode.to) : "";
|
|
17
|
-
try {
|
|
18
|
-
const data = load(yamlContent);
|
|
19
|
-
if (data === null || data === void 0) {
|
|
20
|
-
return { data: {} };
|
|
21
|
-
}
|
|
22
|
-
if (typeof data === "object") {
|
|
23
|
-
return { data };
|
|
24
|
-
}
|
|
25
|
-
return { error: new Error("Frontmatter is not a valid object.") };
|
|
26
|
-
} catch (e) {
|
|
27
|
-
return { error: e };
|
|
28
|
-
}
|
|
11
|
+
return parseFrontmatter(doc);
|
|
29
12
|
}
|
|
30
13
|
function setFrontmatterProperties(properties) {
|
|
31
14
|
const doc = editorUtils.getDoc() || "";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SyntaxNode, Tree } from '@lezer/common';
|
|
2
2
|
import type { Ref } from 'vue';
|
|
3
3
|
import type { TransactionSpec } from '@codemirror/state';
|
|
4
|
+
import { parseMarkdownToAST } from '../utils/markdownParser.js';
|
|
4
5
|
import type { SearchMatch, SearchOptions } from '../editor/types/editor-types.js';
|
|
5
6
|
export declare function useEditorUtils(editor: Ref<any>): {
|
|
6
7
|
getDoc: () => string | undefined;
|
|
@@ -8,7 +9,7 @@ export declare function useEditorUtils(editor: Ref<any>): {
|
|
|
8
9
|
getSelection: () => any;
|
|
9
10
|
replaceSelection: (text: string) => void;
|
|
10
11
|
dispatch: (...specs: TransactionSpec[]) => void;
|
|
11
|
-
parseMarkdownToAST:
|
|
12
|
+
parseMarkdownToAST: typeof parseMarkdownToAST;
|
|
12
13
|
getDocAst: () => Tree;
|
|
13
14
|
findNodesByType: (tree: Tree, nodeTypeName: string) => SyntaxNode[];
|
|
14
15
|
getDocNodesByType: (nodeTypeName: string) => SyntaxNode[];
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { computed, ref, unref } from "vue";
|
|
2
2
|
import { EditorView } from "@codemirror/view";
|
|
3
|
-
import {
|
|
4
|
-
import { CustomOFM } from "../../runtime/editor/lezer-parsers/customOFMParsers";
|
|
5
|
-
import { GFM } from "@lezer/markdown";
|
|
3
|
+
import { parseMarkdownToAST } from "../utils/markdownParser.js";
|
|
6
4
|
export function useEditorUtils(editor) {
|
|
7
5
|
const view = computed(() => {
|
|
8
6
|
const instance = unref(editor);
|
|
@@ -12,6 +10,18 @@ export function useEditorUtils(editor) {
|
|
|
12
10
|
const searchResults = ref([]);
|
|
13
11
|
const currentMatchIndex = ref(-1);
|
|
14
12
|
const searchQuery = ref(null);
|
|
13
|
+
function createSearchRegex(options) {
|
|
14
|
+
return new RegExp(options.query, options.caseSensitive ? "g" : "gi");
|
|
15
|
+
}
|
|
16
|
+
function findAllMatches(doc, options) {
|
|
17
|
+
const matches = [];
|
|
18
|
+
const regex = createSearchRegex(options);
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = regex.exec(doc)) !== null) {
|
|
21
|
+
matches.push({ from: match.index, to: match.index + match[0].length });
|
|
22
|
+
}
|
|
23
|
+
return matches;
|
|
24
|
+
}
|
|
15
25
|
function getDoc() {
|
|
16
26
|
return unref(view)?.state.doc.toString();
|
|
17
27
|
}
|
|
@@ -29,15 +39,6 @@ export function useEditorUtils(editor) {
|
|
|
29
39
|
function dispatch(...specs) {
|
|
30
40
|
unref(view)?.dispatch(...specs);
|
|
31
41
|
}
|
|
32
|
-
function parseMarkdownToAST(markdownText) {
|
|
33
|
-
return markdown({
|
|
34
|
-
extensions: [
|
|
35
|
-
GFM,
|
|
36
|
-
CustomOFM,
|
|
37
|
-
{ remove: ["SetextHeading"] }
|
|
38
|
-
]
|
|
39
|
-
}).language.parser.parse(markdownText);
|
|
40
|
-
}
|
|
41
42
|
function getDocAst() {
|
|
42
43
|
return parseMarkdownToAST(getDoc() || "");
|
|
43
44
|
}
|
|
@@ -69,13 +70,7 @@ export function useEditorUtils(editor) {
|
|
|
69
70
|
currentMatchIndex.value = -1;
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
const regex = new RegExp(options.query, options.caseSensitive ? "g" : "gi");
|
|
74
|
-
let match;
|
|
75
|
-
while ((match = regex.exec(doc)) !== null) {
|
|
76
|
-
matches.push({ from: match.index, to: match.index + match[0].length });
|
|
77
|
-
}
|
|
78
|
-
searchResults.value = matches;
|
|
73
|
+
searchResults.value = findAllMatches(doc, options);
|
|
79
74
|
currentMatchIndex.value = -1;
|
|
80
75
|
}
|
|
81
76
|
function selectAndScrollToMatch(match) {
|
|
@@ -125,12 +120,7 @@ export function useEditorUtils(editor) {
|
|
|
125
120
|
if (!searchQuery.value || !searchQuery.value.query) return;
|
|
126
121
|
const doc = getDoc();
|
|
127
122
|
if (!doc) return;
|
|
128
|
-
const matches =
|
|
129
|
-
const regex = new RegExp(searchQuery.value.query, searchQuery.value.caseSensitive ? "g" : "gi");
|
|
130
|
-
let match;
|
|
131
|
-
while ((match = regex.exec(doc)) !== null) {
|
|
132
|
-
matches.push({ from: match.index, to: match.index + match[0].length });
|
|
133
|
-
}
|
|
123
|
+
const matches = findAllMatches(doc, searchQuery.value);
|
|
134
124
|
if (matches.length === 0) return;
|
|
135
125
|
const changes = matches.map((m) => ({
|
|
136
126
|
from: m.from,
|
|
@@ -155,6 +145,7 @@ export function useEditorUtils(editor) {
|
|
|
155
145
|
replaceSelection,
|
|
156
146
|
dispatch,
|
|
157
147
|
parseMarkdownToAST,
|
|
148
|
+
// Re-exported from utils for convenience
|
|
158
149
|
getDocAst,
|
|
159
150
|
findNodesByType,
|
|
160
151
|
getDocNodesByType,
|
|
@@ -27,30 +27,31 @@ function internalLinkSource(context) {
|
|
|
27
27
|
const linkMap = context.state.facet(internalLinkMapFacet);
|
|
28
28
|
const nameCounts = /* @__PURE__ */ new Map();
|
|
29
29
|
linkMap.forEach((link) => {
|
|
30
|
-
nameCounts.set(link.
|
|
30
|
+
nameCounts.set(link.name, (nameCounts.get(link.name) || 0) + 1);
|
|
31
31
|
});
|
|
32
32
|
const searchString = textBefore.toLowerCase();
|
|
33
33
|
const options = linkMap.filter(
|
|
34
|
-
(link) => link.
|
|
34
|
+
(link) => link.name && link.name.toLowerCase().includes(searchString) || link.filePath && link.filePath.toLowerCase().includes(searchString)
|
|
35
35
|
).map((link) => {
|
|
36
|
-
|
|
36
|
+
if (!link.name) return null;
|
|
37
|
+
const isDuplicate = (nameCounts.get(link.name) || 0) > 1;
|
|
37
38
|
if (isDuplicate) {
|
|
38
39
|
return {
|
|
39
|
-
label: link.
|
|
40
|
-
detail: link.
|
|
41
|
-
apply: link.filePath || link.
|
|
40
|
+
label: link.name,
|
|
41
|
+
detail: link.filePath,
|
|
42
|
+
apply: link.filePath || link.name
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
return {
|
|
45
|
-
label: link.
|
|
46
|
+
label: link.name,
|
|
46
47
|
detail: link.filePath,
|
|
47
|
-
apply: `${link.
|
|
48
|
+
apply: `${link.name}`
|
|
48
49
|
};
|
|
49
|
-
});
|
|
50
|
+
}).filter(Boolean);
|
|
50
51
|
if (options.length === 0) return null;
|
|
51
52
|
return {
|
|
52
53
|
from,
|
|
53
|
-
options,
|
|
54
|
+
options: options || [],
|
|
54
55
|
validFor: /^[^\]|]*/
|
|
55
56
|
};
|
|
56
57
|
}
|
|
@@ -25,14 +25,14 @@ export const editorLinkClickPlugin = EditorView.domEventHandlers({
|
|
|
25
25
|
const path = anchor.dataset.path;
|
|
26
26
|
if (!path) return true;
|
|
27
27
|
const linkMap = view.state.facet(internalLinkMapFacet);
|
|
28
|
-
const linkInfo = linkMap.find((l) => l.
|
|
28
|
+
const linkInfo = linkMap.find((l) => l.name === path || l.filePath === path);
|
|
29
29
|
const type = anchor.dataset.type;
|
|
30
30
|
const detail = {
|
|
31
|
-
path,
|
|
31
|
+
target: path,
|
|
32
32
|
subpath: anchor.dataset.subpath,
|
|
33
33
|
display: anchor.dataset.display,
|
|
34
34
|
type: type || "internal-link",
|
|
35
|
-
|
|
35
|
+
referenceId: anchor.dataset.referenceId || linkInfo?.referenceId
|
|
36
36
|
};
|
|
37
37
|
view.dom.dispatchEvent(new CustomEvent("internal-link-click", {
|
|
38
38
|
bubbles: true,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Decoration, type DecorationSet } from '@codemirror/view';
|
|
2
|
+
import { StateField } from '@codemirror/state';
|
|
3
|
+
import type { EditorState, Range as EditorRange } from '@codemirror/state';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for creating a prose plugin
|
|
6
|
+
*/
|
|
7
|
+
export interface ProsePluginConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Function to build decorations from the editor state
|
|
10
|
+
*/
|
|
11
|
+
buildDecorations: (state: EditorState) => EditorRange<Decoration>[];
|
|
12
|
+
/**
|
|
13
|
+
* Whether to rebuild decorations on document changes (default: true)
|
|
14
|
+
*/
|
|
15
|
+
rebuildOnDocChange?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether to rebuild decorations on selection changes (default: false)
|
|
18
|
+
*/
|
|
19
|
+
rebuildOnSelection?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to sort the decorations (default: true)
|
|
22
|
+
*/
|
|
23
|
+
sortDecorations?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a CodeMirror StateField plugin for prose decorations with the standard pattern.
|
|
27
|
+
* This reduces boilerplate for simple prose plugins that follow the common pattern:
|
|
28
|
+
* - Build decorations from syntax tree
|
|
29
|
+
* - Update on document/selection changes
|
|
30
|
+
* - Provide decorations to editor view
|
|
31
|
+
*
|
|
32
|
+
* @param config Configuration object for the plugin
|
|
33
|
+
* @returns A CodeMirror StateField that can be added to the editor
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* export const myPlugin = createProsePlugin({
|
|
38
|
+
* buildDecorations: (state) => {
|
|
39
|
+
* const decorations: EditorRange<Decoration>[] = []
|
|
40
|
+
* syntaxTree(state).iterate({
|
|
41
|
+
* enter(node) {
|
|
42
|
+
* if (node.name === 'MyNode') {
|
|
43
|
+
* decorations.push(myDecoration.range(node.from, node.to))
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* })
|
|
47
|
+
* return decorations
|
|
48
|
+
* },
|
|
49
|
+
* rebuildOnSelection: true
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function createProsePlugin(config: ProsePluginConfig): StateField<DecorationSet>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
import { StateField, RangeSet } from "@codemirror/state";
|
|
3
|
+
export function createProsePlugin(config) {
|
|
4
|
+
const {
|
|
5
|
+
buildDecorations,
|
|
6
|
+
rebuildOnDocChange = true,
|
|
7
|
+
rebuildOnSelection = false,
|
|
8
|
+
sortDecorations = true
|
|
9
|
+
} = config;
|
|
10
|
+
return StateField.define({
|
|
11
|
+
create(state) {
|
|
12
|
+
return RangeSet.of(buildDecorations(state), sortDecorations);
|
|
13
|
+
},
|
|
14
|
+
update(value, tr) {
|
|
15
|
+
const shouldRebuild = rebuildOnDocChange && tr.docChanged || rebuildOnSelection && tr.selection;
|
|
16
|
+
if (shouldRebuild) {
|
|
17
|
+
return RangeSet.of(buildDecorations(tr.state), sortDecorations);
|
|
18
|
+
}
|
|
19
|
+
return value.map(tr.changes);
|
|
20
|
+
},
|
|
21
|
+
provide: (f) => EditorView.decorations.from(f)
|
|
22
|
+
});
|
|
23
|
+
}
|
package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseHashtagCodemirrorViewPlugin.d.ts
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { StateField } from '@codemirror/state';
|
|
3
|
-
export declare const proseHashtagCodemirrorViewPlugin: StateField<DecorationSet>;
|
|
1
|
+
export declare const proseHashtagCodemirrorViewPlugin: import("@codemirror/state").StateField<import("@codemirror/view").DecorationSet>;
|
package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseHashtagCodemirrorViewPlugin.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { EditorView } from "@codemirror/view";
|
|
2
|
-
import { StateField, RangeSet } from "@codemirror/state";
|
|
3
1
|
import { syntaxTree } from "@codemirror/language";
|
|
4
2
|
import { decorationProseHashtag } from "../../utility/decorations.js";
|
|
3
|
+
import { createProsePlugin } from "./createProsePlugin.js";
|
|
5
4
|
function buildHashtagWrappers(state) {
|
|
6
5
|
const decorations = [];
|
|
7
6
|
syntaxTree(state).iterate({
|
|
@@ -13,15 +12,8 @@ function buildHashtagWrappers(state) {
|
|
|
13
12
|
});
|
|
14
13
|
return decorations;
|
|
15
14
|
}
|
|
16
|
-
export const proseHashtagCodemirrorViewPlugin =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
update(value, tr) {
|
|
21
|
-
if (tr.docChanged) {
|
|
22
|
-
return RangeSet.of(buildHashtagWrappers(tr.state));
|
|
23
|
-
}
|
|
24
|
-
return value.map(tr.changes);
|
|
25
|
-
},
|
|
26
|
-
provide: (f) => EditorView.decorations.from(f)
|
|
15
|
+
export const proseHashtagCodemirrorViewPlugin = createProsePlugin({
|
|
16
|
+
buildDecorations: buildHashtagWrappers,
|
|
17
|
+
rebuildOnDocChange: true,
|
|
18
|
+
rebuildOnSelection: false
|
|
27
19
|
});
|
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
import { type DecorationSet } from '@codemirror/view';
|
|
2
2
|
import { StateField } from '@codemirror/state';
|
|
3
|
-
import type { EditorState } from '@codemirror/state';
|
|
4
|
-
declare function isNodeRangeActive(state: EditorState, nodeFrom: number, nodeTo: number): boolean;
|
|
5
|
-
export { isNodeRangeActive };
|
|
6
3
|
export declare const proseInternalLinkCodemirrorViewPlugin: StateField<DecorationSet>;
|
|
@@ -3,15 +3,7 @@ import { StateField, RangeSet } from "@codemirror/state";
|
|
|
3
3
|
import { syntaxTree } from "@codemirror/language";
|
|
4
4
|
import { internalLinkMapFacet } from "../linkMappingConfig.js";
|
|
5
5
|
import { ProseVueComponentEmbedWidget } from "../codemirror-widgets/proseVueComponentEmbedWidget.js";
|
|
6
|
-
import { cursorSelectionCoveredNode, toCursorNodePositions } from "../../utility/tools.js";
|
|
7
|
-
function isNodeRangeActive(state, nodeFrom, nodeTo) {
|
|
8
|
-
const cursor = state.selection.main;
|
|
9
|
-
if (cursor.empty) {
|
|
10
|
-
return cursor.from >= nodeFrom && cursor.from <= nodeTo;
|
|
11
|
-
} else {
|
|
12
|
-
return Math.max(nodeFrom, cursor.from) < Math.min(nodeTo, cursor.to);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
6
|
+
import { cursorSelectionCoveredNode, toCursorNodePositions, isNodeRangeActive } from "../../utility/tools.js";
|
|
15
7
|
function buildInternalLinkDecorations(state) {
|
|
16
8
|
const decorations = [];
|
|
17
9
|
const linkMap = state.facet(internalLinkMapFacet);
|
|
@@ -22,7 +14,7 @@ function buildInternalLinkDecorations(state) {
|
|
|
22
14
|
const pathNode = node.node.getChild("InternalLink")?.getChild("InternalPath");
|
|
23
15
|
if (pathNode) {
|
|
24
16
|
const path = state.doc.sliceString(pathNode.from, pathNode.to);
|
|
25
|
-
const linkInfo = linkMap.find((l) => l.
|
|
17
|
+
const linkInfo = linkMap.find((l) => l.name === path || l.filePath === path);
|
|
26
18
|
if (linkInfo?.embedComponent) {
|
|
27
19
|
const line = state.doc.lineAt(node.from);
|
|
28
20
|
const props = { linkData: linkInfo };
|
|
@@ -58,7 +50,7 @@ function buildInternalLinkDecorations(state) {
|
|
|
58
50
|
const aliasNode = contentContainerNode.getChild("InternalDisplay");
|
|
59
51
|
const subpath = subpathNode ? state.doc.sliceString(subpathNode.from, subpathNode.to) : void 0;
|
|
60
52
|
const alias = aliasNode ? state.doc.sliceString(aliasNode.from, aliasNode.to) : void 0;
|
|
61
|
-
const linkInfo = linkMap.find((l) => l.
|
|
53
|
+
const linkInfo = linkMap.find((l) => l.name === path || l.filePath === path);
|
|
62
54
|
if (node.name === "Embed" && linkInfo?.embedComponent) {
|
|
63
55
|
return false;
|
|
64
56
|
}
|
|
@@ -71,6 +63,8 @@ function buildInternalLinkDecorations(state) {
|
|
|
71
63
|
};
|
|
72
64
|
if (!linkInfo) {
|
|
73
65
|
linkAttributes["class"] += " cm-unresolved-link";
|
|
66
|
+
} else {
|
|
67
|
+
linkAttributes["data-reference-id"] = linkInfo.referenceId;
|
|
74
68
|
}
|
|
75
69
|
if (subpath) linkAttributes["data-subpath"] = subpath;
|
|
76
70
|
if (alias) linkAttributes["data-display"] = alias;
|
|
@@ -100,7 +94,6 @@ function buildInternalLinkDecorations(state) {
|
|
|
100
94
|
});
|
|
101
95
|
return [...decorations, ...widgets];
|
|
102
96
|
}
|
|
103
|
-
export { isNodeRangeActive };
|
|
104
97
|
export const proseInternalLinkCodemirrorViewPlugin = StateField.define({
|
|
105
98
|
create(state) {
|
|
106
99
|
return RangeSet.of(buildInternalLinkDecorations(state), true);
|
|
@@ -47,16 +47,20 @@ export default class RichEditPlugin {
|
|
|
47
47
|
}
|
|
48
48
|
update(update) {
|
|
49
49
|
if (update.docChanged) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
decorations = decorations.update
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
if (update.selectionSet) {
|
|
51
|
+
this.decorations = this.process(update.view);
|
|
52
|
+
} else {
|
|
53
|
+
let decorations = this.decorations.map(update.changes);
|
|
54
|
+
update.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
|
|
55
|
+
const newWidgets = this.processRange(update.view, fromB, toB);
|
|
56
|
+
decorations = decorations.update({
|
|
57
|
+
filter: (f, t) => f < fromB || t > toB,
|
|
58
|
+
add: newWidgets,
|
|
59
|
+
sort: true
|
|
60
|
+
});
|
|
57
61
|
});
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
this.decorations = decorations;
|
|
63
|
+
}
|
|
60
64
|
} else if (update.viewportChanged || update.selectionSet) {
|
|
61
65
|
this.decorations = this.process(update.view);
|
|
62
66
|
}
|
|
@@ -71,35 +75,33 @@ export default class RichEditPlugin {
|
|
|
71
75
|
processRange(view, from, to) {
|
|
72
76
|
const widgets = [];
|
|
73
77
|
const [cursor] = view.state.selection.ranges;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
if (cursorInNode(cursor?.from, cursor?.to, nodeFrom, nodeTo)) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const line = view.state.doc.lineAt(nodeFrom);
|
|
87
|
-
widgets.push(decorationHidden.range(nodeFrom, nodeTo));
|
|
88
|
-
widgets.push(Decoration.line({ attributes: { class: "hr" } }).range(line.from));
|
|
78
|
+
syntaxTree(view.state).iterate({
|
|
79
|
+
from,
|
|
80
|
+
to,
|
|
81
|
+
enter(node) {
|
|
82
|
+
const nodeName = node.name;
|
|
83
|
+
const nodeFrom = node.from;
|
|
84
|
+
const nodeTo = node.to;
|
|
85
|
+
if (nodeName === "HorizontalRule") {
|
|
86
|
+
if (cursorInNode(cursor?.from, cursor?.to, nodeFrom, nodeTo)) {
|
|
89
87
|
return;
|
|
90
88
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
widgets.push(decorationBullet.range(nodeFrom, nodeTo));
|
|
96
|
-
if (hideComponentMarkTokens.includes(node.name))
|
|
97
|
-
widgets.push(decorationHidden.range(nodeFrom, nodeTo));
|
|
98
|
-
if (nodeName === "HeaderMark")
|
|
99
|
-
widgets.push(decorationHidden.range(nodeFrom, nodeTo + 1));
|
|
89
|
+
const line = view.state.doc.lineAt(nodeFrom);
|
|
90
|
+
widgets.push(decorationHidden.range(nodeFrom, nodeTo));
|
|
91
|
+
widgets.push(Decoration.line({ attributes: { class: "hr" } }).range(line.from));
|
|
92
|
+
return;
|
|
100
93
|
}
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
if ((nodeName.startsWith("ATXHeading") || revealComponentMarkTokensOnCursor.includes(nodeName)) && cursorInNode(cursor?.from, cursor?.to, nodeFrom, nodeTo)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (nodeName === "ListMark" && node.matchContext(["BulletList", "ListItem"]) && cursor?.from != nodeFrom && cursor?.from != nodeFrom + 1)
|
|
98
|
+
widgets.push(decorationBullet.range(nodeFrom, nodeTo));
|
|
99
|
+
if (hideComponentMarkTokens.includes(node.name))
|
|
100
|
+
widgets.push(decorationHidden.range(nodeFrom, nodeTo));
|
|
101
|
+
if (nodeName === "HeaderMark")
|
|
102
|
+
widgets.push(decorationHidden.range(nodeFrom, nodeTo + 1));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
103
105
|
return widgets;
|
|
104
106
|
}
|
|
105
107
|
}
|
|
@@ -2,11 +2,34 @@ import type { Component } from 'vue'
|
|
|
2
2
|
import type { LanguageSupport } from '@codemirror/language'
|
|
3
3
|
import type { SyntaxNode } from '@lezer/common'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Represents a resolvable internal link within the editor.
|
|
7
|
+
* This is used to provide autocompletion and to resolve link targets.
|
|
8
|
+
*/
|
|
5
9
|
export interface InternalLink {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The display name of the link.
|
|
12
|
+
* This is what appears in the autocompletion list and is used as the default link text.
|
|
13
|
+
* It should be unique if `filePath` is not provided.
|
|
14
|
+
*/
|
|
15
|
+
name: string;
|
|
16
|
+
/**
|
|
17
|
+
* An optional file path associated with the link.
|
|
18
|
+
* Used to disambiguate links that have the same `name`.
|
|
19
|
+
* When autocompleting a link with a duplicate name, this path is used as the link target.
|
|
20
|
+
*/
|
|
21
|
+
filePath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* A unique identifier for the internal link.
|
|
24
|
+
* This value has no direct effect on the editor's behavior but is passed through
|
|
25
|
+
* to click events, allowing developers to use it for their own logic (e.g., routing).
|
|
26
|
+
*/
|
|
27
|
+
referenceId: string;
|
|
28
|
+
/**
|
|
29
|
+
* An optional Vue component to render when this link is embedded (e.g., `![[link]]`).
|
|
30
|
+
* If not provided, embeds of this link will be rendered as standard links.
|
|
31
|
+
*/
|
|
32
|
+
embedComponent?: Component;
|
|
10
33
|
}
|
|
11
34
|
|
|
12
35
|
export interface InternalLinkNode {
|
|
@@ -15,12 +38,29 @@ export interface InternalLinkNode {
|
|
|
15
38
|
display?: string
|
|
16
39
|
}
|
|
17
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Details about a clicked internal link.
|
|
43
|
+
* This object is emitted as the payload of the `internal-link-click` event.
|
|
44
|
+
*/
|
|
18
45
|
export interface InternalLinkClickDetail {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The target of the link as specified in the markdown source.
|
|
48
|
+
* This corresponds to the `path` part in `[[path#subpath|display]]`.
|
|
49
|
+
* It can be either the `name` or `filePath` of an `InternalLink`.
|
|
50
|
+
*/
|
|
51
|
+
target: string;
|
|
52
|
+
/** The subpath of the link, if any. Corresponds to the `#subpath` part. */
|
|
53
|
+
subpath?: string;
|
|
54
|
+
/** The display text (alias) of the link, if any. Corresponds to the `|display` part. */
|
|
55
|
+
display?: string;
|
|
56
|
+
/** The type of interaction, either a direct link click or an embed. */
|
|
57
|
+
type: 'embed' | 'internal-link';
|
|
58
|
+
/**
|
|
59
|
+
* The unique identifier (`referenceId`) of the resolved `InternalLink`.
|
|
60
|
+
* This allows developers to identify which link was clicked, even if multiple links
|
|
61
|
+
* share the same name or file path.
|
|
62
|
+
*/
|
|
63
|
+
referenceId?: string;
|
|
24
64
|
}
|
|
25
65
|
|
|
26
66
|
export interface ExternalLinkClickDetail {
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { load } from "js-yaml";
|
|
2
|
-
import {
|
|
3
|
-
import { GFM } from "@lezer/markdown";
|
|
4
|
-
import { CustomOFM } from "../editor/lezer-parsers/customOFMParsers.js";
|
|
2
|
+
import { parseMarkdownToAST } from "./markdownParser.js";
|
|
5
3
|
export function parseFrontmatter(markdownText) {
|
|
6
4
|
if (!markdownText) {
|
|
7
5
|
return {};
|
|
8
6
|
}
|
|
9
|
-
const tree =
|
|
10
|
-
extensions: [GFM, CustomOFM, { remove: ["SetextHeading"] }]
|
|
11
|
-
}).language.parser.parse(markdownText);
|
|
7
|
+
const tree = parseMarkdownToAST(markdownText);
|
|
12
8
|
const firstNode = tree.topNode.firstChild;
|
|
13
9
|
if (!firstNode || firstNode.name !== "YAMLFrontMatter") {
|
|
14
10
|
return {};
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { GFM } from "@lezer/markdown";
|
|
3
|
-
import { CustomOFM } from "../editor/lezer-parsers/customOFMParsers.js";
|
|
1
|
+
import { parseMarkdownToAST } from "./markdownParser.js";
|
|
4
2
|
export function getInternalLinks(markdownText) {
|
|
5
3
|
const links = [];
|
|
6
|
-
const tree =
|
|
7
|
-
extensions: [GFM, CustomOFM, { remove: ["SetextHeading"] }]
|
|
8
|
-
}).language.parser.parse(markdownText);
|
|
4
|
+
const tree = parseMarkdownToAST(markdownText);
|
|
9
5
|
tree.iterate({
|
|
10
6
|
enter: (node) => {
|
|
11
7
|
if (node.name === "InternalLink") {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Tree } from '@lezer/common';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a markdown parser with standard OFM extensions
|
|
4
|
+
* This configuration is used consistently across the codebase
|
|
5
|
+
*/
|
|
6
|
+
export declare function createMarkdownParser(): import("@codemirror/language").LanguageSupport;
|
|
7
|
+
/**
|
|
8
|
+
* Parses markdown text to AST using the standard OFM parser configuration
|
|
9
|
+
* @param markdownText The markdown text to parse
|
|
10
|
+
* @returns The parsed syntax tree
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseMarkdownToAST(markdownText: string): Tree;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
2
|
+
import { GFM } from "@lezer/markdown";
|
|
3
|
+
import { CustomOFM } from "../editor/lezer-parsers/customOFMParsers.js";
|
|
4
|
+
export function createMarkdownParser() {
|
|
5
|
+
return markdown({
|
|
6
|
+
extensions: [GFM, CustomOFM, { remove: ["SetextHeading"] }]
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export function parseMarkdownToAST(markdownText) {
|
|
10
|
+
return createMarkdownParser().language.parser.parse(markdownText);
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@type32/codemirror-rich-obsidian-editor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "OFM Editor Component for Nuxt.",
|
|
5
5
|
"repository": "Type-32/codemirror-rich-obsidian",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,45 +35,44 @@
|
|
|
35
35
|
"test:types": "bunx vue-tsc --noEmit && cd playground && bunx vue-tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@codemirror/autocomplete": "^6.
|
|
38
|
+
"@codemirror/autocomplete": "^6.19.0",
|
|
39
39
|
"@codemirror/lang-json": "^6.0.2",
|
|
40
|
-
"@codemirror/lang-markdown": "^6.
|
|
40
|
+
"@codemirror/lang-markdown": "^6.4.0",
|
|
41
41
|
"@codemirror/lang-yaml": "^6.1.2",
|
|
42
42
|
"@codemirror/language": "^6.11.3",
|
|
43
43
|
"@codemirror/language-data": "^6.5.1",
|
|
44
44
|
"@hsorby/vue3-katex": "0.6.0-rc.7",
|
|
45
45
|
"@lezer/markdown": "^1.4.3",
|
|
46
46
|
"@nuxt/image": "1.10.0",
|
|
47
|
-
"@nuxt/kit": "^4.1.
|
|
48
|
-
"@nuxt/ui": "^4.0.
|
|
47
|
+
"@nuxt/kit": "^4.1.3",
|
|
48
|
+
"@nuxt/ui": "^4.0.1",
|
|
49
49
|
"alfaaz": "^1.1.0",
|
|
50
50
|
"codemirror": "^6.0.2",
|
|
51
51
|
"js-yaml": "^4.1.0",
|
|
52
|
-
"katex": "^0.16.
|
|
52
|
+
"katex": "^0.16.23",
|
|
53
53
|
"lezer-markdown-obsidian": "^0.0.3",
|
|
54
54
|
"markdown-it": "^14.1.0",
|
|
55
55
|
"markdown-it-obsidian-callouts": "^0.3.2",
|
|
56
56
|
"vue-codemirror6": "^1.3.22"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@iconify-json/lucide": "^1.2.
|
|
60
|
-
"@iconify-json/simple-icons": "^1.2.
|
|
59
|
+
"@iconify-json/lucide": "^1.2.69",
|
|
60
|
+
"@iconify-json/simple-icons": "^1.2.54",
|
|
61
61
|
"@nuxt/devtools": "^2.6.5",
|
|
62
62
|
"@nuxt/eslint-config": "^1.9.0",
|
|
63
63
|
"@nuxt/fonts": "0.11.4",
|
|
64
64
|
"@nuxt/icon": "1.15.0",
|
|
65
65
|
"@nuxt/module-builder": "^1.0.2",
|
|
66
|
-
"@nuxt/schema": "^4.1.
|
|
66
|
+
"@nuxt/schema": "^4.1.3",
|
|
67
67
|
"@nuxt/test-utils": "^3.19.2",
|
|
68
68
|
"@types/js-yaml": "^4.0.9",
|
|
69
69
|
"@types/node": "latest",
|
|
70
70
|
"changelogen": "^0.6.2",
|
|
71
|
-
"eslint": "^9.
|
|
72
|
-
"nuxt": "^4.1.
|
|
73
|
-
"typescript": "~5.9.
|
|
71
|
+
"eslint": "^9.37.0",
|
|
72
|
+
"nuxt": "^4.1.3",
|
|
73
|
+
"typescript": "~5.9.3",
|
|
74
74
|
"vitest": "^3.2.4",
|
|
75
|
-
"vue-tsc": "^3.
|
|
76
|
-
"alfaaz": "^1.1.0"
|
|
75
|
+
"vue-tsc": "^3.1.1"
|
|
77
76
|
},
|
|
78
77
|
"trustedDependencies": [
|
|
79
78
|
"@parcel/watcher",
|