@airalogy/aimd-renderer 2.4.1 → 2.6.0

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 (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +10 -5
  3. package/README.zh-CN.md +10 -5
  4. package/dist/__tests__/renderer.test.d.ts +2 -0
  5. package/dist/__tests__/renderer.test.d.ts.map +1 -0
  6. package/dist/aimd-renderer.css +1 -1
  7. package/dist/common/annotateStepReferences.d.ts +10 -0
  8. package/dist/common/annotateStepReferences.d.ts.map +1 -0
  9. package/dist/common/assignerHighlighting.d.ts +14 -0
  10. package/dist/common/assignerHighlighting.d.ts.map +1 -0
  11. package/dist/common/assignerVisibility.d.ts +33 -0
  12. package/dist/common/assignerVisibility.d.ts.map +1 -0
  13. package/dist/common/eventKeys.d.ts +20 -0
  14. package/dist/common/eventKeys.d.ts.map +1 -0
  15. package/dist/common/figureNumbering.d.ts +30 -0
  16. package/dist/common/figureNumbering.d.ts.map +1 -0
  17. package/dist/common/processor.d.ts +96 -0
  18. package/dist/common/processor.d.ts.map +1 -0
  19. package/dist/common/quiz-preview.d.ts +11 -0
  20. package/dist/common/quiz-preview.d.ts.map +1 -0
  21. package/dist/common/unified-token-renderer.d.ts +124 -0
  22. package/dist/common/unified-token-renderer.d.ts.map +1 -0
  23. package/dist/html/index.d.ts +9 -0
  24. package/dist/html/index.d.ts.map +1 -0
  25. package/dist/html.js +1 -1
  26. package/dist/index.d.ts +23 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +270 -225
  29. package/dist/locales.d.ts +52 -0
  30. package/dist/locales.d.ts.map +1 -0
  31. package/dist/{processor-Cv8E7QsA.js → processor-CHbNEcN8.js} +2977 -2212
  32. package/dist/vue/index.d.ts +10 -0
  33. package/dist/vue/index.d.ts.map +1 -0
  34. package/dist/vue/vue-renderer.d.ts +159 -0
  35. package/dist/vue/vue-renderer.d.ts.map +1 -0
  36. package/dist/vue.js +10 -9
  37. package/package.json +17 -17
  38. package/src/__tests__/renderer.test.ts +220 -2
  39. package/src/common/processor.ts +177 -43
  40. package/src/common/unified-token-renderer.ts +106 -26
  41. package/src/index.ts +3 -0
  42. package/src/locales.ts +5 -0
  43. package/src/styles/katex.css +109 -0
  44. package/src/vue/index.ts +3 -0
  45. package/src/vue/vue-renderer.ts +320 -50
@@ -512,6 +512,14 @@ import remarkRehype from "remark-rehype"
512
512
  import { unified } from "unified"
513
513
 
514
514
  import { protectAimdInlineTemplates, remarkAimd } from "@airalogy/aimd-core/parser"
515
+ import {
516
+ formatAimdExampleValue,
517
+ formatAimdExamples,
518
+ getAimdFieldDescription,
519
+ getAimdFieldDisplayLabel,
520
+ getAimdFieldExamples,
521
+ getAimdFieldTitle,
522
+ } from "@airalogy/aimd-core/utils"
515
523
  import {
516
524
  createAimdRendererMessages,
517
525
  getAimdRendererQuizTypeLabel,
@@ -531,6 +539,7 @@ let mathStylesLoadPromise: Promise<unknown> | null = null
531
539
 
532
540
  const EMPTY_EXTRACTED_FIELDS: ExtractedAimdFields = {
533
541
  var: [],
542
+ var_definitions: [],
534
543
  var_table: [],
535
544
  client_assigner: [],
536
545
  quiz: [],
@@ -573,6 +582,7 @@ function createAimdParseInput(content: string) {
573
582
  function createEmptyExtractedFields(): ExtractedAimdFields {
574
583
  return {
575
584
  var: [...EMPTY_EXTRACTED_FIELDS.var],
585
+ var_definitions: [...(EMPTY_EXTRACTED_FIELDS.var_definitions || [])],
576
586
  var_table: [...EMPTY_EXTRACTED_FIELDS.var_table],
577
587
  client_assigner: [...EMPTY_EXTRACTED_FIELDS.client_assigner],
578
588
  quiz: [...EMPTY_EXTRACTED_FIELDS.quiz],
@@ -788,6 +798,121 @@ function buildScaleBandChildren(quizNode: AimdQuizNode): Array<Element | HastTex
788
798
  } as Element]
789
799
  }
790
800
 
801
+ function createTextNode(value: string): HastText {
802
+ return { type: "text", value }
803
+ }
804
+
805
+ interface FieldMetadataHelp {
806
+ tooltip: string
807
+ description?: string
808
+ examples: string[]
809
+ }
810
+
811
+ function getFieldHelpText(definition: { kwargs?: Record<string, unknown> } | undefined): FieldMetadataHelp {
812
+ const description = getAimdFieldDescription(definition)
813
+ const examples = getAimdFieldExamples(definition)
814
+ .map(formatAimdExampleValue)
815
+ .map(example => example.trim())
816
+ .filter(Boolean)
817
+ const exampleText = examples.length > 0 ? `e.g. ${examples.join(", ")}` : undefined
818
+ const tooltipLines = [description, exampleText].filter((value): value is string => Boolean(value))
819
+
820
+ return {
821
+ tooltip: tooltipLines.join("\n"),
822
+ description,
823
+ examples,
824
+ }
825
+ }
826
+
827
+ function createFieldMetadataPopover(help: FieldMetadataHelp): Element | null {
828
+ if (!help.description && help.examples.length === 0) {
829
+ return null
830
+ }
831
+ const children: Array<Element | HastText> = []
832
+ if (help.description) {
833
+ children.push({
834
+ type: "element",
835
+ tagName: "span",
836
+ properties: { className: ["aimd-field__metadata-popover-line"] },
837
+ children: [createTextNode(help.description)],
838
+ } as Element)
839
+ }
840
+ if (help.examples.length > 0) {
841
+ children.push({
842
+ type: "element",
843
+ tagName: "span",
844
+ properties: { className: ["aimd-field__metadata-examples"] },
845
+ children: [
846
+ {
847
+ type: "element",
848
+ tagName: "span",
849
+ properties: { className: ["aimd-field__metadata-examples-label"] },
850
+ children: [createTextNode("e.g.")],
851
+ } as Element,
852
+ ...help.examples.map((example) => ({
853
+ type: "element",
854
+ tagName: "span",
855
+ properties: { className: ["aimd-field__metadata-example"] },
856
+ children: [createTextNode(example)],
857
+ } as Element)),
858
+ ],
859
+ } as Element)
860
+ }
861
+ return {
862
+ type: "element",
863
+ tagName: "span",
864
+ properties: { className: ["aimd-field__metadata-popover"], role: "tooltip" },
865
+ children,
866
+ } as Element
867
+ }
868
+
869
+ function createFieldNameElement(id: string, definition: { kwargs?: Record<string, unknown> } | undefined): Element {
870
+ const displayTitle = getAimdFieldDisplayLabel(id, definition)
871
+ const hasCustomTitle = getAimdFieldTitle(definition) !== undefined && displayTitle !== id
872
+ const help = getFieldHelpText(definition)
873
+ const hasHelp = Boolean(help.description) || help.examples.length > 0
874
+ const className = ["aimd-field__name"]
875
+ if (hasCustomTitle || hasHelp) {
876
+ className.push("aimd-field__name--with-metadata")
877
+ }
878
+ if (hasHelp) {
879
+ className.push("aimd-field__metadata-host")
880
+ }
881
+ const children: Array<Element | HastText> = [
882
+ {
883
+ type: "element",
884
+ tagName: "span",
885
+ properties: { className: ["aimd-field__title"] },
886
+ children: [createTextNode(displayTitle)],
887
+ } as Element,
888
+ ]
889
+
890
+ if (hasCustomTitle) {
891
+ children.push({
892
+ type: "element",
893
+ tagName: "span",
894
+ properties: { className: ["aimd-field__key"] },
895
+ children: [createTextNode(id)],
896
+ } as Element)
897
+ }
898
+
899
+ const popover = createFieldMetadataPopover(help)
900
+ if (popover) {
901
+ children.push(popover)
902
+ }
903
+
904
+ return {
905
+ type: "element",
906
+ tagName: "span",
907
+ properties: cleanProperties({
908
+ className,
909
+ tabIndex: hasHelp ? 0 : undefined,
910
+ "aria-label": help.tooltip || undefined,
911
+ }),
912
+ children,
913
+ } as Element
914
+ }
915
+
791
916
  // ---------------------------------------------------------------------------
792
917
  // AIMD handler (remark-rehype custom handler)
793
918
  // ---------------------------------------------------------------------------
@@ -966,23 +1091,18 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
966
1091
  children: [{ type: "text", value: `[${refs.join(", ")}]` }],
967
1092
  } as Element)
968
1093
  }
