@grandaniel/vue-markdown-editor 1.0.0 → 1.1.0
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/LICENSE +15 -0
- package/README.md +29 -0
- package/dist/components/MarkdownEditor/Composable/HeadlineTypeMap.d.ts +8 -0
- package/dist/components/MarkdownEditor/Composable/useMarkdownEditor.d.ts +26 -0
- package/dist/components/MarkdownEditor/Composable/useMarkdownProcessor.d.ts +28 -0
- package/dist/components/MarkdownEditor/Composable/useReflectiveState.d.ts +20 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenu.vue.d.ts +17 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuBlockItem.vue.d.ts +17 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuInlineItem.vue.d.ts +20 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorImageContextMenu.vue.d.ts +11 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorTextSelectionContextMenu.vue.d.ts +20 -0
- package/dist/components/MarkdownEditor/Factory/MarkdownNodeFactory.d.ts +14 -0
- package/dist/components/MarkdownEditor/MarkdownComponentRegistry.d.ts +15 -0
- package/dist/components/MarkdownEditor/MarkdownEditor.vue.d.ts +42 -0
- package/dist/components/MarkdownEditor/MarkdownEditorModal.vue.d.ts +23 -0
- package/dist/components/MarkdownEditor/MarkdownEditorModule.vue.d.ts +45 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline1.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline2.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline3.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleImage.vue.d.ts +14 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleImageState.d.ts +6 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleList.vue.d.ts +3 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleListState.d.ts +5 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleParagraph.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleTextState.d.ts +4 -0
- package/dist/components/MarkdownEditor/TipTap/SingleLineExtension.d.ts +4 -0
- package/dist/components/MarkdownEditor/Types/MarkdownAstNode.d.ts +14 -0
- package/dist/components/MarkdownEditor/Types/MarkdownAstNodeType.d.ts +11 -0
- package/dist/components/MarkdownEditor/Types/TextishEmits.d.ts +12 -0
- package/dist/components/MarkdownEditor/index.d.ts +5 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/vue-markdown-editor.css +2 -0
- package/dist/vue-markdown-editor.mjs +20524 -0
- package/package.json +53 -5
- package/eslint.config.ts +0 -0
- package/src/components/MarkdownEditor/Composable/HeadlineTypeMap.ts +0 -36
- package/src/components/MarkdownEditor/Composable/useMarkdownEditor.ts +0 -21
- package/src/components/MarkdownEditor/Composable/useMarkdownProcessor.ts +0 -186
- package/src/components/MarkdownEditor/Composable/useReflectiveState.ts +0 -84
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenu.vue +0 -38
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuBlockItem.vue +0 -43
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuInlineItem.vue +0 -52
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorImageContextMenu.vue +0 -35
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorTextSelectionContextMenu.vue +0 -45
- package/src/components/MarkdownEditor/Factory/MarkdownNodeFactory.ts +0 -49
- package/src/components/MarkdownEditor/MarkdownComponentRegistry.ts +0 -48
- package/src/components/MarkdownEditor/MarkdownEditor.vue +0 -333
- package/src/components/MarkdownEditor/MarkdownEditorModal.vue +0 -169
- package/src/components/MarkdownEditor/MarkdownEditorModule.vue +0 -129
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline1.vue +0 -60
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline2.vue +0 -60
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline3.vue +0 -61
- package/src/components/MarkdownEditor/Modules/MarkdownModuleImage.vue +0 -185
- package/src/components/MarkdownEditor/Modules/MarkdownModuleImageState.ts +0 -10
- package/src/components/MarkdownEditor/Modules/MarkdownModuleList.vue +0 -7
- package/src/components/MarkdownEditor/Modules/MarkdownModuleListState.ts +0 -10
- package/src/components/MarkdownEditor/Modules/MarkdownModuleParagraph.vue +0 -54
- package/src/components/MarkdownEditor/Modules/MarkdownModuleTextState.ts +0 -8
- package/src/components/MarkdownEditor/Modules/__tests__/MarkdownModuleHeadline1.test.js +0 -20
- package/src/components/MarkdownEditor/Modules/__tests__/MarkdownModuleHeadline1.test.ts +0 -25
- package/src/components/MarkdownEditor/Modules/__tests__/__snapshots__/MarkdownModuleHeadline1.test.ts.snap +0 -7
- package/src/components/MarkdownEditor/Styles/Mixins.scss +0 -7
- package/src/components/MarkdownEditor/TipTap/SingleLineExtension.ts +0 -21
- package/src/components/MarkdownEditor/Types/MarkdownAstNode.ts +0 -22
- package/src/components/MarkdownEditor/Types/MarkdownAstNodeType.ts +0 -28
- package/src/components/MarkdownEditor/Types/TextishEmits.ts +0 -14
- package/src/components/MarkdownEditor/index.ts +0 -1
- package/src/components/index.ts +0 -1
- package/src/index.ts +0 -1
- package/tsconfig.app.json +0 -11
- package/tsconfig.json +0 -11
- package/tsconfig.node.json +0 -12
- package/vite.config.ts +0 -24
- package/vitest.config.ts +0 -21
package/package.json
CHANGED
|
@@ -1,23 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grandaniel/vue-markdown-editor",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Vue 3 markdown editor component library.",
|
|
5
|
+
"main": "./dist/vue-markdown-editor.mjs",
|
|
6
|
+
"module": "./dist/vue-markdown-editor.mjs",
|
|
7
|
+
"private": false,
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/vue-markdown-editor.mjs",
|
|
13
|
+
"default": "./dist/vue-markdown-editor.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"vue",
|
|
23
|
+
"vue3",
|
|
24
|
+
"markdown",
|
|
25
|
+
"editor",
|
|
26
|
+
"tiptap"
|
|
27
|
+
],
|
|
5
28
|
"author": "danielgran",
|
|
6
29
|
"repository": {
|
|
7
|
-
"
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/danielgran/vue-markdown-editor.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/danielgran/vue-markdown-editor/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/danielgran/vue-markdown-editor#readme",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"provenance": true
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
8
43
|
},
|
|
9
44
|
"scripts": {
|
|
10
45
|
"dev": "vite",
|
|
11
|
-
"build": "tsc && vite build",
|
|
46
|
+
"build": "vue-tsc -p tsconfig.build.json && vite build",
|
|
12
47
|
"type-check": "vue-tsc --noEmit",
|
|
13
48
|
"preview": "vite preview",
|
|
14
49
|
"test": "vitest run"
|
|
15
50
|
},
|
|
16
51
|
"license": "ISC",
|
|
17
|
-
"
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"vue": "^3.5.0"
|
|
54
|
+
},
|
|
55
|
+
"sideEffects": [
|
|
56
|
+
"**/*.css",
|
|
57
|
+
"**/*.scss",
|
|
58
|
+
"**/*.vue"
|
|
59
|
+
],
|
|
18
60
|
"devDependencies": {
|
|
61
|
+
"@types/mdast": "^4.0.4",
|
|
19
62
|
"@tsconfig/node22": "^22.0.5",
|
|
20
63
|
"@types/node": "^22.19.19",
|
|
64
|
+
"@types/sortablejs": "^1.15.8",
|
|
65
|
+
"@types/turndown": "^5.0.5",
|
|
21
66
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
22
67
|
"@vitest/coverage-v8": "^4.1.6",
|
|
23
68
|
"@vue/test-utils": "^2.4.10",
|
|
@@ -38,6 +83,9 @@
|
|
|
38
83
|
"@vueuse/core": "^14.3.0",
|
|
39
84
|
"@vueuse/integrations": "^14.3.0",
|
|
40
85
|
"marked": "^18.0.3",
|
|
86
|
+
"remark-parse": "^11.0.0",
|
|
87
|
+
"sortablejs": "^1.15.6",
|
|
88
|
+
"unified": "^11.0.5",
|
|
41
89
|
"turndown": "^7.2.4"
|
|
42
90
|
}
|
|
43
91
|
}
|
package/eslint.config.ts
DELETED
|
File without changes
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import MarkdownNodeType from "../Types/MarkdownAstNodeType";
|
|
2
|
-
|
|
3
|
-
export interface HeadlineTypeEntry {
|
|
4
|
-
prefix: string;
|
|
5
|
-
type: MarkdownNodeType;
|
|
6
|
-
depth: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const HEADLINE_TYPE_MAP: HeadlineTypeEntry[] = [
|
|
10
|
-
{
|
|
11
|
-
prefix: "###",
|
|
12
|
-
type: MarkdownNodeType.HEADLINE3,
|
|
13
|
-
depth: 3,
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
prefix: "##",
|
|
17
|
-
type: MarkdownNodeType.HEADLINE2,
|
|
18
|
-
depth: 2,
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
prefix: "#",
|
|
22
|
-
type: MarkdownNodeType.HEADLINE1,
|
|
23
|
-
depth: 1,
|
|
24
|
-
},
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
export function detectHeadlineTypeFromContent(content: string, cursorPosition: number): MarkdownNodeType | null {
|
|
28
|
-
const matchedEntry = HEADLINE_TYPE_MAP.find(entry => content.startsWith(entry.prefix));
|
|
29
|
-
if (!matchedEntry) return null;
|
|
30
|
-
|
|
31
|
-
if (matchedEntry.prefix.length + 2 === cursorPosition) {
|
|
32
|
-
return matchedEntry.type;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type ModelRef, ref } from "vue";
|
|
2
|
-
import useMarkdownProcessor from "./useMarkdownProcessor";
|
|
3
|
-
|
|
4
|
-
export function useMarkdownEditor(initialContent: string = "") {
|
|
5
|
-
const markdownContent = ref<string>(initialContent);
|
|
6
|
-
|
|
7
|
-
const { markdownNodes, deleteNode, addBlankNode, replaceNodeType, moveNode } = useMarkdownProcessor(
|
|
8
|
-
markdownContent as ModelRef<string | undefined>,
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
markdownContent,
|
|
13
|
-
markdownNodes,
|
|
14
|
-
deleteNode,
|
|
15
|
-
addBlankNode,
|
|
16
|
-
replaceNodeType,
|
|
17
|
-
moveNode,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type MarkdownEditorInstance = ReturnType<typeof useMarkdownEditor>;
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import type { PhrasingContent } from "mdast";
|
|
2
|
-
import remarkParse from "remark-parse";
|
|
3
|
-
import { unified } from "unified";
|
|
4
|
-
import { type ModelRef, nextTick, ref, watch } from "vue";
|
|
5
|
-
import MarkdownNodeFactory from "../Factory/MarkdownNodeFactory";
|
|
6
|
-
import { isTextNodeState } from "../MarkdownComponentRegistry";
|
|
7
|
-
import type { MarkdownAstNode } from "../Types/MarkdownAstNode";
|
|
8
|
-
import MarkdownNodeType, { isTextNodeType } from "../Types/MarkdownAstNodeType";
|
|
9
|
-
|
|
10
|
-
function useMarkdownProcessor(modelValue: ModelRef<string | undefined>) {
|
|
11
|
-
const markdownNodes = ref<Array<MarkdownAstNode>>([]);
|
|
12
|
-
const isInternalUpdate = ref(false);
|
|
13
|
-
|
|
14
|
-
compileMarkdown(modelValue.value ?? "");
|
|
15
|
-
|
|
16
|
-
watch(
|
|
17
|
-
markdownNodes,
|
|
18
|
-
() => {
|
|
19
|
-
isInternalUpdate.value = true;
|
|
20
|
-
modelValue.value = serializeMarkdown();
|
|
21
|
-
nextTick(() => {
|
|
22
|
-
isInternalUpdate.value = false;
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
{ deep: true },
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
watch(modelValue, (newValue) => {
|
|
29
|
-
if (isInternalUpdate.value) return;
|
|
30
|
-
markdownNodes.value = [];
|
|
31
|
-
compileMarkdown(newValue ?? "");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
function compileMarkdown(markdown: string) {
|
|
35
|
-
const processor = unified().use(remarkParse);
|
|
36
|
-
const tree = processor.parse(markdown);
|
|
37
|
-
|
|
38
|
-
for (const node of tree.children) {
|
|
39
|
-
if (node.type === "paragraph" && node.children[0]?.type === "text") {
|
|
40
|
-
const text = node.children[0].value ?? "";
|
|
41
|
-
|
|
42
|
-
// Custom Component Syntax here
|
|
43
|
-
// eslint-disable-next-line @stylistic/quotes
|
|
44
|
-
if (text.startsWith('"""') && text.endsWith('"""')) {
|
|
45
|
-
const moduleName = (text.split("\n")[0] ?? "").slice(3).trim();
|
|
46
|
-
if (moduleName === "MarkdownModuleImage") {
|
|
47
|
-
const srcMatch = text.match(/src:\s*(\S+)/);
|
|
48
|
-
const altMatch = text.match(/alt:\s*(.+)/);
|
|
49
|
-
const captionMatch = text.match(/caption:\s*(.+)/);
|
|
50
|
-
|
|
51
|
-
markdownNodes.value.push(
|
|
52
|
-
MarkdownNodeFactory.createImageNode(
|
|
53
|
-
srcMatch?.[1] ?? "",
|
|
54
|
-
altMatch?.[1] ?? "",
|
|
55
|
-
captionMatch?.[1] ?? "",
|
|
56
|
-
),
|
|
57
|
-
);
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Regular text node
|
|
63
|
-
|
|
64
|
-
const phrasingContent = phrasingContentToText(node.children);
|
|
65
|
-
|
|
66
|
-
markdownNodes.value.push(MarkdownNodeFactory.createTextNode(MarkdownNodeType.PARAGRAPH, phrasingContent));
|
|
67
|
-
} else if (node.type === "heading" && node.depth === 1) {
|
|
68
|
-
const phrasingContent = phrasingContentToText(node.children);
|
|
69
|
-
|
|
70
|
-
markdownNodes.value.push(MarkdownNodeFactory.createTextNode(MarkdownNodeType.HEADLINE1, phrasingContent));
|
|
71
|
-
} else if (node.type === "heading" && node.depth === 2) {
|
|
72
|
-
markdownNodes.value.push(
|
|
73
|
-
MarkdownNodeFactory.createTextNode(MarkdownNodeType.HEADLINE2, phrasingContentToText(node.children)),
|
|
74
|
-
);
|
|
75
|
-
} else if (node.type === "heading" && node.depth === 3) {
|
|
76
|
-
markdownNodes.value.push(
|
|
77
|
-
MarkdownNodeFactory.createTextNode(MarkdownNodeType.HEADLINE3, phrasingContentToText(node.children)),
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function serializeMarkdown(): string {
|
|
84
|
-
return markdownNodes.value
|
|
85
|
-
.map((node) => {
|
|
86
|
-
if (isTextNodeType(node.type)) {
|
|
87
|
-
const prefix = {
|
|
88
|
-
[MarkdownNodeType.PARAGRAPH]: "",
|
|
89
|
-
[MarkdownNodeType.LIST]: "",
|
|
90
|
-
[MarkdownNodeType.HEADLINE1]: "# ",
|
|
91
|
-
[MarkdownNodeType.HEADLINE2]: "## ",
|
|
92
|
-
[MarkdownNodeType.HEADLINE3]: "### ",
|
|
93
|
-
}[node.type];
|
|
94
|
-
return `${prefix}${node.componentState.text}`;
|
|
95
|
-
}
|
|
96
|
-
if (node.type === MarkdownNodeType.IMAGE) {
|
|
97
|
-
return `"""MarkdownModuleImage
|
|
98
|
-
src: ${node.componentState.src}
|
|
99
|
-
alt: ${node.componentState.alt}
|
|
100
|
-
caption: ${node.componentState.caption}
|
|
101
|
-
"""`;
|
|
102
|
-
}
|
|
103
|
-
return "";
|
|
104
|
-
})
|
|
105
|
-
.join("\n\n");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function deleteNode(nodeIndex: number) {
|
|
109
|
-
nextTick(() => {
|
|
110
|
-
markdownNodes.value.splice(nodeIndex, 1);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function addBlankNode(nodeIndex: number) {
|
|
115
|
-
const newNode = MarkdownNodeFactory.createBlankParagraph();
|
|
116
|
-
|
|
117
|
-
if (nodeIndex !== undefined) {
|
|
118
|
-
markdownNodes.value.splice(nodeIndex + 1, 0, newNode);
|
|
119
|
-
} else {
|
|
120
|
-
markdownNodes.value.push(newNode);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return nodeIndex + 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function createNodeWithType(text: string, newType: MarkdownNodeType): MarkdownAstNode {
|
|
127
|
-
if (isTextNodeType(newType)) {
|
|
128
|
-
return MarkdownNodeFactory.createTextNode(newType, text);
|
|
129
|
-
} else if (newType === MarkdownNodeType.IMAGE) {
|
|
130
|
-
return MarkdownNodeFactory.createImageNode("", "", "");
|
|
131
|
-
} else {
|
|
132
|
-
throw new Error(`Unsupported node type: ${newType}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function replaceNodeType(
|
|
137
|
-
node: MarkdownAstNode,
|
|
138
|
-
newType: MarkdownNodeType,
|
|
139
|
-
): { newNode: MarkdownAstNode; index: number } | null {
|
|
140
|
-
// If the node is already of the desired type, do nothing
|
|
141
|
-
if (node.type === newType) return null;
|
|
142
|
-
|
|
143
|
-
const nodeIndex = markdownNodes.value.indexOf(node);
|
|
144
|
-
if (nodeIndex === -1) return null;
|
|
145
|
-
|
|
146
|
-
const currentText = isTextNodeState(node) ? node.componentState.text : "";
|
|
147
|
-
const newNode = createNodeWithType(currentText, newType);
|
|
148
|
-
|
|
149
|
-
markdownNodes.value.splice(nodeIndex, 1, newNode);
|
|
150
|
-
|
|
151
|
-
return { newNode, index: nodeIndex };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function moveNode(fromIndex: number, toIndex: number) {
|
|
155
|
-
const [node] = markdownNodes.value.splice(fromIndex, 1);
|
|
156
|
-
if (node) markdownNodes.value.splice(toIndex, 0, node);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
markdownNodes,
|
|
161
|
-
deleteNode,
|
|
162
|
-
addBlankNode,
|
|
163
|
-
replaceNodeType,
|
|
164
|
-
moveNode,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export default useMarkdownProcessor;
|
|
169
|
-
|
|
170
|
-
function phrasingContentToText(phrasingContent: PhrasingContent[]): string {
|
|
171
|
-
let result = "";
|
|
172
|
-
|
|
173
|
-
for (const content of phrasingContent) {
|
|
174
|
-
if (content.type === "text") {
|
|
175
|
-
result += content.value;
|
|
176
|
-
}
|
|
177
|
-
if (content.type === "strong") {
|
|
178
|
-
result += `**${phrasingContentToText(content.children)}**`;
|
|
179
|
-
}
|
|
180
|
-
if (content.type === "emphasis") {
|
|
181
|
-
result += `*${phrasingContentToText(content.children)}*`;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return result;
|
|
186
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { EditorContent, EditorEvents } from "@tiptap/vue-3";
|
|
2
|
-
import { marked } from "marked";
|
|
3
|
-
import TurndownService from "turndown";
|
|
4
|
-
import { nextTick, ref, type ModelRef, type Ref } from "vue";
|
|
5
|
-
import type MarkdownModuleTextState from "../Modules/MarkdownModuleTextState";
|
|
6
|
-
import type { TextishEmitFunction } from "../Types/TextishEmits";
|
|
7
|
-
import { detectHeadlineTypeFromContent } from "./HeadlineTypeMap";
|
|
8
|
-
|
|
9
|
-
const turndownService = new TurndownService();
|
|
10
|
-
// Override the default escape function to prevent escaping of special characters,
|
|
11
|
-
turndownService.escape = (text: string) => text;
|
|
12
|
-
|
|
13
|
-
// Headings and paragraphs are represented by MarkdownNodeType, not by the content text itself.
|
|
14
|
-
// Only inline markup (bold, italic, etc.) should be preserved as markdown syntax.
|
|
15
|
-
// Without this, TurndownService would convert <h2>text</h2> → "## text", which would be stored
|
|
16
|
-
// in componentState.text and cause the heading prefix to appear literally inside the TipTap editor
|
|
17
|
-
// on the next initialization (especially visible after HMR).
|
|
18
|
-
turndownService.addRule("blockElementsToInline", {
|
|
19
|
-
filter: ["h1", "h2", "h3", "h4", "h5", "h6", "p"],
|
|
20
|
-
replacement: (content) => content,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
function markdownToHtml(markdown: string): string {
|
|
24
|
-
return marked.parseInline(markdown) as string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function htmlToMarkdown(html: string): string {
|
|
28
|
-
return turndownService.turndown(html);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function useReflectiveState<T extends MarkdownModuleTextState>(options: {
|
|
32
|
-
modelRef: ModelRef<T>;
|
|
33
|
-
emit: TextishEmitFunction;
|
|
34
|
-
editorRef?: Ref<InstanceType<typeof EditorContent> | undefined>;
|
|
35
|
-
containingHtmlElementRef?: Ref<HTMLElement | undefined>;
|
|
36
|
-
}) {
|
|
37
|
-
const editorContent = ref(markdownToHtml(options.modelRef.value.text));
|
|
38
|
-
|
|
39
|
-
async function handleTipTapUpdateEvent(event: EditorEvents["update"]) {
|
|
40
|
-
emitHtml(event.editor.getHTML());
|
|
41
|
-
emitCursorPosition(event.transaction.selection.anchor);
|
|
42
|
-
|
|
43
|
-
// Wait for the DOM to update with the new content before trying to detect type changes
|
|
44
|
-
await nextTick();
|
|
45
|
-
const markdown = htmlToMarkdown(event.editor.getHTML());
|
|
46
|
-
detectInlineTypeChange(markdown, event.transaction.selection.anchor);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function detectInlineTypeChange(markdown: string, cursorPosition: number) {
|
|
50
|
-
const detectedType = detectHeadlineTypeFromContent(markdown, cursorPosition);
|
|
51
|
-
if (!detectedType) return;
|
|
52
|
-
|
|
53
|
-
options.emit("change-type", detectedType);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function handleKeyDown(event: KeyboardEvent) {
|
|
57
|
-
if (event.key === "Enter") {
|
|
58
|
-
event.preventDefault();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function emitHtml(html: string) {
|
|
63
|
-
const markdown = htmlToMarkdown(html);
|
|
64
|
-
options.modelRef.value.text = markdown;
|
|
65
|
-
options.emit("update:model-value", { text: markdown });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function emitCursorPosition(cursorPosition: number) {
|
|
69
|
-
options.emit("update:cursor-position", cursorPosition);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function focus() {
|
|
73
|
-
if (options.editorRef?.value?.editor) {
|
|
74
|
-
options.editorRef.value.editor.commands.focus();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
options.containingHtmlElementRef?.value?.focus();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { handleTipTapUpdateEvent, handleKeyDown, editorContent, expose: { focus } };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export { markdownToHtml };
|
|
84
|
-
export default useReflectiveState;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="markdown-editor-context-menu"
|
|
4
|
-
:style="positionStyle"
|
|
5
|
-
>
|
|
6
|
-
<slot />
|
|
7
|
-
</div>
|
|
8
|
-
</template>
|
|
9
|
-
|
|
10
|
-
<script setup lang="ts">
|
|
11
|
-
import { computed } from "vue";
|
|
12
|
-
|
|
13
|
-
const props = defineProps<{
|
|
14
|
-
x: number;
|
|
15
|
-
y: number;
|
|
16
|
-
}>();
|
|
17
|
-
|
|
18
|
-
const positionStyle = computed(() => ({
|
|
19
|
-
left: `${props.x}px`,
|
|
20
|
-
top: `${props.y}px`,
|
|
21
|
-
}));
|
|
22
|
-
</script>
|
|
23
|
-
|
|
24
|
-
<style lang="scss" scoped>
|
|
25
|
-
.markdown-editor-context-menu {
|
|
26
|
-
position: fixed;
|
|
27
|
-
z-index: 1000;
|
|
28
|
-
background: #ffffff;
|
|
29
|
-
border: 1px solid #e5e7eb;
|
|
30
|
-
border-radius: 0.5rem;
|
|
31
|
-
box-shadow:
|
|
32
|
-
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
33
|
-
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
34
|
-
padding: 0.25rem;
|
|
35
|
-
display: flex;
|
|
36
|
-
gap: 0.25rem;
|
|
37
|
-
}
|
|
38
|
-
</style>
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<button
|
|
3
|
-
type="button"
|
|
4
|
-
class="markdown-editor-context-menu-block-item"
|
|
5
|
-
@click="$emit('click')"
|
|
6
|
-
>
|
|
7
|
-
<slot />
|
|
8
|
-
</button>
|
|
9
|
-
</template>
|
|
10
|
-
|
|
11
|
-
<script setup lang="ts">
|
|
12
|
-
defineEmits<{
|
|
13
|
-
click: [];
|
|
14
|
-
}>();
|
|
15
|
-
</script>
|
|
16
|
-
|
|
17
|
-
<style lang="scss" scoped>
|
|
18
|
-
.markdown-editor-context-menu-block-item {
|
|
19
|
-
display: flex;
|
|
20
|
-
align-items: center;
|
|
21
|
-
gap: 0.5rem;
|
|
22
|
-
padding: 0.625rem 0.875rem;
|
|
23
|
-
border: none;
|
|
24
|
-
background: transparent;
|
|
25
|
-
border-radius: 0.375rem;
|
|
26
|
-
cursor: pointer;
|
|
27
|
-
color: #374151;
|
|
28
|
-
font-size: 0.875rem;
|
|
29
|
-
font-weight: 500;
|
|
30
|
-
transition: all 0.15s ease;
|
|
31
|
-
width: 100%;
|
|
32
|
-
text-align: left;
|
|
33
|
-
white-space: nowrap;
|
|
34
|
-
|
|
35
|
-
&:hover {
|
|
36
|
-
background: #f3f4f6;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
&:active {
|
|
40
|
-
transform: scale(0.98);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
</style>
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<button
|
|
3
|
-
type="button"
|
|
4
|
-
class="markdown-editor-context-menu-inline-item"
|
|
5
|
-
:class="{ 'is-active': active }"
|
|
6
|
-
@click="$emit('click')"
|
|
7
|
-
>
|
|
8
|
-
<slot />
|
|
9
|
-
</button>
|
|
10
|
-
</template>
|
|
11
|
-
|
|
12
|
-
<script setup lang="ts">
|
|
13
|
-
defineProps<{
|
|
14
|
-
active?: boolean;
|
|
15
|
-
}>();
|
|
16
|
-
|
|
17
|
-
defineEmits<{
|
|
18
|
-
click: [];
|
|
19
|
-
}>();
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<style lang="scss" scoped>
|
|
23
|
-
.markdown-editor-context-menu-inline-item {
|
|
24
|
-
display: flex;
|
|
25
|
-
align-items: center;
|
|
26
|
-
justify-content: center;
|
|
27
|
-
padding: 0.5rem;
|
|
28
|
-
border: none;
|
|
29
|
-
background: transparent;
|
|
30
|
-
border-radius: 0.375rem;
|
|
31
|
-
cursor: pointer;
|
|
32
|
-
color: #374151;
|
|
33
|
-
font-size: 0.875rem;
|
|
34
|
-
font-weight: 500;
|
|
35
|
-
transition: all 0.15s ease;
|
|
36
|
-
min-width: 2rem;
|
|
37
|
-
min-height: 2rem;
|
|
38
|
-
|
|
39
|
-
&:hover {
|
|
40
|
-
background: #f3f4f6;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
&.is-active {
|
|
44
|
-
background: #dbeafe;
|
|
45
|
-
color: #1e40af;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
&:active {
|
|
49
|
-
transform: scale(0.95);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
</style>
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<MarkdownEditorContextMenu
|
|
3
|
-
:x="x"
|
|
4
|
-
:y="y"
|
|
5
|
-
>
|
|
6
|
-
<div class="markdown-editor-image-context-menu-content">
|
|
7
|
-
<MarkdownEditorContextMenuBlockItem @click="emit('edit-attributes')">
|
|
8
|
-
Edit Attributes
|
|
9
|
-
</MarkdownEditorContextMenuBlockItem>
|
|
10
|
-
<MarkdownEditorContextMenuBlockItem @click="emit('edit-attributes')">
|
|
11
|
-
Edit Attributes
|
|
12
|
-
</MarkdownEditorContextMenuBlockItem>
|
|
13
|
-
</div>
|
|
14
|
-
</MarkdownEditorContextMenu>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
defineProps<{
|
|
19
|
-
x: number;
|
|
20
|
-
y: number;
|
|
21
|
-
}>();
|
|
22
|
-
|
|
23
|
-
const emit = defineEmits<{
|
|
24
|
-
"edit-attributes": [];
|
|
25
|
-
}>();
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<style lang="scss" scoped>
|
|
29
|
-
.markdown-editor-image-context-menu-content {
|
|
30
|
-
display: flex;
|
|
31
|
-
flex-direction: column;
|
|
32
|
-
gap: 0.125rem;
|
|
33
|
-
min-width: 10rem;
|
|
34
|
-
}
|
|
35
|
-
</style>
|
package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorTextSelectionContextMenu.vue
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<MarkdownEditorContextMenu
|
|
3
|
-
:x="x"
|
|
4
|
-
:y="y"
|
|
5
|
-
>
|
|
6
|
-
<MarkdownEditorContextMenuInlineItem
|
|
7
|
-
:active="isActive.bold"
|
|
8
|
-
@click="emit('toggle-bold')"
|
|
9
|
-
>
|
|
10
|
-
<strong>B</strong>
|
|
11
|
-
</MarkdownEditorContextMenuInlineItem>
|
|
12
|
-
|
|
13
|
-
<MarkdownEditorContextMenuInlineItem
|
|
14
|
-
:active="isActive.italic"
|
|
15
|
-
@click="emit('toggle-italic')"
|
|
16
|
-
>
|
|
17
|
-
<em>I</em>
|
|
18
|
-
</MarkdownEditorContextMenuInlineItem>
|
|
19
|
-
|
|
20
|
-
<MarkdownEditorContextMenuInlineItem
|
|
21
|
-
:active="isActive.underline"
|
|
22
|
-
@click="emit('toggle-underline')"
|
|
23
|
-
>
|
|
24
|
-
<span style="text-decoration: underline">U</span>
|
|
25
|
-
</MarkdownEditorContextMenuInlineItem>
|
|
26
|
-
</MarkdownEditorContextMenu>
|
|
27
|
-
</template>
|
|
28
|
-
|
|
29
|
-
<script setup lang="ts">
|
|
30
|
-
defineProps<{
|
|
31
|
-
x: number;
|
|
32
|
-
y: number;
|
|
33
|
-
isActive: {
|
|
34
|
-
bold: boolean;
|
|
35
|
-
italic: boolean;
|
|
36
|
-
underline: boolean;
|
|
37
|
-
};
|
|
38
|
-
}>();
|
|
39
|
-
|
|
40
|
-
const emit = defineEmits<{
|
|
41
|
-
"toggle-bold": [];
|
|
42
|
-
"toggle-italic": [];
|
|
43
|
-
"toggle-underline": [];
|
|
44
|
-
}>();
|
|
45
|
-
</script>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import registry from "../MarkdownComponentRegistry";
|
|
2
|
-
import type MarkdownModuleImageState from "../Modules/MarkdownModuleImageState";
|
|
3
|
-
import type MarkdownModuleListState from "../Modules/MarkdownModuleListState";
|
|
4
|
-
import MarkdownModuleTextState from "../Modules/MarkdownModuleTextState";
|
|
5
|
-
import { MarkdownAstNode } from "../Types/MarkdownAstNode";
|
|
6
|
-
import MarkdownNodeType, { type TextishNodeType } from "../Types/MarkdownAstNodeType";
|
|
7
|
-
|
|
8
|
-
class MarkdownNodeFactory {
|
|
9
|
-
private createNode<TState extends object>(
|
|
10
|
-
type: MarkdownNodeType,
|
|
11
|
-
stateData: Partial<TState>,
|
|
12
|
-
): MarkdownAstNode<TState> {
|
|
13
|
-
const StateConstructor = registry[type].stateType as unknown as new (object: Partial<TState>) => TState;
|
|
14
|
-
|
|
15
|
-
const componentState = new StateConstructor(stateData as TState);
|
|
16
|
-
|
|
17
|
-
return new MarkdownAstNode<TState>({
|
|
18
|
-
type,
|
|
19
|
-
componentState,
|
|
20
|
-
editingState: {
|
|
21
|
-
cursorPosition: 0,
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
createTextNode(type: TextishNodeType, text: string): MarkdownAstNode<MarkdownModuleTextState> {
|
|
27
|
-
return this.createNode<MarkdownModuleTextState>(type, { text });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
createImageNode(src: string, alt: string, caption: string = ""): MarkdownAstNode<MarkdownModuleImageState> {
|
|
31
|
-
return this.createNode<MarkdownModuleImageState>(MarkdownNodeType.IMAGE, {
|
|
32
|
-
src,
|
|
33
|
-
alt,
|
|
34
|
-
caption,
|
|
35
|
-
} as MarkdownModuleImageState);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
createListNode(items: string[]): MarkdownAstNode<MarkdownModuleListState> {
|
|
39
|
-
return this.createNode<MarkdownModuleListState>(MarkdownNodeType.LIST, {
|
|
40
|
-
items: items.map(text => new MarkdownModuleTextState({ text })),
|
|
41
|
-
} as MarkdownModuleListState);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
createBlankParagraph(): MarkdownAstNode<MarkdownModuleTextState> {
|
|
45
|
-
return this.createTextNode(MarkdownNodeType.PARAGRAPH, "");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export default new MarkdownNodeFactory();
|