@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.
- package/README.md +59 -0
- package/README.zh-CN.md +43 -0
- package/dist/AimdEditorTopBar.vue_vue_type_script_setup_true_lang-gbfMDZSh.js +1131 -0
- package/dist/AimdSourceEditor.vue_vue_type_script_setup_true_lang-t_sUoXky.js +274 -0
- package/dist/AimdWysiwygEditor.vue_vue_type_script_setup_true_lang-B8o1VbUH.js +25012 -0
- package/dist/aimd-editor.css +1 -0
- package/dist/embedded.js +11 -0
- package/dist/index.js +44 -0
- package/dist/monaco.js +16 -0
- package/dist/theme-B8dCnOx-.js +583 -0
- package/dist/vue.js +30 -0
- package/dist/wysiwyg.js +9 -0
- package/package.json +90 -0
- package/src/__tests__/editor.test.ts +296 -0
- package/src/embedded.ts +18 -0
- package/src/index.ts +10 -0
- package/src/language-config.ts +152 -0
- package/src/monaco.ts +19 -0
- package/src/theme.ts +166 -0
- package/src/tokens.ts +120 -0
- package/src/vue/AimdEditor.vue +715 -0
- package/src/vue/AimdEditorToolbar.vue +83 -0
- package/src/vue/AimdEditorTopBar.vue +39 -0
- package/src/vue/AimdFieldDialog.vue +1102 -0
- package/src/vue/AimdSourceEditor.vue +330 -0
- package/src/vue/AimdWysiwygEditor.vue +569 -0
- package/src/vue/aimdInlineMarkdownNormalization.ts +10 -0
- package/src/vue/comparableAimdMarkdown.ts +6 -0
- package/src/vue/env.d.ts +7 -0
- package/src/vue/index.ts +45 -0
- package/src/vue/locales.ts +667 -0
- package/src/vue/milkdown-aimd-plugin.ts +378 -0
- package/src/vue/programmaticMarkdownSyncGuard.ts +66 -0
- package/src/vue/types.ts +449 -0
- package/src/vue/useEditorContent.ts +252 -0
- package/src/wysiwyg.ts +17 -0
package/src/vue/types.ts
ADDED
|
@@ -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
|
+
}
|