969
- else if (fieldType === "var") {
970
- // Variable: type label + id + optional type annotation
971
- const definition = "definition" in node ? node.definition : undefined
972
- children.push(
1094
+ else if (fieldType === "var") {
1095
+ // Variable: type label + id + optional type annotation
1096
+ const definition = "definition" in node ? node.definition : undefined
1097
+ children.push(
973
1098
  {
974
1099
  type: "element",
975
1100
  tagName: "span",
976
- properties: { className: ["aimd-field__scope"] },
977
- children: [{ type: "text", value: getAimdRendererScopeLabel("var", messages) }],
978
- } as Element,
979
- {
980
- type: "element",
981
- tagName: "span",
982
- properties: { className: ["aimd-field__name"] },
983
- children: [{ type: "text", value: id }],
984
- } as Element,
985
- )
1101
+ properties: { className: ["aimd-field__scope"] },
1102
+ children: [{ type: "text", value: getAimdRendererScopeLabel("var", messages) }],
1103
+ } as Element,
1104
+ createFieldNameElement(id, definition),
1105
+ )
986
1106
  if (definition?.type) {
987
1107
  children.push({
988
1108
  type: "element",
@@ -992,11 +1112,13 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
992
1112
  } as Element)
993
1113
  }
994
1114
  }
