@airalogy/aimd-renderer 2.5.0 → 2.7.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.
@@ -21,6 +21,7 @@ import { resolveQuizPreviewOptions } from "./quiz-preview"
21
21
  import { remarkInsertVisibleAssigners, remarkStripAssignerCodeBlocks } from "./assignerVisibility"
22
22
  import { highlightVisibleAssigners } from "./assignerHighlighting"
23
23
  import { annotateStepReferenceSequence } from "./annotateStepReferences"
24
+ import { criticMarkupHandlers } from "./criticMarkup"
24
25
  import { buildFigureChildren, assignFigureSequenceNumbers } from "./figureNumbering"
25
26
 
26
27
  // ---------------------------------------------------------------------------
@@ -511,7 +512,21 @@ import remarkParse from "remark-parse"
511
512
  import remarkRehype from "remark-rehype"
512
513
  import { unified } from "unified"
513
514
 
514
- import { protectAimdInlineTemplates, remarkAimd } from "@airalogy/aimd-core/parser"
515
+ import {
516
+ CRITIC_MARKUP_SUBSTITUTIONS_DATA_KEY,
517
+ protectAimdInlineTemplates,
518
+ protectCriticMarkupSubstitutions,
519
+ remarkAimd,
520
+ remarkCriticMarkup,
521
+ } from "@airalogy/aimd-core/parser"
522
+ import {
523
+ formatAimdExampleValue,
524
+ formatAimdExamples,
525
+ getAimdFieldDescription,
526
+ getAimdFieldDisplayLabel,
527
+ getAimdFieldExamples,
528
+ getAimdFieldTitle,
529
+ } from "@airalogy/aimd-core/utils"
515
530
  import {
516
531
  createAimdRendererMessages,
517
532
  getAimdRendererQuizTypeLabel,
@@ -559,14 +574,16 @@ async function ensureMathStylesLoaded(mathEnabled: boolean | undefined): Promise
559
574
 
560
575
  function createAimdParseInput(content: string) {
561
576
  const { content: protectedContent, templates } = protectAimdInlineTemplates(content)
577
+ const { content: criticProtectedContent, substitutions } = protectCriticMarkupSubstitutions(protectedContent)
562
578
  const file: VFile = {
563
579
  data: {
564
580
  aimdInlineTemplates: templates,
581
+ [CRITIC_MARKUP_SUBSTITUTIONS_DATA_KEY]: substitutions,
565
582
  },
566
583
  } as unknown as VFile
567
584
 
568
585
  return {
569
- content: protectedContent,
586
+ content: criticProtectedContent,
570
587
  file,
571
588
  }
572
589
  }
@@ -790,6 +807,121 @@ function buildScaleBandChildren(quizNode: AimdQuizNode): Array<Element | HastTex
790
807
  } as Element]
791
808
  }
792
809
 
810
+ function createTextNode(value: string): HastText {
811
+ return { type: "text", value }
812
+ }
813
+
814
+ interface FieldMetadataHelp {
815
+ tooltip: string
816
+ description?: string
817
+ examples: string[]
818
+ }
819
+
820
+ function getFieldHelpText(definition: { kwargs?: Record<string, unknown> } | undefined): FieldMetadataHelp {
821
+ const description = getAimdFieldDescription(definition)
822
+ const examples = getAimdFieldExamples(definition)
823
+ .map(formatAimdExampleValue)
824
+ .map(example => example.trim())
825
+ .filter(Boolean)
826
+ const exampleText = examples.length > 0 ? `e.g. ${examples.join(", ")}` : undefined
827
+ const tooltipLines = [description, exampleText].filter((value): value is string => Boolean(value))
828
+
829
+ return {
830
+ tooltip: tooltipLines.join("\n"),
831
+ description,
832
+ examples,
833
+ }
834
+ }
835
+
836
+ function createFieldMetadataPopover(help: FieldMetadataHelp): Element | null {
837
+ if (!help.description && help.examples.length === 0) {
838
+ return null
839
+ }
840
+ const children: Array<Element | HastText> = []
841
+ if (help.description) {
842
+ children.push({
843
+ type: "element",
844
+ tagName: "span",
845
+ properties: { className: ["aimd-field__metadata-popover-line"] },
846
+ children: [createTextNode(help.description)],
847
+ } as Element)
848
+ }
849
+ if (help.examples.length > 0) {
850
+ children.push({
851
+ type: "element",
852
+ tagName: "span",
853
+ properties: { className: ["aimd-field__metadata-examples"] },
854
+ children: [
855
+ {
856
+ type: "element",
857
+ tagName: "span",
858
+ properties: { className: ["aimd-field__metadata-examples-label"] },
859
+ children: [createTextNode("e.g.")],
860
+ } as Element,
861
+ ...help.examples.map((example) => ({
862
+ type: "element",
863
+ tagName: "span",
864
+ properties: { className: ["aimd-field__metadata-example"] },
865
+ children: [createTextNode(example)],
866
+ } as Element)),
867
+ ],
868
+ } as Element)
869
+ }
870
+ return {
871
+ type: "element",
872
+ tagName: "span",
873
+ properties: { className: ["aimd-field__metadata-popover"], role: "tooltip" },
874
+ children,
875
+ } as Element
876
+ }
877
+
878
+ function createFieldNameElement(id: string, definition: { kwargs?: Record<string, unknown> } | undefined): Element {
879
+ const displayTitle = getAimdFieldDisplayLabel(id, definition)
880
+ const hasCustomTitle = getAimdFieldTitle(definition) !== undefined && displayTitle !== id
881
+ const help = getFieldHelpText(definition)
882
+ const hasHelp = Boolean(help.description) || help.examples.length > 0
883
+ const className = ["aimd-field__name"]
884
+ if (hasCustomTitle || hasHelp) {
885
+ className.push("aimd-field__name--with-metadata")
886
+ }
887
+ if (hasHelp) {
888
+ className.push("aimd-field__metadata-host")
889
+ }
890
+ const children: Array<Element | HastText> = [
891
+ {
892
+ type: "element",
893
+ tagName: "span",
894
+ properties: { className: ["aimd-field__title"] },
895
+ children: [createTextNode(displayTitle)],
896
+ } as Element,
897
+ ]
898
+
899
+ if (hasCustomTitle) {
900
+ children.push({
901
+ type: "element",
902
+ tagName: "span",
903
+ properties: { className: ["aimd-field__key"] },
904
+ children: [createTextNode(id)],
905
+ } as Element)
906
+ }
907
+
908
+ const popover = createFieldMetadataPopover(help)
909
+ if (popover) {
910
+ children.push(popover)
911
+ }
912
+
913
+ return {
914
+ type: "element",
915
+ tagName: "span",
916
+ properties: cleanProperties({
917
+ className,
918
+ tabIndex: hasHelp ? 0 : undefined,
919
+ "aria-label": help.tooltip || undefined,
920
+ }),
921
+ children,
922
+ } as Element
923
+ }
924
+
793
925
  // ---------------------------------------------------------------------------
