@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,667 @@
1
+ export type AimdEditorLocale = 'en-US' | 'zh-CN'
2
+
3
+ export const DEFAULT_AIMD_EDITOR_LOCALE: AimdEditorLocale = 'en-US'
4
+
5
+ type DeepPartial<T> = {
6
+ [K in keyof T]?: T[K] extends (...args: any[]) => any
7
+ ? T[K]
8
+ : T[K] extends Array<infer U>
9
+ ? Array<DeepPartial<U>>
10
+ : T[K] extends object
11
+ ? DeepPartial<T[K]>
12
+ : T[K]
13
+ }
14
+
15
+ export interface AimdEditorMessages {
16
+ mode: {
17
+ source: string
18
+ sourceTitle: string
19
+ wysiwyg: string
20
+ wysiwygTitle: string
21
+ }
22
+ common: {
23
+ loadingEditor: string
24
+ preview: string
25
+ cancel: string
26
+ insert: string
27
+ remove: string
28
+ none: string
29
+ available: string
30
+ }
31
+ fieldTypes: {
32
+ var: { label: string, desc: string }
33
+ var_table: { label: string, desc: string }
34
+ quiz: { label: string, desc: string }
35
+ step: { label: string, desc: string }
36
+ check: { label: string, desc: string }
37
+ ref_step: { label: string, desc: string }
38
+ ref_var: { label: string, desc: string }
39
+ ref_fig: { label: string, desc: string }
40
+ cite: { label: string, desc: string }
41
+ }
42
+ varTypePresets: {
43
+ str: { label: string, desc: string }
44
+ int: { label: string, desc: string }
45
+ float: { label: string, desc: string }
46
+ bool: { label: string, desc: string }
47
+ date: { label: string, desc: string }
48
+ datetime: { label: string, desc: string }
49
+ time: { label: string, desc: string }
50
+ codeStr: { label: string, desc: string }
51
+ pyStr: { label: string, desc: string }
52
+ jsStr: { label: string, desc: string }
53
+ tsStr: { label: string, desc: string }
54
+ jsonStr: { label: string, desc: string }
55
+ tomlStr: { label: string, desc: string }
56
+ yamlStr: { label: string, desc: string }
57
+ dnaSequence: { label: string, desc: string }
58
+ currentTime: { label: string, desc: string }
59
+ userName: { label: string, desc: string }
60
+ airalogyMarkdown: { label: string, desc: string }
61
+ }
62
+ mdToolbar: {
63
+ h1: string
64
+ h2: string
65
+ h3: string
66
+ bold: string
67
+ italic: string
68
+ strikethrough: string
69
+ ul: string
70
+ ol: string
71
+ blockquote: string
72
+ code: string
73
+ codeblock: string
74
+ link: string
75
+ image: string
76
+ table: string
77
+ hr: string
78
+ math: string
79
+ }
80
+ dialog: {
81
+ title: (label: string) => string
82
+ variableId: string
83
+ typePresetLabel: string
84
+ typeHint: string
85
+ customType: string
86
+ type: string
87
+ defaultValue: string
88
+ titleLabel: string
89
+ tableId: string
90
+ subVariableColumns: string
91
+ subVariableColumnsHint: string
92
+ stepId: string
93
+ level: string
94
+ level1: string
95
+ level2: string
96
+ level3: string
97
+ quizId: string
98
+ questionType: string
99
+ score: string
100
+ stem: string
101
+ blankStemHint: string
102
+ mode: string
103
+ options: string
104
+ dragToReorder: string
105
+ answer: string
106
+ correct: string
107
+ optionsHint: string
108
+ blanks: string
109
+ blanksHint: string
110
+ rubric: string
111
+ checkpointId: string
112
+ referencedStepId: string
113
+ referencedVariableId: string
114
+ referencedFigureId: string
115
+ citationId: string
116
+ citationHint: string
117
+ customTypeHint: string
118
+ }
119
+ placeholders: {
120
+ variableId: string
121
+ type: string
122
+ defaultValue: string
123
+ title: string
124
+ tableId: string
125
+ subVariableColumns: string
126
+ stepId: string
127
+ quizId: string
128
+ score: string
129
+ stem: string
130
+ optionKey: string
131
+ optionText: string
132
+ blankKey: string
133
+ blankAnswer: string
134
+ rubric: string
135
+ checkpointId: string
136
+ citationIds: string
137
+ }
138
+ quiz: {
139
+ types: {
140
+ choice: string
141
+ blank: string
142
+ open: string
143
+ }
144
+ modes: {
145
+ single: string
146
+ multiple: string
147
+ }
148
+ }
149
+ actions: {
150
+ addOption: string
151
+ addBlank: string
152
+ }
153
+ errors: {
154
+ blankQuizRequiresBlankKey: string
155
+ blankKeysMustBeUnique: (keys: string[]) => string
156
+ blankStemRequiresPlaceholders: string
157
+ duplicatePlaceholders: (keys: string[]) => string
158
+ undefinedPlaceholders: (keys: string[]) => string
159
+ missingPlaceholders: (keys: string[]) => string
160
+ }
161
+ defaults: {
162
+ questionStem: string
163
+ fillQuestionStem: string
164
+ optionText: (key: string) => string
165
+ }
166
+ snippets: {
167
+ heading: string
168
+ boldText: string
169
+ italicText: string
170
+ strikethrough: string
171
+ listItem: string
172
+ quote: string
173
+ code: string
174
+ codeBlock: string
175
+ linkText: string
176
+ altText: string
177
+ mathFormula: string
178
+ tableColumnA: string
179
+ tableColumnB: string
180
+ tableColumnC: string
181
+ }
182
+ blockMenu: {
183
+ addBlockTitle: string
184
+ placeholder: string
185
+ groups: {
186
+ text: string
187
+ list: string
188
+ advanced: string
189
+ aimd: string
190
+ }
191
+ items: {
192
+ text: string
193
+ heading1: string
194
+ heading2: string
195
+ heading3: string
196
+ quote: string
197
+ divider: string
198
+ bulletList: string
199
+ orderedList: string
200
+ codeBlock: string
201
+ table: string
202
+ }
203
+ }
204
+ completions: {
205
+ insertAimdField: (keyword: string) => string
206
+ quizBlockLabel: string
207
+ quizBlock: string
208
+ }
209
+ }
210
+
211
+ export type AimdEditorMessagesInput = DeepPartial<AimdEditorMessages>
212
+
213
+ function detectRuntimeLocale(): string | undefined {
214
+ if (typeof document !== 'undefined') {
215
+ const htmlLang = document.documentElement?.lang?.trim()
216
+ if (htmlLang) return htmlLang
217
+ }
218
+
219
+ if (typeof navigator !== 'undefined') {
220
+ if (navigator.language) return navigator.language
221
+ if (Array.isArray(navigator.languages) && navigator.languages.length > 0) {
222
+ return navigator.languages[0]
223
+ }
224
+ }
225
+
226
+ return undefined
227
+ }
228
+
229
+ const EN_US_MESSAGES: AimdEditorMessages = {
230
+ mode: {
231
+ source: 'Source',
232
+ sourceTitle: 'Source Mode',
233
+ wysiwyg: 'WYSIWYG',
234
+ wysiwygTitle: 'WYSIWYG Mode',
235
+ },
236
+ common: {
237
+ loadingEditor: 'Loading Editor...',
238
+ preview: 'Preview',
239
+ cancel: 'Cancel',
240
+ insert: 'Insert',
241
+ remove: 'Remove',
242
+ none: 'None',
243
+ available: 'Available',
244
+ },
245
+ fieldTypes: {
246
+ var: { label: 'Variable', desc: 'Define a variable' },
247
+ var_table: { label: 'Var Table', desc: 'Define a variable table' },
248
+ quiz: { label: 'Quiz', desc: 'Define a quiz item' },
249
+ step: { label: 'Step', desc: 'Define a step' },
250
+ check: { label: 'Checkpoint', desc: 'Define a checkpoint' },
251
+ ref_step: { label: 'Ref Step', desc: 'Reference a defined step' },
252
+ ref_var: { label: 'Ref Var', desc: 'Reference a defined variable' },
253
+ ref_fig: { label: 'Ref Fig', desc: 'Reference a defined figure' },
254
+ cite: { label: 'Citation', desc: 'Insert a citation' },
255
+ },
256
+ varTypePresets: {
257
+ str: { label: 'str', desc: 'Single-line text' },
258
+ int: { label: 'int', desc: 'Whole number' },
259
+ float: { label: 'float', desc: 'Decimal number' },
260
+ bool: { label: 'bool', desc: 'Yes / no checkbox' },
261
+ date: { label: 'date', desc: 'Calendar date picker' },
262
+ datetime: { label: 'datetime', desc: 'Date and time picker' },
263
+ time: { label: 'time', desc: 'Time-only picker' },
264
+ codeStr: { label: 'CodeStr', desc: 'Plain code editor without language-specific highlighting' },
265
+ pyStr: { label: 'PyStr', desc: 'Python code editor with syntax highlighting' },
266
+ jsStr: { label: 'JsStr', desc: 'JavaScript code editor with syntax highlighting' },
267
+ tsStr: { label: 'TsStr', desc: 'TypeScript code editor with syntax highlighting' },
268
+ jsonStr: { label: 'JsonStr', desc: 'JSON editor with syntax highlighting' },
269
+ tomlStr: { label: 'TomlStr', desc: 'TOML-like code editor with syntax highlighting' },
270
+ yamlStr: { label: 'YamlStr', desc: 'YAML editor with syntax highlighting' },
271
+ dnaSequence: { label: 'DNASequence', desc: 'Editable DNA sequence with annotations' },
272
+ currentTime: { label: 'CurrentTime', desc: 'Auto-fill the current time' },
273
+ userName: { label: 'UserName', desc: 'Auto-fill the current user name' },
274
+ airalogyMarkdown: { label: 'AiralogyMarkdown', desc: 'Multi-line markdown notes' },
275
+ },
276
+ mdToolbar: {
277
+ h1: 'Heading 1',
278
+ h2: 'Heading 2',
279
+ h3: 'Heading 3',
280
+ bold: 'Bold',
281
+ italic: 'Italic',
282
+ strikethrough: 'Strikethrough',
283
+ ul: 'Unordered List',
284
+ ol: 'Ordered List',
285
+ blockquote: 'Blockquote',
286
+ code: 'Inline Code',
287
+ codeblock: 'Code Block',
288
+ link: 'Link',
289
+ image: 'Image',
290
+ table: 'Table',
291
+ hr: 'Horizontal Rule',
292
+ math: 'Math Formula',
293
+ },
294
+ dialog: {
295
+ title: label => `Insert AIMD ${label}`,
296
+ variableId: 'Variable ID',
297
+ typePresetLabel: 'Common Types',
298
+ typeHint: 'Pick the closest match for the field you want to collect.',
299
+ customType: 'Custom / Advanced Type',
300
+ type: 'Type',
301
+ defaultValue: 'Default Value',
302
+ titleLabel: 'Title',
303
+ tableId: 'Table ID',
304
+ subVariableColumns: 'Sub-variable Columns',
305
+ subVariableColumnsHint: 'Comma-separated column names',
306
+ stepId: 'Step ID',
307
+ level: 'Level',
308
+ level1: '1 (Top level)',
309
+ level2: '2 (Sub-step)',
310
+ level3: '3 (Sub-sub-step)',
311
+ quizId: 'Quiz ID',
312
+ questionType: 'Question Type',
313
+ score: 'Score',
314
+ stem: 'Stem',
315
+ blankStemHint: 'Use placeholders in stem like [[b1]], [[b2]] and keep keys consistent with the blanks list.',
316
+ mode: 'Mode',
317
+ options: 'Options',
318
+ dragToReorder: 'Drag to reorder',
319
+ answer: 'Answer',
320
+ correct: 'Correct',
321
+ optionsHint: 'Use unique keys (A/B/C), then mark answer directly in each row.',
322
+ blanks: 'Blanks',
323
+ blanksHint: 'Use keys like b1, b2 and refer to them in stem as [[b1]], [[b2]].',
324
+ rubric: 'Rubric',
325
+ checkpointId: 'Checkpoint ID',
326
+ referencedStepId: 'Referenced Step ID',
327
+ referencedVariableId: 'Referenced Variable ID',
328
+ referencedFigureId: 'Referenced Figure ID',
329
+ citationId: 'Citation ID',
330
+ citationHint: 'Comma-separated citation IDs',
331
+ customTypeHint: 'Optional. Enter another AIMD type if you need something more specific, such as list or dict.',
332
+ },
333
+ placeholders: {
334
+ variableId: 'sample_id',
335
+ type: 'list / dict / CustomType',
336
+ defaultValue: 'Optional',
337
+ title: 'Display title (optional)',
338
+ tableId: 'table_id',
339
+ subVariableColumns: 'col1, col2, col3',
340
+ stepId: 'step_id',
341
+ quizId: 'quiz_choice_1',
342
+ score: 'Optional, e.g. 5',
343
+ stem: 'Question stem',
344
+ optionKey: 'A',
345
+ optionText: 'Option text',
346
+ blankKey: 'b1',
347
+ blankAnswer: 'Expected answer',
348
+ rubric: 'Optional rubric',
349
+ checkpointId: 'check_id',
350
+ citationIds: 'ref1, ref2',
351
+ },
352
+ quiz: {
353
+ types: {
354
+ choice: 'choice',
355
+ blank: 'blank',
356
+ open: 'open',
357
+ },
358
+ modes: {
359
+ single: 'single',
360
+ multiple: 'multiple',
361
+ },
362
+ },
363
+ actions: {
364
+ addOption: '+ Add option',
365
+ addBlank: '+ Add blank',
366
+ },
367
+ errors: {
368
+ blankQuizRequiresBlankKey: 'Blank quiz requires at least one non-empty blank key.',
369
+ blankKeysMustBeUnique: keys => `Blank keys must be unique: ${keys.join(', ')}`,
370
+ blankStemRequiresPlaceholders: 'Blank quiz stem must contain placeholders like [[b1]].',
371
+ duplicatePlaceholders: keys => `Stem contains duplicate placeholders: ${keys.join(', ')}`,
372
+ undefinedPlaceholders: keys => `Stem contains undefined placeholders: ${keys.join(', ')}`,
373
+ missingPlaceholders: keys => `Stem is missing placeholders for blank keys: ${keys.join(', ')}`,
374
+ },
375
+ defaults: {
376
+ questionStem: 'Which option is correct?',
377
+ fillQuestionStem: 'Please fill this question stem.',
378
+ optionText: key => `Option ${key}`,
379
+ },
380
+ snippets: {
381
+ heading: 'Heading',
382
+ boldText: 'bold text',
383
+ italicText: 'italic text',
384
+ strikethrough: 'strikethrough',
385
+ listItem: 'list item',
386
+ quote: 'quote',
387
+ code: 'code',
388
+ codeBlock: 'code block',
389
+ linkText: 'link text',
390
+ altText: 'alt text',
391
+ mathFormula: 'E = mc^2',
392
+ tableColumnA: 'Col A',
393
+ tableColumnB: 'Col B',
394
+ tableColumnC: 'Col C',
395
+ },
396
+ blockMenu: {
397
+ addBlockTitle: 'Click to add block',
398
+ placeholder: 'Please enter...',
399
+ groups: {
400
+ text: 'Text',
401
+ list: 'List',
402
+ advanced: 'Advanced',
403
+ aimd: 'AIMD',
404
+ },
405
+ items: {
406
+ text: 'Text',
407
+ heading1: 'Heading 1',
408
+ heading2: 'Heading 2',
409
+ heading3: 'Heading 3',
410
+ quote: 'Quote',
411
+ divider: 'Divider',
412
+ bulletList: 'Bullet List',
413
+ orderedList: 'Ordered List',
414
+ codeBlock: 'Code Block',
415
+ table: 'Table',
416
+ },
417
+ },
418
+ completions: {
419
+ insertAimdField: keyword => `Insert AIMD ${keyword} field`,
420
+ quizBlockLabel: 'quiz block',
421
+ quizBlock: 'Insert AIMD quiz code block',
422
+ },
423
+ }
424
+
425
+ const ZH_CN_MESSAGES: AimdEditorMessages = {
426
+ mode: {
427
+ source: '源码',
428
+ sourceTitle: '源码模式',
429
+ wysiwyg: '所见即所得',
430
+ wysiwygTitle: '所见即所得模式',
431
+ },
432
+ common: {
433
+ loadingEditor: '正在加载编辑器...',
434
+ preview: '预览',
435
+ cancel: '取消',
436
+ insert: '插入',
437
+ remove: '删除',
438
+ none: '无',
439
+ available: '可用项',
440
+ },
441
+ fieldTypes: {
442
+ var: { label: '变量', desc: '定义一个变量' },
443
+ var_table: { label: '变量表', desc: '定义一个变量表' },
444
+ quiz: { label: '题目', desc: '定义一个题目块' },
445
+ step: { label: '步骤', desc: '定义一个步骤' },
446
+ check: { label: '检查点', desc: '定义一个检查点' },
447
+ ref_step: { label: '引用步骤', desc: '引用已定义的步骤' },
448
+ ref_var: { label: '引用变量', desc: '引用已定义的变量' },
449
+ ref_fig: { label: '引用图片', desc: '引用已定义的图片' },
450
+ cite: { label: '引用文献', desc: '插入文献引用' },
451
+ },
452
+ varTypePresets: {
453
+ str: { label: 'str', desc: '单行文本' },
454
+ int: { label: 'int', desc: '整数' },
455
+ float: { label: 'float', desc: '小数' },
456
+ bool: { label: 'bool', desc: '是 / 否复选框' },
457
+ date: { label: 'date', desc: '日期选择器' },
458
+ datetime: { label: 'datetime', desc: '日期时间选择器' },
459
+ time: { label: 'time', desc: '时间选择器' },
460
+ codeStr: { label: 'CodeStr', desc: '通用代码编辑器,不指定语言高亮' },
461
+ pyStr: { label: 'PyStr', desc: '带 Python 语法高亮的代码编辑器' },
462
+ jsStr: { label: 'JsStr', desc: '带 JavaScript 语法高亮的代码编辑器' },
463
+ tsStr: { label: 'TsStr', desc: '带 TypeScript 语法高亮的代码编辑器' },
464
+ jsonStr: { label: 'JsonStr', desc: '带 JSON 语法高亮的编辑器' },
465
+ tomlStr: { label: 'TomlStr', desc: '带 TOML 风格语法高亮的编辑器' },
466
+ yamlStr: { label: 'YamlStr', desc: '带 YAML 语法高亮的编辑器' },
467
+ dnaSequence: { label: 'DNASequence', desc: '可编辑的 DNA 序列与注释' },
468
+ currentTime: { label: 'CurrentTime', desc: '自动填入当前时间' },
469
+ userName: { label: 'UserName', desc: '自动填入当前用户名' },
470
+ airalogyMarkdown: { label: 'AiralogyMarkdown', desc: '多行 Markdown 备注' },
471
+ },
472
+ mdToolbar: {
473
+ h1: '一级标题',
474
+ h2: '二级标题',
475
+ h3: '三级标题',
476
+ bold: '加粗',
477
+ italic: '斜体',
478
+ strikethrough: '删除线',
479
+ ul: '无序列表',
480
+ ol: '有序列表',
481
+ blockquote: '引用块',
482
+ code: '行内代码',
483
+ codeblock: '代码块',
484
+ link: '链接',
485
+ image: '图片',
486
+ table: '表格',
487
+ hr: '分割线',
488
+ math: '数学公式',
489
+ },
490
+ dialog: {
491
+ title: label => `插入 AIMD ${label}`,
492
+ variableId: '变量 ID',
493
+ typePresetLabel: '常用类型',
494
+ typeHint: '先选一个最接近的输入方式,后面也可以再手动改。',
495
+ customType: '自定义 / 高级类型',
496
+ type: '类型',
497
+ defaultValue: '默认值',
498
+ titleLabel: '标题',
499
+ tableId: '表格 ID',
500
+ subVariableColumns: '子变量列',
501
+ subVariableColumnsHint: '多个列名用逗号分隔',
502
+ stepId: '步骤 ID',
503
+ level: '层级',
504
+ level1: '1(顶层)',
505
+ level2: '2(子步骤)',
506
+ level3: '3(子子步骤)',
507
+ quizId: '题目 ID',
508
+ questionType: '题目类型',
509
+ score: '分值',
510
+ stem: '题干',
511
+ blankStemHint: '在题干中使用 [[b1]]、[[b2]] 这样的占位符,并与下方填空键保持一致。',
512
+ mode: '模式',
513
+ options: '选项',
514
+ dragToReorder: '拖拽排序',
515
+ answer: '答案',
516
+ correct: '正确',
517
+ optionsHint: '请使用唯一键(A/B/C),并直接在每一行标记答案。',
518
+ blanks: '填空项',
519
+ blanksHint: '请使用 b1、b2 这样的键,并在题干中写成 [[b1]]、[[b2]]。',
520
+ rubric: '评分说明',
521
+ checkpointId: '检查点 ID',
522
+ referencedStepId: '引用步骤 ID',
523
+ referencedVariableId: '引用变量 ID',
524
+ referencedFigureId: '引用图片 ID',
525
+ citationId: '文献 ID',
526
+ citationHint: '多个文献 ID 用逗号分隔',
527
+ customTypeHint: '可选。如果需要更具体的 AIMD 类型,可以在这里输入,例如 list 或 dict。',
528
+ },
529
+ placeholders: {
530
+ variableId: 'sample_id',
531
+ type: 'list / dict / CustomType',
532
+ defaultValue: '可选',
533
+ title: '显示标题(可选)',
534
+ tableId: 'table_id',
535
+ subVariableColumns: 'col1, col2, col3',
536
+ stepId: 'step_id',
537
+ quizId: 'quiz_choice_1',
538
+ score: '可选,例如 5',
539
+ stem: '请输入题干',
540
+ optionKey: 'A',
541
+ optionText: '选项内容',
542
+ blankKey: 'b1',
543
+ blankAnswer: '预期答案',
544
+ rubric: '可选的评分说明',
545
+ checkpointId: 'check_id',
546
+ citationIds: 'ref1, ref2',
547
+ },
548
+ quiz: {
549
+ types: {
550
+ choice: '选择题',
551
+ blank: '填空题',
552
+ open: '开放题',
553
+ },
554
+ modes: {
555
+ single: '单选',
556
+ multiple: '多选',
557
+ },
558
+ },
559
+ actions: {
560
+ addOption: '+ 添加选项',
561
+ addBlank: '+ 添加填空项',
562
+ },
563
+ errors: {
564
+ blankQuizRequiresBlankKey: '填空题至少需要一个非空的填空键。',
565
+ blankKeysMustBeUnique: keys => `填空键必须唯一:${keys.join(', ')}`,
566
+ blankStemRequiresPlaceholders: '填空题题干必须包含类似 [[b1]] 的占位符。',
567
+ duplicatePlaceholders: keys => `题干中存在重复占位符:${keys.join(', ')}`,
568
+ undefinedPlaceholders: keys => `题干中存在未定义的占位符:${keys.join(', ')}`,
569
+ missingPlaceholders: keys => `这些填空键未在题干中使用:${keys.join(', ')}`,
570
+ },
571
+ defaults: {
572
+ questionStem: '哪个选项是正确的?',
573
+ fillQuestionStem: '请填写题干内容。',
574
+ optionText: key => `选项 ${key}`,
575
+ },
576
+ snippets: {
577
+ heading: '标题',
578
+ boldText: '加粗文本',
579
+ italicText: '斜体文本',
580
+ strikethrough: '删除线文本',
581
+ listItem: '列表项',
582
+ quote: '引用内容',
583
+ code: '代码',
584
+ codeBlock: '代码块',
585
+ linkText: '链接文字',
586
+ altText: '图片说明',
587
+ mathFormula: 'E = mc^2',
588
+ tableColumnA: '列 A',
589
+ tableColumnB: '列 B',
590
+ tableColumnC: '列 C',
591
+ },
592
+ blockMenu: {
593
+ addBlockTitle: '点击添加块',
594
+ placeholder: '请输入内容...',
595
+ groups: {
596
+ text: '文本',
597
+ list: '列表',
598
+ advanced: '高级',
599
+ aimd: 'AIMD',
600
+ },
601
+ items: {
602
+ text: '正文',
603
+ heading1: '一级标题',
604
+ heading2: '二级标题',
605
+ heading3: '三级标题',
606
+ quote: '引用块',
607
+ divider: '分割线',
608
+ bulletList: '无序列表',
609
+ orderedList: '有序列表',
610
+ codeBlock: '代码块',
611
+ table: '表格',
612
+ },
613
+ },
614
+ completions: {
615
+ insertAimdField: keyword => `插入 AIMD ${keyword} 字段`,
616
+ quizBlockLabel: '题目代码块',
617
+ quizBlock: '插入 AIMD 题目代码块',
618
+ },
619
+ }
620
+
621
+ const BASE_MESSAGES: Record<AimdEditorLocale, AimdEditorMessages> = {
622
+ 'en-US': EN_US_MESSAGES,
623
+ 'zh-CN': ZH_CN_MESSAGES,
624
+ }
625
+
626
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
627
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
628
+ }
629
+
630
+ function deepMerge<T>(base: T, override?: DeepPartial<T>): T {
631
+ if (!override) return base
632
+
633
+ const result: Record<string, unknown> = { ...(base as Record<string, unknown>) }
634
+
635
+ for (const key of Object.keys(override) as Array<keyof T>) {
636
+ const overrideValue = override[key]
637
+ if (overrideValue === undefined) continue
638
+
639
+ const baseValue = base[key]
640
+ if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
641
+ result[key as string] = deepMerge(baseValue, overrideValue as any)
642
+ continue
643
+ }
644
+
645
+ result[key as string] = overrideValue as T[keyof T]
646
+ }
647
+
648
+ return result as T
649
+ }
650
+
651
+ export function resolveAimdEditorLocale(locale?: string): AimdEditorLocale {
652
+ const runtimeLocale = locale || detectRuntimeLocale()
653
+
654
+ if (runtimeLocale?.toLowerCase().startsWith('zh')) {
655
+ return 'zh-CN'
656
+ }
657
+
658
+ return DEFAULT_AIMD_EDITOR_LOCALE
659
+ }
660
+
661
+ export function createAimdEditorMessages(
662
+ locale: string | undefined,
663
+ overrides?: AimdEditorMessagesInput,
664
+ ): AimdEditorMessages {
665
+ const resolvedLocale = resolveAimdEditorLocale(locale)
666
+ return deepMerge(BASE_MESSAGES[resolvedLocale], overrides)
667
+ }