995
- else if (fieldType === "var_table") {
996
- // var_table: render header + table preview
997
- const columns = "columns" in node ? (node as any).columns as string[] : []
998
- children.push(
999
- {
1115
+ else if (fieldType === "var_table") {
1116
+ // var_table: render header + table preview
1117
+ const columns = "columns" in node ? (node as any).columns as string[] : []
1118
+ const definition = "definition" in node ? node.definition : undefined
1119
+ const subvarDefs = definition?.subvars
1120
+ children.push(
1121
+ {
1000
1122
  type: "element",
1001
1123
  tagName: "div",
1002
1124
  properties: { className: ["aimd-field__header"] },
@@ -1004,17 +1126,12 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1004
1126
  {
1005
1127
  type: "element",
1006
1128
  tagName: "span",
1007
- properties: { className: ["aimd-field__scope"] },
1008
- children: [{ type: "text", value: getAimdRendererScopeLabel("var_table", messages) }],
1009
- } as Element,
1010
- {
1011
- type: "element",
1012
- tagName: "span",
1013
- properties: { className: ["aimd-field__name"] },
1014
- children: [{ type: "text", value: id }],
1015
- } as Element,
1016
- ],
1017
- } as Element,
1129
+ properties: { className: ["aimd-field__scope"] },
1130
+ children: [{ type: "text", value: getAimdRendererScopeLabel("var_table", messages) }],
1131
+ } as Element,
1132
+ createFieldNameElement(id, definition),
1133
+ ],
1134
+ } as Element,
1018
1135
  )
