@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,449 @@
1
+ import type { Editor } from '@milkdown/kit/core'
2
+ import {
3
+ createAimdEditorMessages,
4
+ DEFAULT_AIMD_EDITOR_LOCALE,
5
+ type AimdEditorLocale,
6
+ type AimdEditorMessages,
7
+ type AimdEditorMessagesInput,
8
+ } from './locales'
9
+
10
+ export interface AimdFieldTypeDefinition {
11
+ type: string
12
+ icon: string
13
+ svgIcon: string
14
+ color: string
15
+ }
16
+
17
+ export interface AimdFieldType extends AimdFieldTypeDefinition {
18
+ label: string
19
+ desc: string
20
+ }
21
+
22
+ export interface AimdVarTypePresetOption {
23
+ key: string
24
+ value: string
25
+ label: string
26
+ desc: string
27
+ }
28
+
29
+ export interface MdToolbarItemDefinition {
30
+ action: string
31
+ style?: string
32
+ svgIcon?: string
33
+ }
34
+
35
+ export interface MdToolbarItem extends MdToolbarItemDefinition {
36
+ title?: string
37
+ }
38
+
39
+ export interface AimdEditorProps {
40
+ /** Initial / bound markdown content (v-model) */
41
+ modelValue?: string
42
+ /** Built-in UI locale */
43
+ locale?: AimdEditorLocale | string
44
+ /** Optional overrides for built-in UI copy */
45
+ messages?: AimdEditorMessagesInput
46
+ /** Initial editor mode */
47
+ mode?: 'source' | 'wysiwyg'
48
+ /** Theme name for Monaco */
49
+ theme?: string
50
+ /** Whether to show the top toolbar (mode switch + theme toggle) */
51
+ showTopBar?: boolean
52
+ /** Whether to show the formatting toolbar */
53
+ showToolbar?: boolean
54
+ /** Whether to show the AIMD toolbar section */
55
+ showAimdToolbar?: boolean
56
+ /** Whether to show the Markdown toolbar section */
57
+ showMdToolbar?: boolean
58
+ /** Whether to enable the Milkdown block handle (plus button on left) */
59
+ enableBlockHandle?: boolean
60
+ /** Whether to enable the slash menu (type / to insert) */
61
+ enableSlashMenu?: boolean
62
+ /** Whether inactive source / WYSIWYG panes stay mounted in the DOM */
63
+ keepInactiveEditorsMounted?: boolean
64
+ /** Minimum height of the editor area in px */
65
+ minHeight?: number
66
+ /** Whether the editor is read-only */
67
+ readonly?: boolean
68
+ /** Monaco editor options override */
69
+ monacoOptions?: Record<string, any>
70
+ /** Additional var type presets shown in the insertion dialog */
71
+ varTypePlugins?: AimdVarTypePresetOption[]
72
+ }
73
+
74
+ export interface AimdEditorEmits {
75
+ (e: 'update:modelValue', value: string): void
76
+ (e: 'update:mode', mode: 'source' | 'wysiwyg'): void
77
+ (e: 'ready', editor: { monaco?: any; milkdown?: Editor }): void
78
+ }
79
+
80
+ // SVG icon helpers – all 16×16, stroke-based, currentColor
81
+ const _si = (d: string, extra = '') => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${extra}>${d}</svg>`
82
+
83
+ const DEFAULT_EDITOR_MESSAGES = createAimdEditorMessages(DEFAULT_AIMD_EDITOR_LOCALE)
84
+
85
+ export const AIMD_FIELD_TYPE_DEFINITIONS: AimdFieldTypeDefinition[] = [
86
+ { type: 'var', icon: 'x', svgIcon: _si('<path d="M7 4l10 16M17 4L7 20"/>'), color: '#2563eb' },
87
+ { type: 'var_table', icon: '\u229e', svgIcon: _si('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>'), color: '#059669' },
88
+ { type: 'quiz', icon: '?', svgIcon: _si('<circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 0 1 5 0c0 1.5-2 2-2 3.5"/><circle cx="12" cy="17" r="1" fill="currentColor" stroke="none"/>'), color: '#7c3aed' },
89
+ { type: 'step', icon: '\u25b6', svgIcon: _si('<polygon points="5,3 19,12 5,21" fill="currentColor" stroke="none"/>'), color: '#d97706' },
90
+ { type: 'check', icon: '\u2713', svgIcon: _si('<polyline points="4 12 9 17 20 6"/>'), color: '#dc2626' },
91
+ { type: 'ref_step', icon: '\u2197', svgIcon: _si('<path d="M7 17L17 7M17 7H8M17 7v9"/>'), color: '#0891b2' },
92
+ { type: 'ref_var', icon: '\u2197', svgIcon: _si('<circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/>'), color: '#0891b2' },
93
+ { type: 'ref_fig', icon: '\u2197', svgIcon: _si('<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none"/><path d="M21 15l-5-5L5 21"/>'), color: '#0891b2' },
94
+ { type: 'cite', icon: '\ud83d\udcd6', svgIcon: _si('<path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/>'), color: '#6d28d9' },
95
+ ]
96
+
97
+ export const MD_TOOLBAR_ITEM_DEFINITIONS: MdToolbarItemDefinition[] = [
98
+ { action: 'h1', svgIcon: _si('<path d="M4 12h8M4 4v16M12 4v16"/><text x="16.5" y="14" font-size="10" fill="currentColor" stroke="none" font-weight="600">1</text>') },
99
+ { action: 'h2', svgIcon: _si('<path d="M4 12h8M4 4v16M12 4v16"/><path d="M16.5 8.5a2.5 2.5 0 015 0c0 2-5 4-5 6.5h5" stroke-width="1.8"/>') },
100
+ { action: 'h3', svgIcon: _si('<path d="M4 12h8M4 4v16M12 4v16"/><path d="M16.5 8a2 2 0 014 0 2 2 0 01-2.5 2 2 2 0 012.5 2 2 2 0 01-4 0" stroke-width="1.8"/>') },
101
+ { action: 'bold', svgIcon: _si('<path d="M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6z"/><path d="M6 12h9a4 4 0 014 4 4 4 0 01-4 4H6z"/>') },
102
+ { action: 'italic', svgIcon: _si('<line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/>') },
103
+ { action: 'strikethrough', svgIcon: _si('<path d="M16 4c-.5-1.5-2.2-3-5-3-3 0-5 2-5 4.5 0 2 1.5 3.5 5 4.5"/><path d="M3 12h18"/><path d="M8 20c.5 1.5 2.2 3 5 3 3 0 5-2 5-4.5 0-2-1.5-3.5-5-4.5"/>') },
104
+ { action: 'sep1' },
105
+ { action: 'ul', svgIcon: _si('<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"/>') },
106
+ { action: 'ol', svgIcon: _si('<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>') },
107
+ { action: 'blockquote', svgIcon: _si('<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"/>') },
108
+ { action: 'code', svgIcon: _si('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>') },
109
+ { action: 'codeblock', svgIcon: _si('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/><rect x="1" y="1" width="22" height="22" rx="3" stroke-dasharray="4 2" stroke-width="1"/>') },
110
+ { action: 'sep2' },
111
+ { action: 'link', svgIcon: _si('<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/>') },
112
+ { action: 'image', svgIcon: _si('<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" stroke="none"/><path d="M21 15l-5-5L5 21"/>') },
113
+ { action: 'table', svgIcon: _si('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>') },
114
+ { action: 'hr', svgIcon: _si('<line x1="2" y1="12" x2="22" y2="12" stroke-width="2.5"/>') },
115
+ { action: 'math', svgIcon: _si('<path d="M18 4H6l6 8-6 8h12" stroke-width="2"/>') },
116
+ ]
117
+
118
+ export function createAimdFieldTypes(
119
+ messages: Pick<AimdEditorMessages, 'fieldTypes'> = DEFAULT_EDITOR_MESSAGES,
120
+ ): AimdFieldType[] {
121
+ return AIMD_FIELD_TYPE_DEFINITIONS.map((fieldType) => {
122
+ const localized = messages.fieldTypes[fieldType.type as keyof typeof messages.fieldTypes]
123
+ return {
124
+ ...fieldType,
125
+ label: localized?.label || fieldType.type,
126
+ desc: localized?.desc || '',
127
+ }
128
+ })
129
+ }
130
+
131
+ function isMdToolbarSeparator(item: MdToolbarItemDefinition): boolean {
132
+ return item.action.startsWith('sep')
133
+ }
134
+
135
+ export function createMdToolbarItems(
136
+ messages: Pick<AimdEditorMessages, 'mdToolbar'> = DEFAULT_EDITOR_MESSAGES,
137
+ ): MdToolbarItem[] {
138
+ return MD_TOOLBAR_ITEM_DEFINITIONS.map((item) => {
139
+ if (isMdToolbarSeparator(item)) return item
140
+ const title = messages.mdToolbar[item.action as keyof typeof messages.mdToolbar]
141
+ return {
142
+ ...item,
143
+ title: title || item.action,
144
+ }
145
+ })
146
+ }
147
+
148
+ export function createAimdVarTypePresets(
149
+ messages: Pick<AimdEditorMessages, 'varTypePresets'> = DEFAULT_EDITOR_MESSAGES,
150
+ customPresets: AimdVarTypePresetOption[] = [],
151
+ ): AimdVarTypePresetOption[] {
152
+ const defaults: AimdVarTypePresetOption[] = [
153
+ { key: 'str', value: 'str', ...messages.varTypePresets.str },
154
+ { key: 'int', value: 'int', ...messages.varTypePresets.int },
155
+ { key: 'float', value: 'float', ...messages.varTypePresets.float },
156
+ { key: 'bool', value: 'bool', ...messages.varTypePresets.bool },
157
+ { key: 'date', value: 'date', ...messages.varTypePresets.date },
158
+ { key: 'datetime', value: 'datetime', ...messages.varTypePresets.datetime },
159
+ { key: 'time', value: 'time', ...messages.varTypePresets.time },
160
+ { key: 'codeStr', value: 'CodeStr', ...messages.varTypePresets.codeStr },
161
+ { key: 'pyStr', value: 'PyStr', ...messages.varTypePresets.pyStr },
162
+ { key: 'jsStr', value: 'JsStr', ...messages.varTypePresets.jsStr },
163
+ { key: 'tsStr', value: 'TsStr', ...messages.varTypePresets.tsStr },
164
+ { key: 'jsonStr', value: 'JsonStr', ...messages.varTypePresets.jsonStr },
165
+ { key: 'tomlStr', value: 'TomlStr', ...messages.varTypePresets.tomlStr },
166
+ { key: 'yamlStr', value: 'YamlStr', ...messages.varTypePresets.yamlStr },
167
+ { key: 'dnaSequence', value: 'DNASequence', ...messages.varTypePresets.dnaSequence },
168
+ { key: 'currentTime', value: 'CurrentTime', ...messages.varTypePresets.currentTime },
169
+ { key: 'userName', value: 'UserName', ...messages.varTypePresets.userName },
170
+ { key: 'airalogyMarkdown', value: 'AiralogyMarkdown', ...messages.varTypePresets.airalogyMarkdown },
171
+ ]
172
+
173
+ const indexByValue = new Map<string, number>()
174
+ const merged = defaults.map((preset, index) => {
175
+ indexByValue.set(normalizeVarTypePresetValue(preset.value), index)
176
+ return preset
177
+ })
178
+
179
+ for (const preset of customPresets) {
180
+ const normalized = normalizeVarTypePresetValue(preset.value)
181
+ const existingIndex = indexByValue.get(normalized)
182
+ if (typeof existingIndex === 'number') {
183
+ merged[existingIndex] = preset
184
+ continue
185
+ }
186
+
187
+ indexByValue.set(normalized, merged.length)
188
+ merged.push(preset)
189
+ }
190
+
191
+ return merged
192
+ }
193
+
194
+ function normalizeVarTypePresetValue(value: string): string {
195
+ return value.trim().toLowerCase().replace(/[\s_-]/g, '')
196
+ }
197
+
198
+ // Backwards-compatible English defaults. Prefer the factory helpers above.
199
+ /**
200
+ * @deprecated Use `AIMD_FIELD_TYPE_DEFINITIONS` with `createAimdFieldTypes(messages)` instead.
201
+ */
202
+ export const AIMD_FIELD_TYPES: AimdFieldType[] = createAimdFieldTypes(DEFAULT_EDITOR_MESSAGES)
203
+ /**
204
+ * @deprecated Use `MD_TOOLBAR_ITEM_DEFINITIONS` with `createMdToolbarItems(messages)` instead.
205
+ */
206
+ export const MD_TOOLBAR_ITEMS: MdToolbarItem[] = createMdToolbarItems(DEFAULT_EDITOR_MESSAGES)
207
+
208
+ function toYamlScalar(value: string): string {
209
+ const trimmed = value.trim()
210
+ if (!trimmed)
211
+ return '""'
212
+
213
+ if (
214
+ /[:#\[\]\{\},&*!?|><=@`]/.test(trimmed)
215
+ || /^\s|\s$/.test(value)
216
+ || /["']/.test(trimmed)
217
+ ) {
218
+ return JSON.stringify(trimmed)
219
+ }
220
+
221
+ return trimmed
222
+ }
223
+
224
+ function toStemLines(value: string, fallback: string): string[] {
225
+ const stem = (value || fallback).replace(/\r\n?/g, '\n')
226
+ const lines = stem.split('\n')
227
+ if (lines.length === 0)
228
+ return [fallback]
229
+ return lines
230
+ }
231
+
232
+ function getDefaultOptionText(key: string, messages?: Pick<AimdEditorMessages, 'defaults'>): string {
233
+ return messages?.defaults.optionText(key) || DEFAULT_EDITOR_MESSAGES.defaults.optionText(key)
234
+ }
235
+
236
+ function parseQuizOptions(
237
+ input: string,
238
+ messages?: Pick<AimdEditorMessages, 'defaults'>,
239
+ ): Array<{ key: string, text: string }> {
240
+ const parts = input.split(',').map(s => s.trim()).filter(Boolean)
241
+ if (parts.length === 0) {
242
+ return [
243
+ { key: 'A', text: getDefaultOptionText('A', messages) },
244
+ { key: 'B', text: getDefaultOptionText('B', messages) },
245
+ ]
246
+ }
247
+
248
+ return parts.map((part, index) => {
249
+ const sepIndex = part.indexOf(':')
250
+ if (sepIndex > 0) {
251
+ const key = part.slice(0, sepIndex).trim() || String.fromCharCode(65 + index)
252
+ const text = part.slice(sepIndex + 1).trim() || getDefaultOptionText(key, messages)
253
+ return { key, text }
254
+ }
255
+
256
+ const key = String.fromCharCode(65 + index)
257
+ return { key, text: part }
258
+ })
259
+ }
260
+
261
+ function parseBlankItems(input: string): Array<{ key: string, answer: string }> {
262
+ const parts = input.split(',').map(s => s.trim()).filter(Boolean)
263
+ if (parts.length === 0) {
264
+ return [{ key: 'b1', answer: '21%' }]
265
+ }
266
+
267
+ return parts.map((part, index) => {
268
+ const sepIndex = part.indexOf(':')
269
+ if (sepIndex > 0) {
270
+ const key = part.slice(0, sepIndex).trim() || `b${index + 1}`
271
+ const answer = part.slice(sepIndex + 1).trim() || ''
272
+ return { key, answer }
273
+ }
274
+ return { key: `b${index + 1}`, answer: part }
275
+ })
276
+ }
277
+
278
+ function parseOptionalScore(value: string): string | null {
279
+ const trimmed = value.trim()
280
+ if (!trimmed)
281
+ return null
282
+ const score = Number(trimmed)
283
+ if (Number.isNaN(score) || score < 0)
284
+ return null
285
+ return String(score)
286
+ }
287
+
288
+ export function getDefaultAimdFields(
289
+ type: string,
290
+ messages?: Pick<AimdEditorMessages, 'defaults'>,
291
+ ): Record<string, string> {
292
+ switch (type) {
293
+ case 'var': return { name: '', type: 'str', default: '', title: '' }
294
+ case 'var_table': return { name: '', subvars: '' }
295
+ case 'quiz': return {
296
+ id: 'quiz_choice_1',
297
+ quizType: 'choice',
298
+ mode: 'single',
299
+ stem: messages?.defaults.questionStem || DEFAULT_EDITOR_MESSAGES.defaults.questionStem,
300
+ options: `A:${getDefaultOptionText('A', messages)}, B:${getDefaultOptionText('B', messages)}`,
301
+ answer: 'A',
302
+ blanks: 'b1:21%',
303
+ rubric: '',
304
+ score: '',
305
+ }
306
+ case 'step': return { name: '', level: '1' }
307
+ case 'check': return { name: '' }
308
+ case 'ref_step': return { name: '' }
309
+ case 'ref_var': return { name: '' }
310
+ case 'ref_fig': return { name: '' }
311
+ case 'cite': return { refs: '' }
312
+ default: return { name: '' }
313
+ }
314
+ }
315
+
316
+ export function buildAimdSyntax(
317
+ type: string,
318
+ fields: Record<string, string>,
319
+ messages?: Pick<AimdEditorMessages, 'defaults'>,
320
+ ): string {
321
+ switch (type) {
322
+ case 'var': {
323
+ let inner = (fields.name || '').trim() || 'my_var'
324
+ const varType = (fields.type || '').trim()
325
+ const title = (fields.title || '').trim()
326
+ if (varType) inner += ': ' + varType
327
+ if (fields.default) inner += ' = ' + fields.default
328
+ if (title) inner += ', title = "' + title + '"'
329
+ return `{{var|${inner}}}`
330
+ }
331
+ case 'var_table': {
332
+ const name = fields.name || 'my_table'
333
+ const subvars = fields.subvars ? fields.subvars.split(',').map(s => s.trim()).filter(Boolean) : ['col1', 'col2']
334
+ return `{{var_table|${name}, subvars=[${subvars.join(', ')}]}}`
335
+ }
336
+ case 'step': {
337
+ const name = fields.name || 'my_step'
338
+ const level = fields.level && fields.level !== '1' ? ', ' + fields.level : ''
339
+ return `{{step|${name}${level}}}`
340
+ }
341
+ case 'quiz': {
342
+ const quizType = (fields.quizType || 'choice').trim()
343
+ const id = (fields.id || `quiz_${quizType}_1`).trim()
344
+ const score = parseOptionalScore(fields.score || '')
345
+ const lines: string[] = [
346
+ '```quiz',
347
+ `id: ${toYamlScalar(id)}`,
348
+ `type: ${toYamlScalar(quizType)}`,
349
+ ]
350
+
351
+ if (score !== null) {
352
+ lines.push(`score: ${score}`)
353
+ }
354
+
355
+ lines.push('stem: |')
356
+ for (const stemLine of toStemLines(fields.stem, messages?.defaults.fillQuestionStem || DEFAULT_EDITOR_MESSAGES.defaults.fillQuestionStem)) {
357
+ lines.push(` ${stemLine}`)
358
+ }
359
+
360
+ if (quizType === 'choice') {
361
+ const mode = fields.mode === 'multiple' ? 'multiple' : 'single'
362
+ const options = parseQuizOptions(fields.options || '', messages)
363
+ lines.push(`mode: ${mode}`)
364
+ lines.push('options:')
365
+ for (const option of options) {
366
+ lines.push(` - key: ${toYamlScalar(option.key)}`)
367
+ lines.push(` text: ${toYamlScalar(option.text)}`)
368
+ }
369
+
370
+ const answerRaw = (fields.answer || '').trim()
371
+ if (answerRaw) {
372
+ if (mode === 'multiple') {
373
+ const answers = answerRaw.split(',').map(v => v.trim()).filter(Boolean)
374
+ if (answers.length > 0) {
375
+ lines.push('answer:')
376
+ for (const answer of answers) {
377
+ lines.push(` - ${toYamlScalar(answer)}`)
378
+ }
379
+ }
380
+ }
381
+ else {
382
+ lines.push(`answer: ${toYamlScalar(answerRaw)}`)
383
+ }
384
+ }
385
+ }
386
+ else if (quizType === 'blank') {
387
+ const blanks = parseBlankItems(fields.blanks || '')
388
+ lines.push('blanks:')
389
+ for (const blank of blanks) {
390
+ lines.push(` - key: ${toYamlScalar(blank.key)}`)
391
+ lines.push(` answer: ${toYamlScalar(blank.answer)}`)
392
+ }
393
+ }
394
+ else {
395
+ const rubric = (fields.rubric || '').trim()
396
+ if (rubric) {
397
+ lines.push(`rubric: ${toYamlScalar(rubric)}`)
398
+ }
399
+ }
400
+
401
+ lines.push('```')
402
+ return lines.join('\n')
403
+ }
404
+ case 'check':
405
+ return `{{check|${fields.name || 'my_check'}}}`
406
+ case 'ref_step':
407
+ return `{{ref_step|${fields.name || 'step_id'}}}`
408
+ case 'ref_var':
409
+ return `{{ref_var|${fields.name || 'var_id'}}}`
410
+ case 'ref_fig':
411
+ return `{{ref_fig|${fields.name || 'fig_id'}}}`
412
+ case 'cite':
413
+ return `{{cite|${fields.refs || 'ref1'}}}`
414
+ default:
415
+ return `{{${type}|${fields.name || 'id'}}}`
416
+ }
417
+ }
418
+
419
+ export function getQuickAimdSyntax(
420
+ type: string,
421
+ messages?: Pick<AimdEditorMessages, 'defaults'>,
422
+ ): string {
423
+ const defaults: Record<string, string> = {
424
+ var: '{{var|var_id: str}}',
425
+ var_table: '{{var_table|table_id, subvars=[col1, col2, col3]}}',
426
+ quiz: [
427
+ '```quiz',
428
+ 'id: quiz_choice_1',
429
+ 'type: choice',
430
+ 'mode: single',
431
+ 'stem: |',
432
+ ` ${messages?.defaults.questionStem || DEFAULT_EDITOR_MESSAGES.defaults.questionStem}`,
433
+ 'options:',
434
+ ' - key: A',
435
+ ` text: ${getDefaultOptionText('A', messages)}`,
436
+ ' - key: B',
437
+ ` text: ${getDefaultOptionText('B', messages)}`,
438
+ 'answer: A',
439
+ '```',
440
+ ].join('\n'),
441
+ step: '{{step|step_id}}',
442
+ check: '{{check|check_id}}',
443
+ ref_step: '{{ref_step|step_id}}',
444
+ ref_var: '{{ref_var|var_id}}',
445
+ ref_fig: '{{ref_fig|fig_id}}',
446
+ cite: '{{cite|ref1}}',
447
+ }
448
+ return defaults[type] || `{{${type}|id}}`
449
+ }