794
926
  // AIMD handler (remark-rehype custom handler)
795
927
  // ---------------------------------------------------------------------------
@@ -968,23 +1100,18 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
968
1100
  children: [{ type: "text", value: `[${refs.join(", ")}]` }],
969
1101
  } as Element)
970
1102
  }
971
- else if (fieldType === "var") {
972
- // Variable: type label + id + optional type annotation
973
- const definition = "definition" in node ? node.definition : undefined
974
- children.push(
975
- {
976
- type: "element",
977
- tagName: "span",
978
- properties: { className: ["aimd-field__scope"] },
979
- children: [{ type: "text", value: getAimdRendererScopeLabel("var", messages) }],
980
- } as Element,
1103
+ else if (fieldType === "var") {
1104
+ // Variable: type label + id + optional type annotation
1105
+ const definition = "definition" in node ? node.definition : undefined
1106
+ children.push(
981
1107
  {
982
1108
  type: "element",
983
1109
  tagName: "span",
984
- properties: { className: ["aimd-field__name"] },
985
- children: [{ type: "text", value: id }],
986
- } as Element,
987
- )
1110
+ properties: { className: ["aimd-field__scope"] },
1111
+ children: [{ type: "text", value: getAimdRendererScopeLabel("var", messages) }],
1112
+ } as Element,
1113
+ createFieldNameElement(id, definition),
1114
+ )
988
1115
  if (definition?.type) {
989
1116
  children.push({
990
1117
  type: "element",
@@ -994,11 +1121,13 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
994
1121
  } as Element)
995
1122
  }
996
1123
  }
997
- else if (fieldType === "var_table") {
998
- // var_table: render header + table preview
999
- const columns = "columns" in node ? (node as any).columns as string[] : []
1000
- children.push(
1001
- {
1124
+ else if (fieldType === "var_table") {
1125
+ // var_table: render header + table preview
1126
+ const columns = "columns" in node ? (node as any).columns as string[] : []
1127
+ const definition = "definition" in node ? node.definition : undefined
1128
+ const subvarDefs = definition?.subvars
1129
+ children.push(
1130
+ {
1002
1131
  type: "element",
1003
1132
  tagName: "div",
1004
1133
  properties: { className: ["aimd-field__header"] },
@@ -1006,17 +1135,12 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1006
1135
  {
1007
1136
  type: "element",
1008
1137
  tagName: "span",
1009
- properties: { className: ["aimd-field__scope"] },
1010
- children: [{ type: "text", value: getAimdRendererScopeLabel("var_table", messages) }],
1011
- } as Element,
1012
- {
1013
- type: "element",
1014
- tagName: "span",
1015
- properties: { className: ["aimd-field__name"] },
1016
- children: [{ type: "text", value: id }],
1017
- } as Element,
1018
- ],
1019
- } as Element,
1138
+ properties: { className: ["aimd-field__scope"] },
1139
+ children: [{ type: "text", value: getAimdRendererScopeLabel("var_table", messages) }],
1140
+ } as Element,
1141
+ createFieldNameElement(id, definition),
1142
+ ],
1143
+ } as Element,
1020
1144
  )