1019
1136
  if (columns && columns.length > 0) {
1020
1137
  children.push({
@@ -1031,12 +1148,14 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1031
1148
  type: "element",
1032
1149
  tagName: "tr",
1033
1150
  properties: {},
1034
- children: columns.map(col => ({
1035
- type: "element",
1036
- tagName: "th",
1037
- properties: {},
1038
- children: [{ type: "text", value: col }],
1039
- } as Element)),
1151
+ children: columns.map(col => ({
1152
+ type: "element",
1153
+ tagName: "th",
1154
+ properties: cleanProperties({
1155
+ "data-column-id": col,
1156
+ }),
1157
+ children: [createFieldNameElement(col, subvarDefs?.[col])],
1158
+ } as Element)),
1040
1159
  } as Element,
1041
1160
  ],
1042
1161
  } as Element,
@@ -1171,7 +1290,7 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1171
1290
  } as Element)
1172
1291
  }
1173
1292
 
1174
- if (quizType === "choice" && Array.isArray(quizNode.options) && quizNode.options.length > 0) {
1293
+ if ((quizType === "choice" || quizType === "true_false") && Array.isArray(quizNode.options) && quizNode.options.length > 0) {
1175
1294
  children.push({
1176
1295
  type: "element",
1177
1296
  tagName: "ul",
@@ -1190,7 +1309,7 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1190
1309
  children.push(...buildScaleBandChildren(quizNode))
1191
1310
  }
1192
1311
 
1193
- if (quizPreview.showAnswers && quizType === "choice" && quizNode.answer !== undefined) {
1312
+ if (quizPreview.showAnswers && (quizType === "choice" || quizType === "true_false") && quizNode.answer !== undefined) {
1194
1313
  const answerText = Array.isArray(quizNode.answer)
1195
1314
  ? quizNode.answer.join(", ")
1196
1315
  : String(quizNode.answer)
@@ -1240,14 +1359,29 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1240
1359
  }
1241
1360
 
1242
1361
  // Build properties
1243
- const properties: Properties = {
1244
- "className": [baseClass, modifierClass],
1245
- "data-aimd-type": node.fieldType,
1362
+ const properties: Properties = {
1363
+ "className": [baseClass, modifierClass],
1364
+ "data-aimd-type": node.fieldType,
1246
1365
  "data-aimd-id": node.id,
1247
1366
  "data-aimd-scope": node.scope,
1248
1367
  "data-aimd-raw": node.raw,
1249
- "data-aimd-json": aimdJson,
1250
- }
1368
+ "data-aimd-json": aimdJson,
1369
+ }
1370
+
1371
+ if ((node.fieldType === "var" || node.fieldType === "var_table") && "definition" in node) {
1372
+ const title = getAimdFieldTitle(node.definition)
1373
+ const description = getAimdFieldDescription(node.definition)
1374
+ const examples = formatAimdExamples(getAimdFieldExamples(node.definition))
1375
+ if (title) {
1376
+ properties["data-aimd-title"] = title
1377
+ }
1378
+ if (description) {
1379
+ properties["data-aimd-description"] = description
1380
+ }
1381
+ if (examples) {
1382
+ properties["data-aimd-examples"] = examples
1383
+ }
1384
+ }
1251
1385
 
1252
1386
  // Add reference href
1253
1387
  if (isRef) {
@@ -13,6 +13,13 @@ import type {
13
13
  } from "@airalogy/aimd-core/types"
14
14
  import type { AimdNode, QuizPreviewOptions, RenderContext } from "@airalogy/aimd-core/types"
15
15
  import type { ExtractedAimdFields } from "@airalogy/aimd-core/types"
16
+ import {
17
+ formatAimdExampleValue,
18
+ getAimdFieldDescription,
19
+ getAimdFieldDisplayLabel,
20
+ getAimdFieldExamples,
21
+ getAimdFieldTitle,
22
+ } from "@airalogy/aimd-core/utils"
16
23
  import type { AimdRendererI18nOptions, AimdRendererMessages } from "../locales"
17
24
  import type { AimdComponentRenderer, ElementRenderer, ShikiHighlighter, VueRendererOptions } from "../vue/vue-renderer"
18
25
  import type { AimdRendererOptions, RenderResult } from "./processor"
@@ -226,6 +233,74 @@ function buildScaleBandChildren(quizNode: AimdQuizNode): VNode[] {
226
233
  ]
227
234
  }
228
235
 
236
+ interface FieldMetadataHelp {
237
+ tooltip: string
238
+ description?: string
239
+ examples: string[]
240
+ }
241
+
242
+ function getFieldHelpText(definition: { kwargs?: Record<string, unknown> } | undefined): FieldMetadataHelp {
243
+ const description = getAimdFieldDescription(definition)
244
+ const examples = getAimdFieldExamples(definition)
245
+ .map(formatAimdExampleValue)
246
+ .map(example => example.trim())
247
+ .filter(Boolean)
248
+ const exampleText = examples.length > 0 ? `e.g. ${examples.join(", ")}` : undefined
249
+ const tooltipLines = [description, exampleText].filter((value): value is string => Boolean(value))
250
+
251
+ return {
252
+ tooltip: tooltipLines.join("\n"),
253
+ description,
254
+ examples,
255
+ }
256
+ }
257
+
258
+ function renderFieldMetadataPopover(help: FieldMetadataHelp): VNode | null {
259
+ if (!help.description && help.examples.length === 0) {
260
+ return null
261
+ }
262
+ const children: VNode[] = []
263
+ if (help.description) {
264
+ children.push(h("span", {
265
+ class: "aimd-field__metadata-popover-line",
266
+ }, help.description))
267
+ }
268
+ if (help.examples.length > 0) {
269
+ children.push(h("span", { class: "aimd-field__metadata-examples" }, [
270
+ h("span", { class: "aimd-field__metadata-examples-label" }, "e.g."),
271
+ ...help.examples.map((example, index) => h("span", {
272
+ key: `${index}-${example}`,
273
+ class: "aimd-field__metadata-example",
274
+ }, example)),
275
+ ]))
276
+ }
277
+ return h("span", {
278
+ class: "aimd-field__metadata-popover",
279
+ role: "tooltip",
280
+ }, children)
281
+ }
282
+
283
+ function renderFieldName(id: string, definition: { kwargs?: Record<string, unknown> } | undefined): VNode {
284
+ const displayTitle = getAimdFieldDisplayLabel(id, definition)
285
+ const hasCustomTitle = getAimdFieldTitle(definition) !== undefined && displayTitle !== id
286
+ const help = getFieldHelpText(definition)
287
+ const hasHelp = Boolean(help.description) || help.examples.length > 0
288
+
289
+ return h("span", {
290
+ class: [
291
+ "aimd-field__name",
292
+ (hasCustomTitle || hasHelp) ? "aimd-field__name--with-metadata" : undefined,
293
+ hasHelp ? "aimd-field__metadata-host" : undefined,
294
+ ],
295
+ tabindex: hasHelp ? 0 : undefined,
296
+ "aria-label": help.tooltip || undefined,
297
+ }, [
298
+ h("span", { class: "aimd-field__title" }, displayTitle),
299
+ hasCustomTitle ? h("span", { class: "aimd-field__key" }, id) : null,
300
+ renderFieldMetadataPopover(help),
301
+ ])
302
+ }
303
+
229
304
  /**
230
305
  * Render preview tag for AIMD field
231
306
  */
@@ -234,6 +309,7 @@ function renderPreviewTag(
234
309
  id: string,
235
310
  messages: AimdRendererMessages,
236
311
  columns?: string[],
312
+ definition?: { kwargs?: Record<string, unknown>, subvars?: Record<string, { kwargs?: Record<string, unknown> }> },
237
313
  ): VNode {
238
314
  const scopeKey = getScopeKey(scope)
239
315
  const scopeLabel = getAimdRendererScopeLabel(scope, messages)
@@ -241,17 +317,19 @@ function renderPreviewTag(
241
317
  // var_table: render tag with table preview inside
242
318
  if (scope === "var_table") {
243
319
  const children: VNode[] = [
244
- h("div", { class: "aimd-field__header" }, [
245
- h("span", { class: "aimd-field__scope" }, messages.scope.table),
246
- h("span", { class: "aimd-field__name" }, id),
247
- ]),
320
+ h("div", { class: "aimd-field__header" }, [
321
+ h("span", { class: "aimd-field__scope" }, messages.scope.table),
322
+ renderFieldName(id, definition),
323
+ ]),
248
324
  ]
249
325
  // Add table preview inside the container
250
326
  if (columns && columns.length > 0) {
251
327
  children.push(
252
328
  h("table", { class: "aimd-field__table-preview" }, [
253
329
  h("thead", [
254
- h("tr", columns.map(col => h("th", col))),
330
+ h("tr", columns.map(col => h("th", {
331
+ "data-column-id": col,
332
+ }, [renderFieldName(col, definition?.subvars?.[col])]))),
255
333
  ]),
256
334
  h("tbody", [
257
335
  h("tr", columns.map(() => h("td", "..."))),
@@ -272,10 +350,10 @@ function renderPreviewTag(
272
350
  "class": `aimd-field aimd-field--${classSuffix}`,
273
351
  "data-aimd-type": scopeKey,
274
352
  "data-aimd-id": id,
275
- }, [
276
- h("span", { class: "aimd-field__scope" }, scopeLabel),
277
- h("span", { class: "aimd-field__name" }, id),
278
- ])
353
+ }, [
354
+ h("span", { class: "aimd-field__scope" }, scopeLabel),
355
+ renderFieldName(id, definition),
356
+ ])
279
357
  }
280
358
 
281
359
  /**
@@ -299,19 +377,20 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
299
377
  resolveQuizPreviewOptions(getMode(), options.quizPreview)
300
378
 
301
379
  return {
302
- var: async (node, ctx, children) => {
303
- const varNode = node as AimdVarNode
304
- const { id, scope } = varNode
380
+ var: async (node, ctx, children) => {
381
+ const varNode = node as AimdVarNode
382
+ const { id, scope } = varNode
383
+ const definition = varNode.definition
305
384
 
306
- if (isPreview()) {
385
+ if (isPreview()) {
307
386
  if (PreviewRenderer) {
308
387
  return h(PreviewRenderer, { type: "var" }, {
309
388
  default: () => children,
310
389
  name: () => id,
311
390
  })
312
391
  }
313
- return renderPreviewTag(scope, id, messages)
314
- }
392
+ return renderPreviewTag(scope, id, messages, undefined, definition)
393
+ }
315
394
 
316
395
  // Edit mode
317
396
  if (getTokenProps && AIMDItem) {
@@ -325,12 +404,13 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
325
404
  }
326
405
  }
327
406
 
328
- return renderPreviewTag(scope, id, messages)
329
- },
407
+ return renderPreviewTag(scope, id, messages, undefined, definition)
408
+ },
330
409
 
331
- var_table: async (node, ctx, children) => {
332
- const tableNode = node as AimdVarTableNode
333
- const { id, scope, columns } = tableNode
410
+ var_table: async (node, ctx, children) => {
411
+ const tableNode = node as AimdVarTableNode
412
+ const { id, scope, columns } = tableNode
413
+ const definition = tableNode.definition
334
414
 
335
415
  if (isPreview()) {
336
416
  if (PreviewRenderer) {
@@ -340,8 +420,8 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
340
420
  })
341
421
  }
342
422
  // Preview mode: render inline tag with columns info
343
- return renderPreviewTag(scope, id, messages, columns)
344
- }
423
+ return renderPreviewTag(scope, id, messages, columns, definition)
424
+ }
345
425
 
346
426
  // Edit mode
347
427
  if (getTokenProps && AIMDTag) {
@@ -349,8 +429,8 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
349
429
  return h(AIMDTag, { ...item, props: columns })
350
430
  }
351
431
 
352
- return renderPreviewTag(scope, id, messages, columns)
353
- },
432
+ return renderPreviewTag(scope, id, messages, columns, definition)
433
+ },
354
434
 
355
435
  quiz: async (node, ctx, children) => {
356
436
  const quizNode = node as AimdQuizNode
@@ -381,7 +461,7 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
381
461
  previewChildren.push(h("div", { class: "aimd-quiz__description" }, quizNode.description))
382
462
  }
383
463
 
384
- if (quizType === "choice" && Array.isArray(quizNode.options) && quizNode.options.length > 0) {
464
+ if ((quizType === "choice" || quizType === "true_false") && Array.isArray(quizNode.options) && quizNode.options.length > 0) {
385
465
  previewChildren.push(
386
466
  h("ul", { class: "aimd-quiz__options" }, quizNode.options.map(opt =>
387
467
  h("li", `${opt.key}. ${opt.text}`),
@@ -396,7 +476,7 @@ function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<strin
396
476
 
397
477
  const quizPreview = getQuizPreview()
398
478
 
399
- if (quizPreview.showAnswers && quizType === "choice" && quizNode.answer !== undefined) {
479
+ if (quizPreview.showAnswers && (quizType === "choice" || quizType === "true_false") && quizNode.answer !== undefined) {
400
480
  const answerText = Array.isArray(quizNode.answer)
401
481
  ? quizNode.answer.join(", ")
402
482
  : String(quizNode.answer)
package/src/index.ts CHANGED
@@ -51,9 +51,12 @@ export {
51
51
  createEmbeddedRenderer,
52
52
  createMermaidRenderer,
53
53
  createStepCardRenderer,
54
+ loadShikiHighlighter,
55
+ type CodeBlockRendererOptions,
54
56
  type ElementRenderer,
55
57
  hastToVue,
56
58
  renderToVNodes,
59
+ type LoadShikiHighlighterOptions,
57
60
  type AimdStepCardRendererOptions,
58
61
  type ShikiHighlighter,
59
62
  type VueRendererOptions,
package/src/locales.ts CHANGED
@@ -26,6 +26,7 @@ export interface AimdRendererMessages {
26
26
  choice: string
27
27
  singleChoice: string
28
28
  multipleChoice: string
29
+ trueFalse: string
29
30
  blank: string
30
31
  open: string
31
32
  scale: string
@@ -86,6 +87,7 @@ const EN_US_MESSAGES: AimdRendererMessages = {
86
87
  choice: "choice",
87
88
  singleChoice: "Single choice",
88
89
  multipleChoice: "Multiple choice",
90
+ trueFalse: "True/false",
89
91
  blank: "blank",
90
92
  open: "open",
91
93
  scale: "scale",
@@ -122,6 +124,7 @@ const ZH_CN_MESSAGES: AimdRendererMessages = {
122
124
  choice: "选择",
123
125
  singleChoice: "单选",
124
126
  multipleChoice: "多选",
127
+ trueFalse: "判断",
125
128
  blank: "填空",
126
129
  open: "开放",
127
130
  scale: "量表",
@@ -246,6 +249,8 @@ export function getAimdRendererQuizTypeLabel(
246
249
  return messages.quiz.types.choice
247
250
  case "blank":
248
251
  return messages.quiz.types.blank
252
+ case "true_false":
253
+ return messages.quiz.types.trueFalse
249
254
  case "open":
250
255
  return messages.quiz.types.open
251
256
  case "scale":
@@ -1,2 +1,111 @@
1
1
  /* Re-export KaTeX base styles for AIMD renderer consumers. */
2
2
  @import "katex/dist/katex.min.css";
3
+
4
+ .aimd-field__metadata-host {
5
+ position: relative;
6
+ cursor: help;
7
+ outline: none;
8
+ }
9
+
10
+ .aimd-field__metadata-host:hover,
11
+ .aimd-field__metadata-host:focus,
12
+ .aimd-field__metadata-host:focus-within {
13
+ z-index: 90;
14
+ }
15
+
16
+ .aimd-field__metadata-host:focus-visible {
17
+ border-radius: 4px;
18
+ box-shadow: 0 0 0 2px rgba(65, 129, 253, 0.24);
19
+ }
20
+
21
+ .aimd-field__metadata-host .aimd-field__title {
22
+ text-decoration-line: underline;
23
+ text-decoration-style: dotted;
24
+ text-decoration-color: rgba(15, 23, 42, 0.36);
25
+ text-underline-offset: 3px;
26
+ }
27
+
28
+ .aimd-field__metadata-popover {
29
+ position: absolute;
30
+ z-index: 80;
31
+ inset-inline-start: 0;
32
+ top: calc(100% + 7px);
33
+ display: inline-flex;
34
+ flex-direction: column;
35
+ gap: 4px;
36
+ width: max-content;
37
+ min-width: 220px;
38
+ max-width: min(360px, 82vw);
39
+ padding: 8px 10px;
40
+ border-radius: 8px;
41
+ background: rgba(15, 23, 42, 0.96);
42
+ color: #f8fafc;
43
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.28);
44
+ font-size: 12px;
45
+ font-weight: 500;
46
+ line-height: 1.45;
47
+ letter-spacing: 0;
48
+ text-align: left;
49
+ text-transform: none;
50
+ white-space: normal;
51
+ pointer-events: none;
52
+ opacity: 0;
53
+ visibility: hidden;
54
+ transform: translateY(-2px);
55
+ transition:
56
+ opacity 120ms ease,
57
+ transform 120ms ease,
58
+ visibility 120ms ease;
59
+ }
60
+
61
+ .aimd-field__metadata-popover::before {
62
+ content: "";
63
+ position: absolute;
64
+ inset-inline-start: 14px;
65
+ top: -5px;
66
+ width: 10px;
67
+ height: 10px;
68
+ background: rgba(15, 23, 42, 0.96);
69
+ transform: rotate(45deg);
70
+ }
71
+
72
+ .aimd-field__metadata-popover-line {
73
+ display: block;
74
+ color: inherit;
75
+ }
76
+
77
+ .aimd-field__metadata-examples {
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ align-items: center;
81
+ gap: 4px;
82
+ padding-top: 1px;
83
+ }
84
+
85
+ .aimd-field__metadata-examples-label {
86
+ color: rgba(226, 232, 240, 0.78);
87
+ }
88
+
89
+ .aimd-field__metadata-example {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ max-width: 100%;
93
+ padding: 1px 6px;
94
+ border: 1px solid rgba(248, 250, 252, 0.18);
95
+ border-radius: 5px;
96
+ background: rgba(248, 250, 252, 0.1);
97
+ color: #f8fafc;
98
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
99
+ font-size: 11px;
100
+ font-weight: 600;
101
+ line-height: 1.45;
102
+ overflow-wrap: anywhere;
103
+ }
104
+
105
+ .aimd-field__metadata-host:hover > .aimd-field__metadata-popover,
106
+ .aimd-field__metadata-host:focus > .aimd-field__metadata-popover,
107
+ .aimd-field__metadata-host:focus-within > .aimd-field__metadata-popover {
108
+ opacity: 1;
109
+ visibility: visible;
110
+ transform: translateY(0);
111
+ }
package/src/vue/index.ts CHANGED
@@ -12,9 +12,12 @@ export {
12
12
  createEmbeddedRenderer,
13
13
  createMermaidRenderer,
14
14
  createStepCardRenderer,
15
+ loadShikiHighlighter,
16
+ type CodeBlockRendererOptions,
15
17
  type ElementRenderer,
16
18
  hastToVue,
17
19
  renderToVNodes,
20
+ type LoadShikiHighlighterOptions,
18
21
  type AimdStepCardRendererOptions,
19
22
  type ShikiHighlighter,
20
23
  type VueRendererOptions,