@airalogy/aimd-editor 1.7.1

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 (36) hide show
  1. package/README.md +59 -0
  2. package/README.zh-CN.md +43 -0
  3. package/dist/AimdEditorTopBar.vue_vue_type_script_setup_true_lang-gbfMDZSh.js +1131 -0
  4. package/dist/AimdSourceEditor.vue_vue_type_script_setup_true_lang-t_sUoXky.js +274 -0
  5. package/dist/AimdWysiwygEditor.vue_vue_type_script_setup_true_lang-B8o1VbUH.js +25012 -0
  6. package/dist/aimd-editor.css +1 -0
  7. package/dist/embedded.js +11 -0
  8. package/dist/index.js +44 -0
  9. package/dist/monaco.js +16 -0
  10. package/dist/theme-B8dCnOx-.js +583 -0
  11. package/dist/vue.js +30 -0
  12. package/dist/wysiwyg.js +9 -0
  13. package/package.json +90 -0
  14. package/src/__tests__/editor.test.ts +296 -0
  15. package/src/embedded.ts +18 -0
  16. package/src/index.ts +10 -0
  17. package/src/language-config.ts +152 -0
  18. package/src/monaco.ts +19 -0
  19. package/src/theme.ts +166 -0
  20. package/src/tokens.ts +120 -0
  21. package/src/vue/AimdEditor.vue +715 -0
  22. package/src/vue/AimdEditorToolbar.vue +83 -0
  23. package/src/vue/AimdEditorTopBar.vue +39 -0
  24. package/src/vue/AimdFieldDialog.vue +1102 -0
  25. package/src/vue/AimdSourceEditor.vue +330 -0
  26. package/src/vue/AimdWysiwygEditor.vue +569 -0
  27. package/src/vue/aimdInlineMarkdownNormalization.ts +10 -0
  28. package/src/vue/comparableAimdMarkdown.ts +6 -0
  29. package/src/vue/env.d.ts +7 -0
  30. package/src/vue/index.ts +45 -0
  31. package/src/vue/locales.ts +667 -0
  32. package/src/vue/milkdown-aimd-plugin.ts +378 -0
  33. package/src/vue/programmaticMarkdownSyncGuard.ts +66 -0
  34. package/src/vue/types.ts +449 -0
  35. package/src/vue/useEditorContent.ts +252 -0
  36. package/src/wysiwyg.ts +17 -0
