@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.
Files changed (23) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/Editor/ImageEmbedComponent.vue.d.ts +2 -1
  3. package/dist/runtime/components/Editor/TestCustomCodeBlock.vue.d.ts +2 -1
  4. package/dist/runtime/components/Editor.client.vue.d.ts +4 -2
  5. package/dist/runtime/composables/useDocumentUtils.js +3 -9
  6. package/dist/runtime/composables/useEditorFrontmatter.js +3 -20
  7. package/dist/runtime/composables/useEditorUtils.d.ts +2 -1
  8. package/dist/runtime/composables/useEditorUtils.js +16 -25
  9. package/dist/runtime/editor/plugins/codemirror-editor-plugins/editorInternalLinkAutocompletePlugin.js +11 -10
  10. package/dist/runtime/editor/plugins/codemirror-editor-plugins/editorLinkClickPlugin.js +3 -3
  11. package/dist/runtime/editor/plugins/codemirror-plugin-proses/createProsePlugin.d.ts +53 -0
  12. package/dist/runtime/editor/plugins/codemirror-plugin-proses/createProsePlugin.js +23 -0
  13. package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseHashtagCodemirrorViewPlugin.d.ts +1 -3
  14. package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseHashtagCodemirrorViewPlugin.js +5 -13
  15. package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseInternalLinkCodemirrorViewPlugin.d.ts +0 -3
  16. package/dist/runtime/editor/plugins/codemirror-plugin-proses/proseInternalLinkCodemirrorViewPlugin.js +5 -12
  17. package/dist/runtime/editor/plugins/richTextPlugin.js +37 -35
  18. package/dist/runtime/editor/types/editor-types.d.ts +49 -9
  19. package/dist/runtime/utils/frontmatter.js +2 -6
  20. package/dist/runtime/utils/internalLinks.js +2 -6
  21. package/dist/runtime/utils/markdownParser.d.ts +12 -0
  22. package/dist/runtime/utils/markdownParser.js +11 -0
  23. package/package.json +13 -14
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@type32/codemirror-rich-obsidian-editor",
3
3
  "configKey": "cmOfmEditor",
4
- "version": "0.0.24",
4
+ "version": "0.0.26",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -2,5 +2,6 @@ type __VLS_Props = {
2
2
  filePath: string;
3
3
  display?: string;
4
4
  };
5
- declare const _default: 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 __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 _default: 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>;
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 __VLS_PublicProps = __VLS_Props & {
13
+ type __VLS_ModelProps = {
14
14
  modelValue?: string;
15
15
  };
16
- declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {
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 { markdown } from "@codemirror/lang-markdown";
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 = markdown({
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 = markdown({
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 { load, dump } from "js-yaml";
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
- const ast = editorUtils.parseMarkdownToAST(doc);
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: (markdownText: string) => Tree;
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 { markdown } from "@codemirror/lang-markdown";
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
- const matches = [];
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.internalLinkName, (nameCounts.get(link.internalLinkName) || 0) + 1);
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.internalLinkName.toLowerCase().includes(searchString) || link.filePath && link.filePath.toLowerCase().includes(searchString)
34
+ (link) => link.name && link.name.toLowerCase().includes(searchString) || link.filePath && link.filePath.toLowerCase().includes(searchString)
35
35
  ).map((link) => {
36
- const isDuplicate = (nameCounts.get(link.internalLinkName) || 0) > 1;
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.filePath || link.internalLinkName,
40
- detail: link.internalLinkName,
41
- apply: link.filePath || link.internalLinkName
40
+ label: link.name,
41
+ detail: link.filePath,
42
+ apply: link.filePath || link.name
42
43
  };
43
44
  }
44
45
  return {
45
- label: link.internalLinkName,
46
+ label: link.name,
46
47
  detail: link.filePath,
47
- apply: `${link.internalLinkName}`
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.internalLinkName === path);
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
- redirectToPath: linkInfo?.redirectToPath
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
+ }
@@ -1,3 +1 @@
1
- import { type DecorationSet } from '@codemirror/view';
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>;
@@ -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 = StateField.define({
17
- create(state) {
18
- return RangeSet.of(buildHashtagWrappers(state));
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.internalLinkName === path || l.filePath === path);
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.internalLinkName === path || l.filePath === path);
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
- let decorations = this.decorations.map(update.changes);
51
- update.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
52
- const newWidgets = this.processRange(update.view, fromB, toB);
53
- decorations = decorations.update({
54
- filter: (f, t) => f < fromB || t > toB,
55
- add: newWidgets,
56
- sort: true
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
- this.decorations = decorations;
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
- for (let { from: from2, to: to2 } of view.visibleRanges) {
75
- syntaxTree(view.state).iterate({
76
- from: from2,
77
- to: to2,
78
- enter(node) {
79
- const nodeName = node.name;
80
- const nodeFrom = node.from;
81
- const nodeTo = node.to;
82
- if (nodeName === "HorizontalRule") {
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
- if ((nodeName.startsWith("ATXHeading") || revealComponentMarkTokensOnCursor.includes(nodeName)) && cursorInNode(cursor?.from, cursor?.to, nodeFrom, nodeTo)) {
92
- return false;
93
- }
94
- if (nodeName === "ListMark" && node.matchContext(["BulletList", "ListItem"]) && cursor?.from != nodeFrom && cursor?.from != nodeFrom + 1)
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
- internalLinkName: string;
7
- filePath?: string;
8
- redirectToPath: string;
9
- embedComponent?: Component;
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
- path: string;
20
- subpath?: string;
21
- display?: string;
22
- type: 'embed' | 'internal-link';
23
- redirectToPath?: string;
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 { markdown } from "@codemirror/lang-markdown";
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 = markdown({
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 { markdown } from "@codemirror/lang-markdown";
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 = markdown({
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.24",
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.18.7",
38
+ "@codemirror/autocomplete": "^6.19.0",
39
39
  "@codemirror/lang-json": "^6.0.2",
40
- "@codemirror/lang-markdown": "^6.3.4",
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.2",
48
- "@nuxt/ui": "^4.0.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.22",
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.68",
60
- "@iconify-json/simple-icons": "^1.2.53",
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.2",
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.36.0",
72
- "nuxt": "^4.1.2",
73
- "typescript": "~5.9.2",
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.0.8",
76
- "alfaaz": "^1.1.0"
75
+ "vue-tsc": "^3.1.1"
77
76
  },
78
77
  "trustedDependencies": [
79
78
  "@parcel/watcher",