@adminforth/markdown 1.10.3 → 1.10.5
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/build.log +4 -2
- package/custom/MarkdownEditor.vue +41 -11
- package/custom/package-lock.json +78 -0
- package/custom/topPanelButtons.vue +131 -0
- package/dist/custom/MarkdownEditor.vue +41 -11
- package/dist/custom/package-lock.json +78 -0
- package/dist/custom/topPanelButtons.vue +131 -0
- package/dist/index.js +16 -1
- package/index.ts +20 -0
- package/package.json +1 -1
- package/types.ts +29 -0
package/build.log
CHANGED
|
@@ -6,11 +6,13 @@ sending incremental file list
|
|
|
6
6
|
custom/
|
|
7
7
|
custom/MarkdownEditor.vue
|
|
8
8
|
custom/MarkdownRenderer.vue
|
|
9
|
+
custom/package-lock.json
|
|
9
10
|
custom/package.json
|
|
10
11
|
custom/pnpm-lock.yaml
|
|
12
|
+
custom/topPanelButtons.vue
|
|
11
13
|
custom/tsconfig.json
|
|
12
14
|
custom/utils/
|
|
13
15
|
custom/utils/monacoMarkdownToggle.ts
|
|
14
16
|
|
|
15
|
-
sent
|
|
16
|
-
total size is
|
|
17
|
+
sent 45,566 bytes received 180 bytes 91,492.00 bytes/sec
|
|
18
|
+
total size is 44,884 speedup is 0.98
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="mb-2"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
<div class="mb-2 w-full flex flex-col">
|
|
3
|
+
<TopPanelButtons :editor="editor" :meta="meta" />
|
|
4
|
+
<div
|
|
5
|
+
ref="editorContainer"
|
|
6
|
+
id="editor"
|
|
7
|
+
:class="[
|
|
8
|
+
'text-sm block w-full transition-all box-border overflow-hidden rounded-b-lg border border-t-0 pt-3',
|
|
9
|
+
isFocused
|
|
10
|
+
? 'ring-1 ring-lightPrimary border-lightPrimary dark:ring-darkPrimary dark:border-darkPrimary'
|
|
11
|
+
: 'border-gray-300 dark:border-gray-600',
|
|
12
|
+
]"
|
|
13
|
+
></div>
|
|
14
|
+
</div>
|
|
13
15
|
</template>
|
|
14
16
|
|
|
15
17
|
<script setup lang="ts">
|
|
@@ -19,6 +21,7 @@ import * as monaco from 'monaco-editor';
|
|
|
19
21
|
import TurndownService from 'turndown';
|
|
20
22
|
import { gfm, tables } from 'turndown-plugin-gfm';
|
|
21
23
|
import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
|
|
24
|
+
import TopPanelButtons from './topPanelButtons.vue';
|
|
22
25
|
|
|
23
26
|
const props = defineProps<{
|
|
24
27
|
column: any,
|
|
@@ -529,6 +532,33 @@ onMounted(async () => {
|
|
|
529
532
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyU, () => {
|
|
530
533
|
toggleWrapSmart(editor!, '<u>', '</u>');
|
|
531
534
|
});
|
|
535
|
+
|
|
536
|
+
disposables.push(
|
|
537
|
+
editor.onKeyDown((e) => {
|
|
538
|
+
if (e.keyCode !== monaco.KeyCode.Enter) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const pos = editor!.getPosition();
|
|
542
|
+
if (!pos) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const line = model!.getLineContent(pos.lineNumber);
|
|
546
|
+
const match = line.match(/^(\s*)([*+-]|\d+\.)\s+/);
|
|
547
|
+
if (!match) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
e.preventDefault();
|
|
551
|
+
|
|
552
|
+
if (line.trim() === match[2].trim()) {
|
|
553
|
+
const range = new monaco.Range(pos.lineNumber, 1, pos.lineNumber, line.length + 1);
|
|
554
|
+
editor!.executeEdits('exit-list', [{ range, text: '', forceMoveMarkers: true }]);
|
|
555
|
+
} else {
|
|
556
|
+
const isNum = match[2].includes('.');
|
|
557
|
+
const next = isNum ? `${parseInt(match[2]) + 1}. ` : `${match[2]} `;
|
|
558
|
+
editor!.trigger('keyboard', 'type', { text: `\n${match[1]}${next}` });
|
|
559
|
+
}
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
532
562
|
|
|
533
563
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
|
|
534
564
|
const selection = editor!.getSelection();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "custom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"dompurify": "^3.2.4",
|
|
13
|
+
"marked": "^15.0.7",
|
|
14
|
+
"monaco-editor": "^0.45.0",
|
|
15
|
+
"turndown": "^7.2.2",
|
|
16
|
+
"turndown-plugin-gfm": "^1.0.2"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"node_modules/@mixmark-io/domino": {
|
|
20
|
+
"version": "2.2.0",
|
|
21
|
+
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
|
22
|
+
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
|
|
23
|
+
"license": "BSD-2-Clause"
|
|
24
|
+
},
|
|
25
|
+
"node_modules/@types/trusted-types": {
|
|
26
|
+
"version": "2.0.7",
|
|
27
|
+
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
|
28
|
+
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"node_modules/dompurify": {
|
|
33
|
+
"version": "3.3.2",
|
|
34
|
+
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
|
35
|
+
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
|
36
|
+
"license": "(MPL-2.0 OR Apache-2.0)",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"@types/trusted-types": "^2.0.7"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"node_modules/marked": {
|
|
45
|
+
"version": "15.0.12",
|
|
46
|
+
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
|
47
|
+
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"bin": {
|
|
50
|
+
"marked": "bin/marked.js"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">= 18"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"node_modules/monaco-editor": {
|
|
57
|
+
"version": "0.45.0",
|
|
58
|
+
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz",
|
|
59
|
+
"integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==",
|
|
60
|
+
"license": "MIT"
|
|
61
|
+
},
|
|
62
|
+
"node_modules/turndown": {
|
|
63
|
+
"version": "7.2.2",
|
|
64
|
+
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
|
|
65
|
+
"integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@mixmark-io/domino": "^2.2.0"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"node_modules/turndown-plugin-gfm": {
|
|
72
|
+
"version": "1.0.2",
|
|
73
|
+
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
|
|
74
|
+
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
|
|
75
|
+
"license": "MIT"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { markRaw } from 'vue';
|
|
3
|
+
import * as monaco from 'monaco-editor';
|
|
4
|
+
import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
IconLinkOutline, IconCodeOutline, IconRectangleListOutline,
|
|
8
|
+
IconOrderedListOutline, IconLetterBoldOutline, IconLetterUnderlineOutline,
|
|
9
|
+
IconLetterItalicOutline, IconTextSlashOutline
|
|
10
|
+
} from '@iconify-prerendered/vue-flowbite';
|
|
11
|
+
import { IconH116Solid, IconH216Solid, IconH316Solid } from '@iconify-prerendered/vue-heroicons';
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
editor: monaco.editor.IStandaloneCodeEditor | null;
|
|
15
|
+
meta: any;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const isBtnVisible = (btnKey: string) => {
|
|
19
|
+
const settings = props.meta?.topPanelSettings;
|
|
20
|
+
if (!settings || Object.keys(settings).length === 0) return true;
|
|
21
|
+
return settings[btnKey] !== undefined ? settings[btnKey] : true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const btnClass = 'flex items-center justify-center h-8 px-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200';
|
|
25
|
+
|
|
26
|
+
const fenceForCodeBlock = (text: string): string => {
|
|
27
|
+
let maxBackticks = 0;
|
|
28
|
+
let current = 0;
|
|
29
|
+
for (let i = 0; i < text.length; i++) {
|
|
30
|
+
if (text[i] === '`') { current++; if (current > maxBackticks) maxBackticks = current; }
|
|
31
|
+
else { current = 0; }
|
|
32
|
+
}
|
|
33
|
+
return '`'.repeat(Math.max(3, maxBackticks + 1));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const applyFormat = (type: string) => {
|
|
37
|
+
const editor = props.editor;
|
|
38
|
+
if (!editor) return;
|
|
39
|
+
|
|
40
|
+
const model = editor.getModel();
|
|
41
|
+
if (!model) return;
|
|
42
|
+
|
|
43
|
+
editor.focus();
|
|
44
|
+
const rawSelection = editor.getSelection();
|
|
45
|
+
if (!rawSelection) return;
|
|
46
|
+
|
|
47
|
+
const selection = rawSelection.startLineNumber !== rawSelection.endLineNumber && rawSelection.endColumn === 1
|
|
48
|
+
? new monaco.Selection(rawSelection.startLineNumber, rawSelection.startColumn, rawSelection.endLineNumber - 1, model.getLineMaxColumn(rawSelection.endLineNumber - 1))
|
|
49
|
+
: rawSelection;
|
|
50
|
+
|
|
51
|
+
const selectedText = model.getValueInRange(selection);
|
|
52
|
+
|
|
53
|
+
const applyEdits = (id: string, edits: monaco.editor.IIdentifiedSingleEditOperation[]) => {
|
|
54
|
+
editor.executeEdits(id, edits);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
switch (type) {
|
|
58
|
+
case 'bold': toggleWrapSmart(editor, '**'); break;
|
|
59
|
+
case 'italic': toggleWrapSmart(editor, '*'); break;
|
|
60
|
+
case 'strike': toggleWrapSmart(editor, '~~'); break;
|
|
61
|
+
case 'underline': toggleWrapSmart(editor, '<u>', '</u>'); break;
|
|
62
|
+
case 'codeBlock': {
|
|
63
|
+
const trimmed = selectedText.trim();
|
|
64
|
+
const match = trimmed.match(/^(`{3,})[^\n]*\n([\s\S]*)\n\1$/);
|
|
65
|
+
if (match) {
|
|
66
|
+
applyEdits('unwrap-code', [{ range: selection, text: match[2], forceMoveMarkers: true }]);
|
|
67
|
+
} else {
|
|
68
|
+
const fence = fenceForCodeBlock(selectedText);
|
|
69
|
+
applyEdits('wrap-code', [{ range: selection, text: `\n${fence}\n${selectedText}\n${fence}\n`, forceMoveMarkers: true }]);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'link': {
|
|
74
|
+
const match = selectedText.trim().match(/^\[(.*?)\]\(.*?\)$/);
|
|
75
|
+
if (match) {
|
|
76
|
+
applyEdits('unlink', [{ range: selection, text: match[1], forceMoveMarkers: true }]);
|
|
77
|
+
} else {
|
|
78
|
+
applyEdits('insert-link', [{ range: selection, text: `[${selectedText}](url)`, forceMoveMarkers: true }]);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'h1': case 'h2': case 'h3': case 'ul': case 'ol': {
|
|
83
|
+
const prefixMap: any = { h1: '# ', h2: '## ', h3: '### ', ul: '* ' };
|
|
84
|
+
const edits: any[] = [];
|
|
85
|
+
for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
|
|
86
|
+
const line = model.getLineContent(i);
|
|
87
|
+
const targetPrefix = type === 'ol' ? `${i - selection.startLineNumber + 1}. ` : prefixMap[type];
|
|
88
|
+
const match = line.match(/^(#{1,6}\s+|[*+-]\s+|\d+[.)]\s+)/);
|
|
89
|
+
if (match) {
|
|
90
|
+
edits.push({ range: new monaco.Range(i, 1, i, match[0].length + 1), text: match[0].trim() === targetPrefix.trim() ? '' : targetPrefix });
|
|
91
|
+
} else {
|
|
92
|
+
edits.push({ range: new monaco.Range(i, 1, i, 1), text: targetPrefix });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
applyEdits('format-block', edits);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const buttons = [
|
|
102
|
+
{ id: 'bold', title: 'Bold', icon: markRaw(IconLetterBoldOutline), group: 1 },
|
|
103
|
+
{ id: 'italic', title: 'Italic', icon: markRaw(IconLetterItalicOutline), group: 1 },
|
|
104
|
+
{ id: 'underline', title: 'Underline', icon: markRaw(IconLetterUnderlineOutline), group: 1 },
|
|
105
|
+
{ id: 'strike', title: 'Strike', icon: markRaw(IconTextSlashOutline), group: 1, separator: true },
|
|
106
|
+
{ id: 'h1', title: 'H1', icon: markRaw(IconH116Solid), group: 2 },
|
|
107
|
+
{ id: 'h2', title: 'H2', icon: markRaw(IconH216Solid), group: 2 },
|
|
108
|
+
{ id: 'h3', title: 'H3', icon: markRaw(IconH316Solid), group: 2, separator: true },
|
|
109
|
+
{ id: 'ul', title: 'UL', icon: markRaw(IconRectangleListOutline), group: 3 },
|
|
110
|
+
{ id: 'ol', title: 'OL', icon: markRaw(IconOrderedListOutline), group: 3 },
|
|
111
|
+
{ id: 'link', title: 'Link', icon: markRaw(IconLinkOutline), group: 3 },
|
|
112
|
+
{ id: 'codeBlock', title: 'Code', icon: markRaw(IconCodeOutline), group: 3 },
|
|
113
|
+
];
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="flex flex-wrap items-center gap-3 p-1.5 border border-gray-300 dark:border-gray-600 rounded-t-lg bg-gray-50 dark:bg-gray-800 w-full box-border">
|
|
118
|
+
<template v-for="btn in buttons" :key="btn.id">
|
|
119
|
+
<button
|
|
120
|
+
v-if="isBtnVisible(btn.id)"
|
|
121
|
+
type="button"
|
|
122
|
+
@click="applyFormat(btn.id)"
|
|
123
|
+
:class="btnClass"
|
|
124
|
+
:title="btn.title"
|
|
125
|
+
>
|
|
126
|
+
<component :is="btn.icon" class="w-5 h-5" />
|
|
127
|
+
</button>
|
|
128
|
+
<div v-if="btn.separator && isBtnVisible(btn.id)" class="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
|
|
129
|
+
</template>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="mb-2"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
<div class="mb-2 w-full flex flex-col">
|
|
3
|
+
<TopPanelButtons :editor="editor" :meta="meta" />
|
|
4
|
+
<div
|
|
5
|
+
ref="editorContainer"
|
|
6
|
+
id="editor"
|
|
7
|
+
:class="[
|
|
8
|
+
'text-sm block w-full transition-all box-border overflow-hidden rounded-b-lg border border-t-0 pt-3',
|
|
9
|
+
isFocused
|
|
10
|
+
? 'ring-1 ring-lightPrimary border-lightPrimary dark:ring-darkPrimary dark:border-darkPrimary'
|
|
11
|
+
: 'border-gray-300 dark:border-gray-600',
|
|
12
|
+
]"
|
|
13
|
+
></div>
|
|
14
|
+
</div>
|
|
13
15
|
</template>
|
|
14
16
|
|
|
15
17
|
<script setup lang="ts">
|
|
@@ -19,6 +21,7 @@ import * as monaco from 'monaco-editor';
|
|
|
19
21
|
import TurndownService from 'turndown';
|
|
20
22
|
import { gfm, tables } from 'turndown-plugin-gfm';
|
|
21
23
|
import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
|
|
24
|
+
import TopPanelButtons from './topPanelButtons.vue';
|
|
22
25
|
|
|
23
26
|
const props = defineProps<{
|
|
24
27
|
column: any,
|
|
@@ -529,6 +532,33 @@ onMounted(async () => {
|
|
|
529
532
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyU, () => {
|
|
530
533
|
toggleWrapSmart(editor!, '<u>', '</u>');
|
|
531
534
|
});
|
|
535
|
+
|
|
536
|
+
disposables.push(
|
|
537
|
+
editor.onKeyDown((e) => {
|
|
538
|
+
if (e.keyCode !== monaco.KeyCode.Enter) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const pos = editor!.getPosition();
|
|
542
|
+
if (!pos) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const line = model!.getLineContent(pos.lineNumber);
|
|
546
|
+
const match = line.match(/^(\s*)([*+-]|\d+\.)\s+/);
|
|
547
|
+
if (!match) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
e.preventDefault();
|
|
551
|
+
|
|
552
|
+
if (line.trim() === match[2].trim()) {
|
|
553
|
+
const range = new monaco.Range(pos.lineNumber, 1, pos.lineNumber, line.length + 1);
|
|
554
|
+
editor!.executeEdits('exit-list', [{ range, text: '', forceMoveMarkers: true }]);
|
|
555
|
+
} else {
|
|
556
|
+
const isNum = match[2].includes('.');
|
|
557
|
+
const next = isNum ? `${parseInt(match[2]) + 1}. ` : `${match[2]} `;
|
|
558
|
+
editor!.trigger('keyboard', 'type', { text: `\n${match[1]}${next}` });
|
|
559
|
+
}
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
532
562
|
|
|
533
563
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
|
|
534
564
|
const selection = editor!.getSelection();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "custom",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"dompurify": "^3.2.4",
|
|
13
|
+
"marked": "^15.0.7",
|
|
14
|
+
"monaco-editor": "^0.45.0",
|
|
15
|
+
"turndown": "^7.2.2",
|
|
16
|
+
"turndown-plugin-gfm": "^1.0.2"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"node_modules/@mixmark-io/domino": {
|
|
20
|
+
"version": "2.2.0",
|
|
21
|
+
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
|
22
|
+
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
|
|
23
|
+
"license": "BSD-2-Clause"
|
|
24
|
+
},
|
|
25
|
+
"node_modules/@types/trusted-types": {
|
|
26
|
+
"version": "2.0.7",
|
|
27
|
+
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
|
28
|
+
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"node_modules/dompurify": {
|
|
33
|
+
"version": "3.3.2",
|
|
34
|
+
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
|
35
|
+
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
|
36
|
+
"license": "(MPL-2.0 OR Apache-2.0)",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"@types/trusted-types": "^2.0.7"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"node_modules/marked": {
|
|
45
|
+
"version": "15.0.12",
|
|
46
|
+
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
|
47
|
+
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"bin": {
|
|
50
|
+
"marked": "bin/marked.js"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">= 18"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"node_modules/monaco-editor": {
|
|
57
|
+
"version": "0.45.0",
|
|
58
|
+
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz",
|
|
59
|
+
"integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==",
|
|
60
|
+
"license": "MIT"
|
|
61
|
+
},
|
|
62
|
+
"node_modules/turndown": {
|
|
63
|
+
"version": "7.2.2",
|
|
64
|
+
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
|
|
65
|
+
"integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@mixmark-io/domino": "^2.2.0"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"node_modules/turndown-plugin-gfm": {
|
|
72
|
+
"version": "1.0.2",
|
|
73
|
+
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
|
|
74
|
+
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
|
|
75
|
+
"license": "MIT"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { markRaw } from 'vue';
|
|
3
|
+
import * as monaco from 'monaco-editor';
|
|
4
|
+
import { toggleWrapSmart } from './utils/monacoMarkdownToggle';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
IconLinkOutline, IconCodeOutline, IconRectangleListOutline,
|
|
8
|
+
IconOrderedListOutline, IconLetterBoldOutline, IconLetterUnderlineOutline,
|
|
9
|
+
IconLetterItalicOutline, IconTextSlashOutline
|
|
10
|
+
} from '@iconify-prerendered/vue-flowbite';
|
|
11
|
+
import { IconH116Solid, IconH216Solid, IconH316Solid } from '@iconify-prerendered/vue-heroicons';
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
editor: monaco.editor.IStandaloneCodeEditor | null;
|
|
15
|
+
meta: any;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const isBtnVisible = (btnKey: string) => {
|
|
19
|
+
const settings = props.meta?.topPanelSettings;
|
|
20
|
+
if (!settings || Object.keys(settings).length === 0) return true;
|
|
21
|
+
return settings[btnKey] !== undefined ? settings[btnKey] : true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const btnClass = 'flex items-center justify-center h-8 px-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200';
|
|
25
|
+
|
|
26
|
+
const fenceForCodeBlock = (text: string): string => {
|
|
27
|
+
let maxBackticks = 0;
|
|
28
|
+
let current = 0;
|
|
29
|
+
for (let i = 0; i < text.length; i++) {
|
|
30
|
+
if (text[i] === '`') { current++; if (current > maxBackticks) maxBackticks = current; }
|
|
31
|
+
else { current = 0; }
|
|
32
|
+
}
|
|
33
|
+
return '`'.repeat(Math.max(3, maxBackticks + 1));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const applyFormat = (type: string) => {
|
|
37
|
+
const editor = props.editor;
|
|
38
|
+
if (!editor) return;
|
|
39
|
+
|
|
40
|
+
const model = editor.getModel();
|
|
41
|
+
if (!model) return;
|
|
42
|
+
|
|
43
|
+
editor.focus();
|
|
44
|
+
const rawSelection = editor.getSelection();
|
|
45
|
+
if (!rawSelection) return;
|
|
46
|
+
|
|
47
|
+
const selection = rawSelection.startLineNumber !== rawSelection.endLineNumber && rawSelection.endColumn === 1
|
|
48
|
+
? new monaco.Selection(rawSelection.startLineNumber, rawSelection.startColumn, rawSelection.endLineNumber - 1, model.getLineMaxColumn(rawSelection.endLineNumber - 1))
|
|
49
|
+
: rawSelection;
|
|
50
|
+
|
|
51
|
+
const selectedText = model.getValueInRange(selection);
|
|
52
|
+
|
|
53
|
+
const applyEdits = (id: string, edits: monaco.editor.IIdentifiedSingleEditOperation[]) => {
|
|
54
|
+
editor.executeEdits(id, edits);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
switch (type) {
|
|
58
|
+
case 'bold': toggleWrapSmart(editor, '**'); break;
|
|
59
|
+
case 'italic': toggleWrapSmart(editor, '*'); break;
|
|
60
|
+
case 'strike': toggleWrapSmart(editor, '~~'); break;
|
|
61
|
+
case 'underline': toggleWrapSmart(editor, '<u>', '</u>'); break;
|
|
62
|
+
case 'codeBlock': {
|
|
63
|
+
const trimmed = selectedText.trim();
|
|
64
|
+
const match = trimmed.match(/^(`{3,})[^\n]*\n([\s\S]*)\n\1$/);
|
|
65
|
+
if (match) {
|
|
66
|
+
applyEdits('unwrap-code', [{ range: selection, text: match[2], forceMoveMarkers: true }]);
|
|
67
|
+
} else {
|
|
68
|
+
const fence = fenceForCodeBlock(selectedText);
|
|
69
|
+
applyEdits('wrap-code', [{ range: selection, text: `\n${fence}\n${selectedText}\n${fence}\n`, forceMoveMarkers: true }]);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'link': {
|
|
74
|
+
const match = selectedText.trim().match(/^\[(.*?)\]\(.*?\)$/);
|
|
75
|
+
if (match) {
|
|
76
|
+
applyEdits('unlink', [{ range: selection, text: match[1], forceMoveMarkers: true }]);
|
|
77
|
+
} else {
|
|
78
|
+
applyEdits('insert-link', [{ range: selection, text: `[${selectedText}](url)`, forceMoveMarkers: true }]);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'h1': case 'h2': case 'h3': case 'ul': case 'ol': {
|
|
83
|
+
const prefixMap: any = { h1: '# ', h2: '## ', h3: '### ', ul: '* ' };
|
|
84
|
+
const edits: any[] = [];
|
|
85
|
+
for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
|
|
86
|
+
const line = model.getLineContent(i);
|
|
87
|
+
const targetPrefix = type === 'ol' ? `${i - selection.startLineNumber + 1}. ` : prefixMap[type];
|
|
88
|
+
const match = line.match(/^(#{1,6}\s+|[*+-]\s+|\d+[.)]\s+)/);
|
|
89
|
+
if (match) {
|
|
90
|
+
edits.push({ range: new monaco.Range(i, 1, i, match[0].length + 1), text: match[0].trim() === targetPrefix.trim() ? '' : targetPrefix });
|
|
91
|
+
} else {
|
|
92
|
+
edits.push({ range: new monaco.Range(i, 1, i, 1), text: targetPrefix });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
applyEdits('format-block', edits);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const buttons = [
|
|
102
|
+
{ id: 'bold', title: 'Bold', icon: markRaw(IconLetterBoldOutline), group: 1 },
|
|
103
|
+
{ id: 'italic', title: 'Italic', icon: markRaw(IconLetterItalicOutline), group: 1 },
|
|
104
|
+
{ id: 'underline', title: 'Underline', icon: markRaw(IconLetterUnderlineOutline), group: 1 },
|
|
105
|
+
{ id: 'strike', title: 'Strike', icon: markRaw(IconTextSlashOutline), group: 1, separator: true },
|
|
106
|
+
{ id: 'h1', title: 'H1', icon: markRaw(IconH116Solid), group: 2 },
|
|
107
|
+
{ id: 'h2', title: 'H2', icon: markRaw(IconH216Solid), group: 2 },
|
|
108
|
+
{ id: 'h3', title: 'H3', icon: markRaw(IconH316Solid), group: 2, separator: true },
|
|
109
|
+
{ id: 'ul', title: 'UL', icon: markRaw(IconRectangleListOutline), group: 3 },
|
|
110
|
+
{ id: 'ol', title: 'OL', icon: markRaw(IconOrderedListOutline), group: 3 },
|
|
111
|
+
{ id: 'link', title: 'Link', icon: markRaw(IconLinkOutline), group: 3 },
|
|
112
|
+
{ id: 'codeBlock', title: 'Code', icon: markRaw(IconCodeOutline), group: 3 },
|
|
113
|
+
];
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="flex flex-wrap items-center gap-3 p-1.5 border border-gray-300 dark:border-gray-600 rounded-t-lg bg-gray-50 dark:bg-gray-800 w-full box-border">
|
|
118
|
+
<template v-for="btn in buttons" :key="btn.id">
|
|
119
|
+
<button
|
|
120
|
+
v-if="isBtnVisible(btn.id)"
|
|
121
|
+
type="button"
|
|
122
|
+
@click="applyFormat(btn.id)"
|
|
123
|
+
:class="btnClass"
|
|
124
|
+
:title="btn.title"
|
|
125
|
+
>
|
|
126
|
+
<component :is="btn.icon" class="w-5 h-5" />
|
|
127
|
+
</button>
|
|
128
|
+
<div v-if="btn.separator && isBtnVisible(btn.id)" class="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1"></div>
|
|
129
|
+
</template>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export default class MarkdownPlugin extends AdminForthPlugin {
|
|
|
41
41
|
modifyResourceConfig: { get: () => super.modifyResourceConfig }
|
|
42
42
|
});
|
|
43
43
|
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
-
var _a, _b;
|
|
44
|
+
var _a, _b, _c;
|
|
45
45
|
_super.modifyResourceConfig.call(this, adminforth, resourceConfig);
|
|
46
46
|
this.resourceConfig = resourceConfig;
|
|
47
47
|
const fieldName = this.options.fieldName;
|
|
@@ -117,6 +117,21 @@ export default class MarkdownPlugin extends AdminForthPlugin {
|
|
|
117
117
|
uploadPluginInstanceId: (_b = this.uploadPlugin) === null || _b === void 0 ? void 0 : _b.pluginInstanceId,
|
|
118
118
|
},
|
|
119
119
|
};
|
|
120
|
+
const topPanelSettings = this.options.topPanelSettings || {};
|
|
121
|
+
const commonMeta = {
|
|
122
|
+
pluginInstanceId: this.pluginInstanceId,
|
|
123
|
+
columnName: fieldName,
|
|
124
|
+
uploadPluginInstanceId: (_c = this.uploadPlugin) === null || _c === void 0 ? void 0 : _c.pluginInstanceId,
|
|
125
|
+
topPanelSettings: topPanelSettings,
|
|
126
|
+
};
|
|
127
|
+
column.components.edit = {
|
|
128
|
+
file: this.componentPath("MarkdownEditor.vue"),
|
|
129
|
+
meta: commonMeta,
|
|
130
|
+
};
|
|
131
|
+
column.components.create = {
|
|
132
|
+
file: this.componentPath("MarkdownEditor.vue"),
|
|
133
|
+
meta: commonMeta,
|
|
134
|
+
};
|
|
120
135
|
const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
|
|
121
136
|
if (this.options.attachments) {
|
|
122
137
|
const stripQueryAndHash = (value) => value.split('#')[0].split('?')[0];
|
package/index.ts
CHANGED
|
@@ -126,6 +126,26 @@ export default class MarkdownPlugin extends AdminForthPlugin {
|
|
|
126
126
|
uploadPluginInstanceId: this.uploadPlugin?.pluginInstanceId,
|
|
127
127
|
},
|
|
128
128
|
};
|
|
129
|
+
|
|
130
|
+
const topPanelSettings = this.options.topPanelSettings || {};
|
|
131
|
+
|
|
132
|
+
const commonMeta = {
|
|
133
|
+
pluginInstanceId: this.pluginInstanceId,
|
|
134
|
+
columnName: fieldName,
|
|
135
|
+
uploadPluginInstanceId: this.uploadPlugin?.pluginInstanceId,
|
|
136
|
+
topPanelSettings: topPanelSettings,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
column.components.edit = {
|
|
140
|
+
file: this.componentPath("MarkdownEditor.vue"),
|
|
141
|
+
meta: commonMeta,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
column.components.create = {
|
|
145
|
+
file: this.componentPath("MarkdownEditor.vue"),
|
|
146
|
+
meta: commonMeta,
|
|
147
|
+
};
|
|
148
|
+
|
|
129
149
|
const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
|
|
130
150
|
if (this.options.attachments) {
|
|
131
151
|
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -53,4 +53,33 @@ export interface PluginOptions {
|
|
|
53
53
|
*/
|
|
54
54
|
attachmentAltFieldName?: string; // e.g. 'alt',
|
|
55
55
|
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Optional configuration for the editor's top toolbar (formatting panel).
|
|
59
|
+
*
|
|
60
|
+
* If `topPanelSettings` is omitted, the editor uses its internal default
|
|
61
|
+
* toolbar configuration.
|
|
62
|
+
*
|
|
63
|
+
* If `topPanelSettings` is provided as an empty object, all controls behave
|
|
64
|
+
* as if their flags were `undefined`, i.e. they also fall back to the same
|
|
65
|
+
* internal defaults.
|
|
66
|
+
*
|
|
67
|
+
* For each flag below:
|
|
68
|
+
* - `true` – explicitly enable/show the control in the top panel.
|
|
69
|
+
* - `false` – explicitly disable/hide the control.
|
|
70
|
+
* - `undefined` – use the editor's default behavior for that control.
|
|
71
|
+
*/
|
|
72
|
+
topPanelSettings?: {
|
|
73
|
+
bold?: boolean;
|
|
74
|
+
italic?: boolean;
|
|
75
|
+
underline?: boolean;
|
|
76
|
+
strike?: boolean;
|
|
77
|
+
h1?: boolean;
|
|
78
|
+
h2?: boolean;
|
|
79
|
+
h3?: boolean;
|
|
80
|
+
ul?: boolean;
|
|
81
|
+
ol?: boolean;
|
|
82
|
+
link?: boolean;
|
|
83
|
+
codeBlock?: boolean;
|
|
84
|
+
};
|
|
56
85
|
}
|