@@ -0,0 +1,569 @@
1
+ <script setup lang="ts">
2
+ import { ref, shallowRef, computed, watch, nextTick, defineComponent, h, toRef, onBeforeUnmount } from 'vue'
3
+ import type { Editor } from '@milkdown/kit/core'
4
+ import { MilkdownProvider, Milkdown, useEditor, useInstance } from '@milkdown/vue'
5
+ import { defaultValueCtx, Editor as MilkdownEditor, rootCtx, editorViewOptionsCtx, editorViewCtx } from '@milkdown/kit/core'
6
+ import type { Ctx, MilkdownPlugin } from '@milkdown/kit/ctx'
7
+ import { createTable } from '@milkdown/kit/preset/gfm'
8
+ import { commonmark, paragraphSchema, headingSchema, blockquoteSchema, bulletListSchema, orderedListSchema, codeBlockSchema, hrSchema, listItemSchema } from '@milkdown/kit/preset/commonmark'
9
+ import { commandsCtx } from '@milkdown/kit/core'
10
+ import { setBlockTypeCommand, wrapInBlockTypeCommand, addBlockTypeCommand, clearTextInCurrentBlockCommand } from '@milkdown/kit/preset/commonmark'
11
+ import { gfm } from '@milkdown/kit/preset/gfm'
12
+ import { history } from '@milkdown/kit/plugin/history'
13
+ import { listener, listenerCtx } from '@milkdown/kit/plugin/listener'
14
+ import { clipboard } from '@milkdown/kit/plugin/clipboard'
15
+ import { indent } from '@milkdown/kit/plugin/indent'
16
+ import { trailing } from '@milkdown/kit/plugin/trailing'
17
+ import { block, BlockProvider } from '@milkdown/kit/plugin/block'
18
+ import { replaceAll, $prose } from '@milkdown/kit/utils'
19
+ import { tableBlock, tableBlockConfig } from '@milkdown/kit/component/table-block'
20
+ import { Plugin, PluginKey, TextSelection } from '@milkdown/kit/prose/state'
21
+ import { Decoration, DecorationSet } from '@milkdown/kit/prose/view'
22
+ import { findParent } from '@milkdown/kit/prose'
23
+ import { protectAimdInlineTemplates } from '@airalogy/aimd-core'
24
+
25
+ import '@milkdown/theme-nord/style.css'
26
+ import '@milkdown/kit/prose/tables/style/tables.css'
27
+
28
+ import { aimdMilkdownPlugins } from './milkdown-aimd-plugin'
29
+ import { createProgrammaticMarkdownSyncGuard } from './programmaticMarkdownSyncGuard'
30
+ import { normalizeComparableAimdMarkdown } from './comparableAimdMarkdown'
31
+ import type { AimdFieldType } from './types'
32
+ import type { AimdEditorMessages } from './locales'
33
+
34
+ const props = withDefaults(defineProps<{
35
+ content: string
36
+ minHeight: number
37
+ enableBlockHandle: boolean
38
+ active?: boolean
39
+ readonly?: boolean
40
+ resolvedMessages: AimdEditorMessages
41
+ localizedFieldTypes: AimdFieldType[]
42
+ milkdownPlugins?: MilkdownPlugin[]
43
+ }>(), {
44
+ active: true,
45
+ readonly: false,
46
+ milkdownPlugins: undefined,
47
+ })
48
+
49
+ const emit = defineEmits<{
50
+ (e: 'markdown-updated', ctx: any, markdown: string, prev: string): void
51
+ (e: 'ready', editor: Editor): void
52
+ (e: 'open-aimd-dialog', type: string): void
53
+ }>()
54
+
55
+ function toMilkdownMarkdown(markdown: string): string {
56
+ return protectAimdInlineTemplates(markdown).content
57
+ }
58
+
59
+ function createEditorViewOptions(readonly: boolean) {
60
+ return {
61
+ attributes: {
62
+ class: readonly ? 'milkdown-editor-content milkdown-editor-content--readonly' : 'milkdown-editor-content',
63
+ spellcheck: 'false',
64
+ },
65
+ editable: () => !readonly,
66
+ }
67
+ }
68
+
69
+ // --- Block add menu ---
70
+ const showBlockMenu = ref(false)
71
+ const blockMenuPos = ref({ x: 0, y: 0 })
72
+ const blockProviderRef = shallowRef<BlockProvider | null>(null)
73
+
74
+ interface BlockMenuItem {
75
+ label: string
76
+ icon: string
77
+ onRun: (ctx: Ctx) => void
78
+ }
79
+
80
+ interface BlockMenuGroup {
81
+ label: string
82
+ items: BlockMenuItem[]
83
+ }
84
+
85
+ // Block menu SVG icon helper (14x14)
86
+ const _bsi = (d: string) => `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${d}</svg>`
87
+
88
+ const blockMenuGroups = computed<BlockMenuGroup[]>(() => ([
89
+ {
90
+ label: props.resolvedMessages.blockMenu.groups.text,
91
+ items: [
92
+ {
93
+ label: props.resolvedMessages.blockMenu.items.text, icon: 'T',
94
+ onRun: (ctx: Ctx) => {
95
+ const commands = ctx.get(commandsCtx)
96
+ commands.call(clearTextInCurrentBlockCommand.key)
97
+ commands.call(setBlockTypeCommand.key, { nodeType: paragraphSchema.type(ctx) })
98
+ },
99
+ },
100
+ {
101
+ label: props.resolvedMessages.blockMenu.items.heading1, icon: 'H1',
102
+ onRun: (ctx: Ctx) => {
103
+ const commands = ctx.get(commandsCtx)
104
+ commands.call(clearTextInCurrentBlockCommand.key)
105
+ commands.call(setBlockTypeCommand.key, { nodeType: headingSchema.type(ctx), attrs: { level: 1 } })
106
+ },
107
+ },
108
+ {
109
+ label: props.resolvedMessages.blockMenu.items.heading2, icon: 'H2',
110
+ onRun: (ctx: Ctx) => {
111
+ const commands = ctx.get(commandsCtx)
112
+ commands.call(clearTextInCurrentBlockCommand.key)
113
+ commands.call(setBlockTypeCommand.key, { nodeType: headingSchema.type(ctx), attrs: { level: 2 } })
114
+ },
115
+ },
116
+ {
117
+ label: props.resolvedMessages.blockMenu.items.heading3, icon: 'H3',
118
+ onRun: (ctx: Ctx) => {
119
+ const commands = ctx.get(commandsCtx)
120
+ commands.call(clearTextInCurrentBlockCommand.key)
121
+ commands.call(setBlockTypeCommand.key, { nodeType: headingSchema.type(ctx), attrs: { level: 3 } })
122
+ },
123
+ },
124
+ {
125
+ label: props.resolvedMessages.blockMenu.items.quote,
126
+ icon: _bsi('<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/>'),
127
+ onRun: (ctx: Ctx) => {
128
+ const commands = ctx.get(commandsCtx)
129
+ commands.call(clearTextInCurrentBlockCommand.key)
130
+ commands.call(wrapInBlockTypeCommand.key, { nodeType: blockquoteSchema.type(ctx) })
131
+ },
132
+ },
133
+ {
134
+ label: props.resolvedMessages.blockMenu.items.divider,
135
+ icon: _bsi('<line x1="2" y1="12" x2="22" y2="12" stroke-width="2.5"/>'),
136
+ onRun: (ctx: Ctx) => {
137
+ const commands = ctx.get(commandsCtx)
138
+ commands.call(clearTextInCurrentBlockCommand.key)
139
+ commands.call(addBlockTypeCommand.key, { nodeType: hrSchema.type(ctx) })
140
+ },
141
+ },
142
+ ],
143
+ },
144
+ {
145
+ label: props.resolvedMessages.blockMenu.groups.list,
146
+ items: [
147
+ {
148
+ label: props.resolvedMessages.blockMenu.items.bulletList,
149
+ icon: _bsi('<line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="5" cy="6" r="1" fill="currentColor"/><circle cx="5" cy="12" r="1" fill="currentColor"/><circle cx="5" cy="18" r="1" fill="currentColor"/>'),
150
+ onRun: (ctx: Ctx) => {
151
+ const commands = ctx.get(commandsCtx)
152
+ commands.call(clearTextInCurrentBlockCommand.key)
153
+ commands.call(wrapInBlockTypeCommand.key, { nodeType: bulletListSchema.type(ctx) })
154
+ },
155
+ },
156
+ {
157
+ label: props.resolvedMessages.blockMenu.items.orderedList,
158
+ icon: _bsi('<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><text x="3" y="7.5" font-size="6" fill="currentColor" stroke="none" font-weight="600">1</text><text x="3" y="13.5" font-size="6" fill="currentColor" stroke="none" font-weight="600">2</text><text x="3" y="19.5" font-size="6" fill="currentColor" stroke="none" font-weight="600">3</text>'),
159
+ onRun: (ctx: Ctx) => {
160
+ const commands = ctx.get(commandsCtx)
161
+ commands.call(clearTextInCurrentBlockCommand.key)
162
+ commands.call(wrapInBlockTypeCommand.key, { nodeType: orderedListSchema.type(ctx) })
163
+ },
164
+ },
165
+ ],
166
+ },
167
+ {
168
+ label: props.resolvedMessages.blockMenu.groups.advanced,
169
+ items: [
170
+ {
171
+ label: props.resolvedMessages.blockMenu.items.codeBlock,
172
+ icon: _bsi('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'),
173
+ onRun: (ctx: Ctx) => {
174
+ const commands = ctx.get(commandsCtx)
175
+ commands.call(clearTextInCurrentBlockCommand.key)
176
+ commands.call(setBlockTypeCommand.key, { nodeType: codeBlockSchema.type(ctx) })
177
+ },
178
+ },
179
+ {
180
+ label: props.resolvedMessages.blockMenu.items.table,
181
+ icon: _bsi('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>'),
182
+ onRun: (ctx: Ctx) => {
183
+ const commands = ctx.get(commandsCtx)
184
+ commands.call(clearTextInCurrentBlockCommand.key)
185
+ commands.call(addBlockTypeCommand.key, { nodeType: createTable(ctx, 3, 3) })
186
+ },
187
+ },
188
+ ],
189
+ },
190
+ {
191
+ label: props.resolvedMessages.blockMenu.groups.aimd,
192
+ items: props.localizedFieldTypes.map(ft => ({
193
+ label: ft.label,
194
+ icon: ft.svgIcon,
195
+ onRun: (_ctx: any) => {
196
+ emit('open-aimd-dialog', ft.type)
197
+ },
198
+ })),
199
+ },
200
+ ]).filter(group => group.items.length > 0))
201
+
202
+ function onBlockMenuClick(item: BlockMenuItem) {
203
+ showBlockMenu.value = false
204
+ const editor = milkdownEditorRef.value
205
+ if (!editor) return
206
+
207
+ try {
208
+ editor.action((ctx) => {
209
+ item.onRun(ctx)
210
+ nextTick(() => {
211
+ try {
212
+ const view = ctx.get(editorViewCtx)
213
+ view.focus()
214
+ } catch {}
215
+ })
216
+ })
217
+ } catch {}
218
+ }
219
+
220
+ function closeBlockMenu(e: MouseEvent) {
221
+ const menu = document.querySelector('.aimd-block-add-menu')
222
+ const btn = document.querySelector('.aimd-block-handle-btn')
223
+ if (menu && !menu.contains(e.target as Node) && !(btn && btn.contains(e.target as Node))) {
224
+ showBlockMenu.value = false
225
+ }
226
+ }
227
+
228
+ let blockMenuListenerTimer: ReturnType<typeof setTimeout> | null = null
229
+
230
+ watch(showBlockMenu, (val) => {
231
+ if (val) {
232
+ blockMenuListenerTimer = setTimeout(() => {
233
+ blockMenuListenerTimer = null
234
+ document.addEventListener('click', closeBlockMenu)
235
+ }, 0)
236
+ } else {
237
+ if (blockMenuListenerTimer !== null) {
238
+ clearTimeout(blockMenuListenerTimer)
239
+ blockMenuListenerTimer = null
240
+ }
241
+ document.removeEventListener('click', closeBlockMenu)
242
+ }
243
+ })
244
+
245
+ // --- Placeholder plugin ---
246
+ const placeholderPlugin = $prose(() => {
247
+ return new Plugin({
248
+ key: new PluginKey('AIMD_PLACEHOLDER'),
249
+ props: {
250
+ decorations: (state) => {
251
+ const { selection } = state
252
+ if (!selection.empty) return null
253
+
254
+ const $pos = selection.$anchor
255
+ const node = $pos.parent
256
+ if (node.content.size > 0) return null
257
+
258
+ const inTable = findParent((n) => n.type.name === 'table')($pos)
259
+ if (inTable) return null
260
+
261
+ const before = $pos.before()
262
+ const deco = Decoration.node(before, before + node.nodeSize, {
263
+ class: 'aimd-placeholder',
264
+ 'data-placeholder': props.resolvedMessages.blockMenu.placeholder,
265
+ })
266
+ return DecorationSet.create(state.doc, [deco])
267
+ },
268
+ },
269
+ })
270
+ })
271
+
272
+ // --- Table block icon SVGs ---
273
+ const tableIcons = {
274
+ plus: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g clip-path="url(#clip0_971_7676)"><path d="M18 13H13V18C13 18.55 12.55 19 12 19C11.45 19 11 18.55 11 18V13H6C5.45 13 5 12.55 5 12C5 11.45 5.45 11 6 11H11V6C11 5.45 11.45 5 12 5C12.55 5 13 5.45 13 6V11H18C18.55 11 19 11.45 19 12C19 12.55 18.55 13 18 13Z"/></g><defs><clipPath id="clip0_971_7676"><rect width="24" height="24"/></clipPath></defs></svg>`,
275
+ remove: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.30775 20.4997C6.81058 20.4997 6.385 20.3227 6.031 19.9687C5.677 19.6147 5.5 19.1892 5.5 18.692V5.99973H5.25C5.0375 5.99973 4.85942 5.92782 4.71575 5.78398C4.57192 5.64015 4.5 5.46198 4.5 5.24948C4.5 5.03682 4.57192 4.85873 4.71575 4.71523C4.85942 4.57157 5.0375 4.49973 5.25 4.49973H9C9 4.2549 9.08625 4.04624 9.25875 3.87374C9.43108 3.7014 9.63967 3.61523 9.8845 3.61523H14.1155C14.3603 3.61523 14.5689 3.7014 14.7413 3.87374C14.9138 4.04624 15 4.2549 15 4.49973H18.75C18.9625 4.49973 19.1406 4.57165 19.2843 4.71548C19.4281 4.85932 19.5 5.03748 19.5 5.24998C19.5 5.46265 19.4281 5.64073 19.2843 5.78423C19.1406 5.9279 18.9625 5.99973 18.75 5.99973H18.5V18.692C18.5 19.1892 18.323 19.6147 17.969 19.9687C17.615 20.3227 17.1894 20.4997 16.6923 20.4997H7.30775ZM17 5.99973H7V18.692C7 18.7818 7.02883 18.8556 7.0865 18.9132C7.14417 18.9709 7.21792 18.9997 7.30775 18.9997H16.6923C16.7821 18.9997 16.8558 18.9709 16.9135 18.9132C16.9712 18.8556 17 18.7818 17 18.692V5.99973ZM10.1543 16.9997C10.3668 16.9997 10.5448 16.9279 10.6885 16.7842C10.832 16.6404 10.9037 16.4622 10.9037 16.2497V8.74973C10.9037 8.53723 10.8318 8.35907 10.688 8.21523C10.5443 8.07157 10.3662 7.99973 10.1535 7.99973C9.941 7.99973 9.76292 8.07157 9.61925 8.21523C9.47575 8.35907 9.404 8.53723 9.404 8.74973V16.2497C9.404 16.4622 9.47583 16.6404 9.6195 16.7842C9.76333 16.9279 9.94158 16.9997 10.1543 16.9997ZM13.8465 16.9997C14.059 16.9997 14.2371 16.9279 14.3807 16.7842C14.5243 16.6404 14.596 16.4622 14.596 16.2497V8.74973C14.596 8.53723 14.5242 8.35907 14.3805 8.21523C14.2367 8.07157 14.0584 7.99973 13.8458 7.99973C13.6333 7.99973 13.4552 8.07157 13.3115 8.21523C13.168 8.35907 13.0962 8.53723 13.0962 8.74973V16.2497C13.0962 16.4622 13.1682 16.6404 13.312 16.7842C13.4557 16.9279 13.6338 16.9997 13.8465 16.9997Z"/></svg>`,
276
+ alignLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4.25 20.5C4.0375 20.5 3.85942 20.4281 3.71575 20.2843C3.57192 20.1404 3.5 19.9622 3.5 19.7498C3.5 19.5371 3.57192 19.359 3.71575 19.2155C3.85942 19.0718 4.0375 19 4.25 19H19.75C19.9625 19 20.1406 19.0719 20.2843 19.2158C20.4281 19.3596 20.5 19.5378 20.5 19.7502C20.5 19.9629 20.4281 20.141 20.2843 20.2845C20.1406 20.4282 19.9625 20.5 19.75 20.5H4.25ZM4.25 16.625C4.0375 16.625 3.85942 16.5531 3.71575 16.4093C3.57192 16.2654 3.5 16.0872 3.5 15.8748C3.5 15.6621 3.57192 15.484 3.71575 15.3405C3.85942 15.1968 4.0375 15.125 4.25 15.125H13.75C13.9625 15.125 14.1406 15.1969 14.2843 15.3408C14.4281 15.4846 14.5 15.6628 14.5 15.8753C14.5 16.0879 14.4281 16.266 14.2843 16.4095C14.1406 16.5532 13.9625 16.625 13.75 16.625H4.25ZM4.25 12.75C4.0375 12.75 3.85942 12.6781 3.71575 12.5343C3.57192 12.3904 3.5 12.2122 3.5 11.9998C3.5 11.7871 3.57192 11.609 3.71575 11.4655C3.85942 11.3218 4.0375 11.25 4.25 11.25H19.75C19.9625 11.25 20.1406 11.3219 20.2843 11.4658C20.4281 11.6096 20.5 11.7878 20.5 12.0003C20.5 12.2129 20.4281 12.391 20.2843 12.5345C20.1406 12.6782 19.9625 12.75 19.75 12.75H4.25ZM4.25 8.875C4.0375 8.875 3.85942 8.80308 3.71575 8.65925C3.57192 8.51542 3.5 8.33725 3.5 8.12475C3.5 7.91208 3.57192 7.734 3.71575 7.5905C3.85942 7.44683 4.0375 7.375 4.25 7.375H13.75C13.9625 7.375 14.1406 7.44692 14.2843 7.59075C14.4281 7.73458 14.5 7.91275 14.5 8.12525C14.5 8.33792 14.4281 8.516 14.2843 8.6595C14.1406 8.80317 13.9625 8.875 13.75 8.875H4.25ZM4.25 5C4.0375 5 3.85942 4.92808 3.71575 4.78425C3.57192 4.64042 3.5 4.46225 3.5 4.24975C3.5 4.03708 3.57192 3.859 3.71575 3.7155C3.85942 3.57183 4.0375 3.5 4.25 3.5H19.75C19.9625 3.5 20.1406 3.57192 20.2843 3.71575C20.4281 3.85958 20.5 4.03775 20.5 4.25025C20.5 4.46292 20.4281 4.641 20.2843 4.7845C20.1406 4.92817 19.9625 5 19.75 5H4.25Z"/></svg>`,
277
+ alignCenter: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4.25 20.5C4.0375 20.5 3.85942 20.4281 3.71575 20.2843C3.57192 20.1404 3.5 19.9622 3.5 19.7498C3.5 19.5371 3.57192 19.359 3.71575 19.2155C3.85942 19.0718 4.0375 19 4.25 19H19.75C19.9625 19 20.1406 19.0719 20.2843 19.2158C20.4281 19.3596 20.5 19.5378 20.5 19.7502C20.5 19.9629 20.4281 20.141 20.2843 20.2845C20.1406 20.4282 19.9625 20.5 19.75 20.5H4.25ZM8.25 16.625C8.0375 16.625 7.85942 16.5531 7.71575 16.4093C7.57192 16.2654 7.5 16.0872 7.5 15.8748C7.5 15.6621 7.57192 15.484 7.71575 15.3405C7.85942 15.1968 8.0375 15.125 8.25 15.125H15.75C15.9625 15.125 16.1406 15.1969 16.2843 15.3408C16.4281 15.4846 16.5 15.6628 16.5 15.8753C16.5 16.0879 16.4281 16.266 16.2843 16.4095C16.1406 16.5532 15.9625 16.625 15.75 16.625H8.25ZM4.25 12.75C4.0375 12.75 3.85942 12.6781 3.71575 12.5343C3.57192 12.3904 3.5 12.2122 3.5 11.9998C3.5 11.7871 3.57192 11.609 3.71575 11.4655C3.85942 11.3218 4.0375 11.25 4.25 11.25H19.75C19.9625 11.25 20.1406 11.3219 20.2843 11.4658C20.4281 11.6096 20.5 11.7878 20.5 12.0003C20.5 12.2129 20.4281 12.391 20.2843 12.5345C20.1406 12.6782 19.9625 12.75 19.75 12.75H4.25ZM8.25 8.875C8.0375 8.875 7.85942 8.80308 7.71575 8.65925C7.57192 8.51542 7.5 8.33725 7.5 8.12475C7.5 7.91208 7.57192 7.734 7.71575 7.5905C7.85942 7.44683 8.0375 7.375 8.25 7.375H15.75C15.9625 7.375 16.1406 7.44692 16.2843 7.59075C16.4281 7.73458 16.5 7.91275 16.5 8.12525C16.5 8.33792 16.4281 8.516 16.2843 8.6595C16.1406 8.80317 15.9625 8.875 15.75 8.875H8.25ZM4.25 5C4.0375 5 3.85942 4.92808 3.71575 4.78425C3.57192 4.64042 3.5 4.46225 3.5 4.24975C3.5 4.03708 3.57192 3.859 3.71575 3.7155C3.85942 3.57183 4.0375 3.5 4.25 3.5H19.75C19.9625 3.5 20.1406 3.57192 20.2843 3.71575C20.4281 3.85958 20.5 4.03775 20.5 4.25025C20.5 4.46292 20.4281 4.641 20.2843 4.7845C20.1406 4.92817 19.9625 5 19.75 5H4.25Z"/></svg>`,
278
+ alignRight: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4.25 5C4.0375 5 3.85942 4.92808 3.71575 4.78425C3.57192 4.64042 3.5 4.46225 3.5 4.24975C3.5 4.03708 3.57192 3.859 3.71575 3.7155C3.85942 3.57183 4.0375 3.5 4.25 3.5H19.75C19.9625 3.5 20.1406 3.57192 20.2843 3.71575C20.4281 3.85958 20.5 4.03775 20.5 4.25025C20.5 4.46292 20.4281 4.641 20.2843 4.7845C20.1406 4.92817 19.9625 5 19.75 5H4.25ZM10.25 8.875C10.0375 8.875 9.85942 8.80308 9.71575 8.65925C9.57192 8.51542 9.5 8.33725 9.5 8.12475C9.5 7.91208 9.57192 7.734 9.71575 7.5905C9.85942 7.44683 10.0375 7.375 10.25 7.375H19.75C19.9625 7.375 20.1406 7.44692 20.2843 7.59075C20.4281 7.73458 20.5 7.91275 20.5 8.12525C20.5 8.33792 20.4281 8.516 20.2843 8.6595C20.1406 8.80317 19.9625 8.875 19.75 8.875H10.25ZM4.25 12.75C4.0375 12.75 3.85942 12.6781 3.71575 12.5343C3.57192 12.3904 3.5 12.2122 3.5 11.9998C3.5 11.7871 3.57192 11.609 3.71575 11.4655C3.85942 11.3218 4.0375 11.25 4.25 11.25H19.75C19.9625 11.25 20.1406 11.3219 20.2843 11.4658C20.4281 11.6096 20.5 11.7878 20.5 12.0003C20.5 12.2129 20.4281 12.391 20.2843 12.5345C20.1406 12.6782 19.9625 12.75 19.75 12.75H4.25ZM10.25 16.625C10.0375 16.625 9.85942 16.5531 9.71575 16.4093C9.57192 16.2654 9.5 16.0872 9.5 15.8748C9.5 15.6621 9.57192 15.484 9.71575 15.3405C9.85942 15.1968 10.0375 15.125 10.25 15.125H19.75C19.9625 15.125 20.1406 15.1969 20.2843 15.3408C20.4281 15.4846 20.5 15.6628 20.5 15.8753C20.5 16.0879 20.4281 16.266 20.2843 16.4095C20.1406 16.5532 19.9625 16.625 19.75 16.625H10.25ZM4.25 20.5C4.0375 20.5 3.85942 20.4281 3.71575 20.2843C3.57192 20.1404 3.5 19.9622 3.5 19.7498C3.5 19.5371 3.57192 19.359 3.71575 19.2155C3.85942 19.0718 4.0375 19 4.25 19H19.75C19.9625 19 20.1406 19.0719 20.2843 19.2158C20.4281 19.3596 20.5 19.5378 20.5 19.7502C20.5 19.9629 20.4281 20.141 20.2843 20.2845C20.1406 20.4282 19.9625 20.5 19.75 20.5H4.25Z"/></svg>`,
279
+ dragHandle: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3.5 9.83366C3.35833 9.83366 3.23961 9.78571 3.14383 9.68983C3.04794 9.59394 3 9.47516 3 9.33349C3 9.19171 3.04794 9.07299 3.14383 8.97733C3.23961 8.88155 3.35833 8.83366 3.5 8.83366H12.5C12.6417 8.83366 12.7604 8.8816 12.8562 8.97749C12.9521 9.07338 13 9.19216 13 9.33383C13 9.4756 12.9521 9.59433 12.8562 9.68999C12.7604 9.78577 12.6417 9.83366 12.5 9.83366H3.5ZM3.5 7.16699C3.35833 7.16699 3.23961 7.11905 3.14383 7.02316C3.04794 6.92727 3 6.80849 3 6.66683C3 6.52505 3.04794 6.40633 3.14383 6.31066C3.23961 6.21488 3.35833 6.16699 3.5 6.16699H12.5C12.6417 6.16699 12.7604 6.21494 12.8562 6.31083C12.9521 6.40671 13 6.52549 13 6.66716C13 6.80894 12.9521 6.92766 12.8562 7.02333C12.7604 7.1191 12.6417 7.16699 12.5 7.16699H3.5Z"/></svg>`,
280
+ }
281
+
282
+ const milkdownEditorRef = shallowRef<Editor | null>(null)
283
+ let isSyncingContent = false
284
+ let lastKnownMarkdown = normalizeComparableAimdMarkdown(props.content)
285
+ const programmaticMarkdownSyncGuard = createProgrammaticMarkdownSyncGuard()
286
+ const PROGRAMMATIC_MARKDOWN_SUPPRESSION_MS = 420
287
+ let suppressProgrammaticMarkdownUpdatedUntil = 0
288
+
289
+ // --- Milkdown inner component ---
290
+ const MilkdownEditorInner = defineComponent({
291
+ name: 'MilkdownEditorInner',
292
+ props: {
293
+ defaultValue: { type: String, default: '' },
294
+ enableBlockHandle: { type: Boolean, default: true },
295
+ readonly: { type: Boolean, default: false },
296
+ milkdownPlugins: { type: Array as () => MilkdownPlugin[] | undefined, default: undefined },
297
+ },
298
+ emits: ['ready', 'markdown-updated'],
299
+ setup(innerProps, { emit: innerEmit }) {
300
+ const defaultVal = toRef(innerProps, 'defaultValue')
301
+
302
+ useEditor((root) => {
303
+ const editor = MilkdownEditor.make()
304
+ .config((ctx) => {
305
+ ctx.set(rootCtx, root)
306
+ ctx.set(defaultValueCtx, toMilkdownMarkdown(defaultVal.value))
307
+ ctx.set(editorViewOptionsCtx, createEditorViewOptions(innerProps.readonly))
308
+ ctx.get(listenerCtx)
309
+ .markdownUpdated((_ctx, markdown, prev) => {
310
+ if (markdown !== prev) {
311
+ innerEmit('markdown-updated', _ctx, markdown, prev)
312
+ }
313
+ })
314
+ })
315
+ .use(commonmark)
316
+ .use(gfm)
317
+ .use(history)
318
+ .use(listener)
319
+ .use(clipboard)
320
+ .use(indent)
321
+ .use(trailing)
322
+ .use(innerProps.milkdownPlugins ?? aimdMilkdownPlugins)
323
+ .use(tableBlock)
324
+ .use(placeholderPlugin)
325
+ .config((ctx) => {
326
+ ctx.update(tableBlockConfig.key, (defaultConfig) => ({
327
+ ...defaultConfig,
328
+ renderButton: (renderType: string) => {
329
+ switch (renderType) {
330
+ case 'add_row': return tableIcons.plus
331
+ case 'add_col': return tableIcons.plus
332
+ case 'delete_row': return tableIcons.remove
333
+ case 'delete_col': return tableIcons.remove
334
+ case 'align_col_left': return tableIcons.alignLeft
335
+ case 'align_col_center': return tableIcons.alignCenter
336
+ case 'align_col_right': return tableIcons.alignRight
337
+ case 'col_drag_handle': return tableIcons.dragHandle
338
+ case 'row_drag_handle': return tableIcons.dragHandle
339
+ default: return ''
340
+ }
341
+ },
342
+ }))
343
+ })
344
+
345
+ if (innerProps.enableBlockHandle) {
346
+ editor.use(block)
347
+ }
348
+
349
+ return editor
350
+ })
351
+
352
+ const [loadingState, getInstance] = useInstance()
353
+
354
+ watch(loadingState, (isLoading) => {
355
+ if (!isLoading) {
356
+ const editorInstance = getInstance()
357
+ if (editorInstance) {
358
+ innerEmit('ready', editorInstance)
359
+
360
+ if (innerProps.enableBlockHandle) {
361
+ nextTick(() => {
362
+ try {
363
+ editorInstance.action((ctx) => {
364
+ const blockContent = document.createElement('div')
365
+ blockContent.className = 'aimd-block-handle'
366
+
367
+ const btn = document.createElement('div')
368
+ btn.className = 'aimd-block-handle-btn'
369
+ btn.title = props.resolvedMessages.blockMenu.addBlockTitle
370
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`
371
+
372
+ btn.addEventListener('mousedown', (e) => {
373
+ e.preventDefault()
374
+ e.stopPropagation()
375
+ })
376
+ blockContent.appendChild(btn)
377
+
378
+ // eslint-disable-next-line prefer-const
379
+ let providerRef: any = null
380
+
381
+ btn.addEventListener('click', (e) => {
382
+ e.preventDefault()
383
+ e.stopPropagation()
384
+
385
+ const rect = btn.getBoundingClientRect()
386
+
387
+ const view = ctx.get(editorViewCtx)
388
+ if (!view.hasFocus()) view.focus()
389
+
390
+ const active = providerRef?.active
391
+ if (!active) return
392
+
393
+ const { state, dispatch } = view
394
+ const pos = active.$pos.pos + active.node.nodeSize
395
+ let tr = state.tr.insert(pos, paragraphSchema.type(ctx).create())
396
+ tr = tr.setSelection(TextSelection.near(tr.doc.resolve(pos)))
397
+ dispatch(tr.scrollIntoView())
398
+
399
+ providerRef?.hide()
400
+
401
+ blockMenuPos.value = { x: rect.right + 8, y: rect.top }
402
+ showBlockMenu.value = true
403
+
404
+ nextTick(() => {
405
+ const menuEl = document.querySelector('.aimd-block-add-menu') as HTMLElement
406
+ if (menuEl) {
407
+ const menuRect = menuEl.getBoundingClientRect()
408
+ const viewportHeight = window.innerHeight
409
+ if (menuRect.bottom > viewportHeight) {
410
+ const newTop = rect.top - menuRect.height
411
+ blockMenuPos.value = { x: rect.right + 8, y: Math.max(4, newTop) }
412
+ }
413
+ }
414
+ })
415
+ })
416
+
417
+ const provider = new BlockProvider({
418
+ ctx,
419
+ content: blockContent,
420
+ getOffset: () => ({ mainAxis: 16, crossAxis: 0 }),
421
+ getPlacement: () => 'left',
422
+ })
423
+ provider.update()
424
+ blockContent.draggable = false
425
+ providerRef = provider
426
+ blockProviderRef.value = provider
427
+ })
428
+ } catch (e) {
429
+ console.warn('BlockProvider init failed:', e)
430
+ }
431
+ })
432
+ }
433
+ }
434
+ }
435
+ }, { immediate: true })
436
+
437
+ return () => h(Milkdown)
438
+ },
439
+ })
440
+
441
+ function onInnerReady(editor: Editor) {
442
+ milkdownEditorRef.value = editor
443
+ lastKnownMarkdown = normalizeComparableAimdMarkdown(props.content)
444
+ emit('ready', editor)
445
+ }
446
+
447
+ function onInnerMarkdownUpdated(ctx: any, markdown: string, prev: string) {
448
+ const isWithinProgrammaticSuppressionWindow = Date.now() < suppressProgrammaticMarkdownUpdatedUntil
449
+ const comparableMarkdown = normalizeComparableAimdMarkdown(markdown)
450
+ if (
451
+ isSyncingContent
452
+ || isWithinProgrammaticSuppressionWindow
453
+ || programmaticMarkdownSyncGuard.consume(comparableMarkdown)
454
+ ) {
455
+ lastKnownMarkdown = isWithinProgrammaticSuppressionWindow
456
+ ? normalizeComparableAimdMarkdown(props.content)
457
+ : comparableMarkdown
458
+ return
459
+ }
460
+
461
+ lastKnownMarkdown = comparableMarkdown
462
+ emit('markdown-updated', ctx, markdown, prev)
463
+ }
464
+
465
+ function syncMilkdownContent(content: string): boolean {
466
+ if (!milkdownEditorRef.value) {
467
+ return false
468
+ }
469
+
470
+ isSyncingContent = true
471
+ try {
472
+ const comparableContent = normalizeComparableAimdMarkdown(content)
473
+ suppressProgrammaticMarkdownUpdatedUntil = Date.now() + PROGRAMMATIC_MARKDOWN_SUPPRESSION_MS
474
+ programmaticMarkdownSyncGuard.track(comparableContent)
475
+ milkdownEditorRef.value.action(replaceAll(toMilkdownMarkdown(content)))
476
+ lastKnownMarkdown = comparableContent
477
+ } catch {
478
+ isSyncingContent = false
479
+ return false
480
+ }
481
+ isSyncingContent = false
482
+ return true
483
+ }
484
+
485
+ watch(() => props.content, (content) => {
486
+ const comparableContent = normalizeComparableAimdMarkdown(content)
487
+ if (!props.active || !milkdownEditorRef.value || comparableContent === lastKnownMarkdown) {
488
+ return
489
+ }
490
+
491
+ syncMilkdownContent(content)
492
+ })
493
+
494
+ watch(() => props.readonly, (readonly) => {
495
+ if (!milkdownEditorRef.value) {
496
+ return
497
+ }
498
+
499
+ try {
500
+ milkdownEditorRef.value.action((ctx) => {
501
+ ctx.get(editorViewCtx).setProps(createEditorViewOptions(!!readonly))
502
+ })
503
+ } catch {}
504
+ }, { immediate: true })
505
+
506
+ watch(() => props.active, async (active) => {
507
+ if (
508
+ !active
509
+ || !milkdownEditorRef.value
510
+ || normalizeComparableAimdMarkdown(props.content) === lastKnownMarkdown
511
+ ) {
512
+ return
513
+ }
514
+
515
+ await nextTick()
516
+ syncMilkdownContent(props.content)
517
+ })
518
+
519
+ onBeforeUnmount(() => {
520
+ programmaticMarkdownSyncGuard.clear()
521
+ blockProviderRef.value?.destroy()
522
+ if (blockMenuListenerTimer !== null) {
523
+ clearTimeout(blockMenuListenerTimer)
524
+ blockMenuListenerTimer = null
525
+ }
526
+ document.removeEventListener('click', closeBlockMenu)
527
+ })
528
+
529
+ // Expose editor ref for parent
530
+ function getEditor() { return milkdownEditorRef.value }
531
+
532
+ defineExpose({
533
+ getEditor,
534
+ })
535
+ </script>
536
+
537
+ <template>
538
+ <div class="aimd-editor-wysiwyg-mode" :style="{ height: minHeight + 'px', overflowY: 'auto' }">
539
+ <MilkdownProvider>
540
+ <MilkdownEditorInner
541
+ :default-value="content"
542
+ :enable-block-handle="enableBlockHandle"
543
+ :readonly="!!readonly"
544
+ :milkdown-plugins="milkdownPlugins"
545
+ @ready="onInnerReady"
546
+ @markdown-updated="onInnerMarkdownUpdated"
547
+ />
548
+ </MilkdownProvider>
549
+ </div>
550
+
551
+ <!-- Block add menu (teleported to body for correct positioning) -->
552
+ <Teleport to="body">
553
+ <div v-if="showBlockMenu" class="aimd-block-add-menu" :style="{ top: blockMenuPos.y + 'px', left: blockMenuPos.x + 'px' }">
554
+ <template v-for="(group, gi) in blockMenuGroups" :key="group.label">
555
+ <div v-if="gi > 0" class="aimd-block-add-menu-divider" />
556
+ <div class="aimd-block-add-menu-group-label">{{ group.label }}</div>
557
+ <button
558
+ v-for="item in group.items"
559
+ :key="item.label"
560
+ class="aimd-block-add-menu-item"
561
+ @click="onBlockMenuClick(item)"
562
+ >
563
+ <span class="aimd-block-add-menu-icon" v-html="item.icon" />
564
+ <span>{{ item.label }}</span>
565
+ </button>
566
+ </template>
567
+ </div>
568
+ </Teleport>
569
+ </template>
@@ -0,0 +1,10 @@
1
+ const AIMD_INLINE_TEMPLATE_PATTERN = /\{\{(var_table|var|step|check|ref_step|ref_var|ref_fig|cite)\s*\|[^}]+?\}\}/g
2
+ const MARKDOWN_ESCAPABLE_PATTERN = /\\([\\!"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-])/g
3
+
4
+ function normalizeSingleAimdInlineTemplate(template: string): string {
5
+ return template.replace(MARKDOWN_ESCAPABLE_PATTERN, '$1')
6
+ }
7
+
8
+ export function normalizeAimdInlineTemplateMarkdownEscapes(content: string): string {
9
+ return content.replace(AIMD_INLINE_TEMPLATE_PATTERN, normalizeSingleAimdInlineTemplate)
10
+ }
@@ -0,0 +1,6 @@
1
+ import { restoreAimdInlineTemplates } from '@airalogy/aimd-core'
2
+ import { normalizeAimdInlineTemplateMarkdownEscapes } from './aimdInlineMarkdownNormalization'
3
+
4
+ export function normalizeComparableAimdMarkdown(markdown: string): string {
5
+ return normalizeAimdInlineTemplateMarkdownEscapes(restoreAimdInlineTemplates(markdown))
6
+ }
@@ -0,0 +1,7 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<{}, {}, any>
4
+ export default component
5
+ }
6
+
7
+ declare module '*.css' {}
@@ -0,0 +1,45 @@
1
+ export { default as AimdEditor } from './AimdEditor.vue'
2
+ export { default as AimdEditorToolbar } from './AimdEditorToolbar.vue'
3
+ export { default as AimdEditorTopBar } from './AimdEditorTopBar.vue'
4
+ export { default as AimdSourceEditor } from './AimdSourceEditor.vue'
5
+ export { default as AimdWysiwygEditor } from './AimdWysiwygEditor.vue'
6
+ export { default as AimdFieldDialog } from './AimdFieldDialog.vue'
7
+ export { useEditorContent } from './useEditorContent'
8
+ export {
9
+ createAimdEditorMessages,
10
+ DEFAULT_AIMD_EDITOR_LOCALE,
11
+ resolveAimdEditorLocale,
12
+ } from './locales'
13
+ export {
14
+ AIMD_FIELD_TYPE_DEFINITIONS,
15
+ AIMD_FIELD_TYPES,
16
+ MD_TOOLBAR_ITEM_DEFINITIONS,
17
+ MD_TOOLBAR_ITEMS,
18
+ createAimdFieldTypes,
19
+ createAimdVarTypePresets,
20
+ createMdToolbarItems,
21
+ getDefaultAimdFields,
22
+ buildAimdSyntax,
23
+ getQuickAimdSyntax,
24
+ } from './types'
25
+ export type {
26
+ AimdFieldTypeDefinition,
27
+ AimdFieldType,
28
+ AimdVarTypePresetOption,
29
+ MdToolbarItemDefinition,
30
+ MdToolbarItem,
31
+ AimdEditorProps,
32
+ AimdEditorEmits,
33
+ } from './types'
34
+ export type {
35
+ AimdEditorLocale,
36
+ AimdEditorMessages,
37
+ AimdEditorMessagesInput,
38
+ } from './locales'
39
+ export {
40
+ aimdMilkdownPlugins,
41
+ aimdRemarkPlugin,
42
+ aimdFieldNode,
43
+ aimdFieldView,
44
+ aimdFieldInputRule,
45
+ } from './milkdown-aimd-plugin'