1021
1145
  if (columns && columns.length > 0) {
1022
1146
  children.push({
@@ -1033,12 +1157,14 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1033
1157
  type: "element",
1034
1158
  tagName: "tr",
1035
1159
  properties: {},
1036
- children: columns.map(col => ({
1037
- type: "element",
1038
- tagName: "th",
1039
- properties: {},
1040
- children: [{ type: "text", value: col }],
1041
- } as Element)),
1160
+ children: columns.map(col => ({
1161
+ type: "element",
1162
+ tagName: "th",
1163
+ properties: cleanProperties({
1164
+ "data-column-id": col,
1165
+ }),
1166
+ children: [createFieldNameElement(col, subvarDefs?.[col])],
1167
+ } as Element)),
1042
1168
  } as Element,
1043
1169
  ],
1044
1170
  } as Element,
@@ -1242,14 +1368,29 @@ function createAimdHandler(options: AimdRendererOptions = {}) {
1242
1368
  }
1243
1369
 
1244
1370
  // Build properties
1245
- const properties: Properties = {
1246
- "className": [baseClass, modifierClass],
1247
- "data-aimd-type": node.fieldType,
1371
+ const properties: Properties = {
1372
+ "className": [baseClass, modifierClass],
1373
+ "data-aimd-type": node.fieldType,
1248
1374
  "data-aimd-id": node.id,
1249
1375
  "data-aimd-scope": node.scope,
1250
1376
  "data-aimd-raw": node.raw,
1251
- "data-aimd-json": aimdJson,
1252
- }
1377
+ "data-aimd-json": aimdJson,
1378
+ }
1379
+
1380
+ if ((node.fieldType === "var" || node.fieldType === "var_table") && "definition" in node) {
1381
+ const title = getAimdFieldTitle(node.definition)
1382
+ const description = getAimdFieldDescription(node.definition)
1383
+ const examples = formatAimdExamples(getAimdFieldExamples(node.definition))
1384
+ if (title) {
1385
+ properties["data-aimd-title"] = title
1386
+ }
1387
+ if (description) {
1388
+ properties["data-aimd-description"] = description
1389
+ }
1390
+ if (examples) {
1391
+ properties["data-aimd-examples"] = examples
1392
+ }
1393
+ }
1253
1394
 
1254
1395
  // Add reference href
1255
1396
  if (isRef) {
@@ -1380,6 +1521,7 @@ function createBaseProcessor(options: AimdRendererOptions = {}) {
1380
1521
  // to properly parse multiline AIMD syntax like var_table with subvars
1381
1522
  processor.use(remarkAimd)
1382
1523
  processor.use(remarkStripAssignerCodeBlocks)
1524
+ processor.use(remarkCriticMarkup)
1383
1525
 
1384
1526
  // Single line break to <br> conversion (default enabled for AIMD)
1385
1527
  if (breaks) {
@@ -1402,6 +1544,7 @@ export function createHtmlProcessor(options: AimdRendererOptions = {}) {
1402
1544
  handlers: {
1403
1545
  // Custom handler for AIMD nodes
1404
1546
  aimd: aimdHandler,
1547
+ ...criticMarkupHandlers,
1405
1548
  },
1406
1549
  } as any)
1407
1550
  .use(rehypeRaw)
@@ -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
package/src/index.ts CHANGED
@@ -41,6 +41,24 @@ export {
41
41
  } from './common/unified-token-renderer'
42
42
 
43
43
  // Vue renderer exports
44
+ export {
45
+ AIMD_RECORD_RENDER_SCOPES,
46
+ createReadonlyRecordAimdRenderers,
47
+ createReadonlyRecordElementRenderers,
48
+ createReadonlyRecordRenderContext,
49
+ normalizeRecordRenderValue,
50
+ renderReadonlyRecordToVue,
51
+ type AimdRecordRenderScope,
52
+ type AimdRecordRenderValue,
53
+ type ReadonlyRecordAsset,
54
+ type ReadonlyRecordAssetKind,
55
+ type ReadonlyRecordAssetResolveContext,
56
+ type ReadonlyRecordAssetResolver,
57
+ type ReadonlyRecordMarkdownRenderOptions,
58
+ type ReadonlyRecordRenderContextInput,
59
+ type ReadonlyRecordVueRendererOptions,
60
+ } from './vue/readonly-record-renderer'
61
+
44
62
  export {
45
63
  type AimdComponentRenderer,
46
64
  type AimdRendererContext,
@@ -51,9 +69,12 @@ export {
51
69
  createEmbeddedRenderer,
52
70
  createMermaidRenderer,
53
71
  createStepCardRenderer,
72
+ loadShikiHighlighter,
73
+ type CodeBlockRendererOptions,
54
74
  type ElementRenderer,
55
75
  hastToVue,
56
76
  renderToVNodes,
77
+ type LoadShikiHighlighterOptions,
57
78
  type AimdStepCardRendererOptions,
58
79
  type ShikiHighlighter,
59
80
  type VueRendererOptions,