@airalogy/aimd-renderer 2.7.0 → 2.8.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.
@@ -1,6 +1,154 @@
1
1
  /* Re-export KaTeX base styles for AIMD renderer consumers. */
2
2
  @import "katex/dist/katex.min.css";
3
3
 
4
+ .aimd-figure {
5
+ display: block;
6
+ box-sizing: border-box;
7
+ width: fit-content;
8
+ max-width: min(100%, 920px);
9
+ margin: 1.2em auto 1.45em;
10
+ overflow: hidden;
11
+ border: 1px solid #d8e2ef;
12
+ border-radius: 10px;
13
+ background: #ffffff;
14
+ }
15
+
16
+ .aimd-figure > .aimd-figure__image {
17
+ display: block;
18
+ max-width: 100%;
19
+ height: auto;
20
+ margin: 0 auto;
21
+ border-radius: 0;
22
+ background: #f3f7fc;
23
+ object-fit: contain;
24
+ }
25
+
26
+ .aimd-figure__caption {
27
+ display: block;
28
+ margin: 0;
29
+ padding: 10px 14px 12px 16px;
30
+ border-top: 1px solid #d8e2ef;
31
+ background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
32
+ color: #253247;
33
+ line-height: 1.45;
34
+ }
35
+
36
+ .aimd-figure__title {
37
+ margin: 0;
38
+ color: #172033;
39
+ font-size: 0.94em;
40
+ font-weight: 700;
41
+ line-height: 1.38;
42
+ }
43
+
44
+ .aimd-figure__legend {
45
+ margin: 4px 0 0;
46
+ color: #526176;
47
+ font-size: 0.9em;
48
+ line-height: 1.5;
49
+ }
50
+
51
+ @media (max-width: 640px) {
52
+ .aimd-figure {
53
+ width: 100%;
54
+ margin-right: 0;
55
+ margin-left: 0;
56
+ }
57
+
58
+ .aimd-figure > .aimd-figure__image {
59
+ width: 100%;
60
+ }
61
+ }
62
+
63
+ .aimd-ref[data-aimd-ref-target] {
64
+ cursor: pointer;
65
+ }
66
+
67
+ .aimd-ref[data-aimd-ref-target]:focus-visible {
68
+ outline: 2px solid rgba(25, 118, 210, 0.36);
69
+ outline-offset: 3px;
70
+ }
71
+
72
+ .aimd-cite {
73
+ display: inline;
74
+ color: #1976d2;
75
+ font-size: 13px;
76
+ text-decoration: none;
77
+ }
78
+
79
+ .aimd-cite__refs {
80
+ font-weight: 500;
81
+ }
82
+
83
+ .aimd-cite__ref {
84
+ position: relative;
85
+ display: inline-flex;
86
+ align-items: center;
87
+ border-radius: 999px;
88
+ outline: none;
89
+ color: inherit;
90
+ cursor: help;
91
+ font-weight: 600;
92
+ line-height: 1;
93
+ text-decoration: none;
94
+ }
95
+
96
+ .aimd-cite__ref:hover,
97
+ .aimd-cite__ref:focus-visible {
98
+ text-decoration: underline;
99
+ }
100
+
101
+ .aimd-cite__label {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ }
105
+
106
+ .aimd-cite__popover {
107
+ position: absolute;
108
+ left: 50%;
109
+ top: calc(100% + 8px);
110
+ z-index: 30;
111
+ width: max-content;
112
+ max-width: min(420px, 80vw);
113
+ padding: 8px 10px;
114
+ border: 1px solid #d6e1f0;
115
+ border-radius: 8px;
116
+ background: #ffffff;
117
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.14);
118
+ color: #1f2937;
119
+ font-size: 12px;
120
+ font-weight: 500;
121
+ line-height: 1.45;
122
+ opacity: 0;
123
+ overflow-wrap: anywhere;
124
+ pointer-events: none;
125
+ user-select: text;
126
+ text-align: left;
127
+ text-decoration: none;
128
+ transform: translate(-50%, -4px);
129
+ transition: opacity 120ms ease, transform 120ms ease, visibility 120ms ease;
130
+ visibility: hidden;
131
+ white-space: normal;
132
+ }
133
+
134
+ .aimd-cite__popover::before {
135
+ content: "";
136
+ position: absolute;
137
+ left: 0;
138
+ right: 0;
139
+ top: -10px;
140
+ height: 10px;
141
+ }
142
+
143
+ .aimd-cite__ref:hover .aimd-cite__popover,
144
+ .aimd-cite__ref:focus .aimd-cite__popover,
145
+ .aimd-cite__ref:focus-within .aimd-cite__popover {
146
+ opacity: 1;
147
+ pointer-events: auto;
148
+ transform: translate(-50%, 0);
149
+ visibility: visible;
150
+ }
151
+
4
152
  .aimd-field__metadata-host {
5
153
  position: relative;
6
154
  cursor: help;
package/src/vue/index.ts CHANGED
@@ -60,6 +60,10 @@ export type {
60
60
  } from '@airalogy/aimd-core/types'
61
61
 
62
62
  export type { AimdAssignerVisibility, AimdRendererOptions, RenderResult } from '../common/processor'
63
+ export type {
64
+ AimdAssetUrlResolver,
65
+ AimdAssetUrlResolveContext,
66
+ } from '../common/assetUrls'
63
67
  export type {
64
68
  AimdRendererI18nOptions,
65
69
  AimdRendererLocale,
@@ -1,6 +1,8 @@
1
1
  import type { Element, Root as HastRoot, Text as HastText, RootContent } from "hast"
2
2
  import type { Component, VNode, VNodeChild } from "vue"
3
- import type { AimdNode, AimdQuizNode, AimdStepNode, RenderContext } from "@airalogy/aimd-core/types"
3
+ import type { AimdNode, AimdQuizNode, AimdReferenceEntry, AimdStepNode, RenderContext } from "@airalogy/aimd-core/types"
4
+ import type { AimdCitationReferenceDisplay } from "../common/citationNumbering"
5
+ import type { AimdAssetUrlResolver } from "../common/assetUrls"
4
6
  import {
5
7
  formatAimdExampleValue,
6
8
  getAimdFieldDescription,
@@ -10,6 +12,8 @@ import {
10
12
  } from "@airalogy/aimd-core/utils"
11
13
  import { Fragment, h } from "vue"
12
14
  import type { AimdRendererI18nOptions, AimdRendererLocale, AimdRendererMessages } from "../locales"
15
+ import { resolveAimdAssetUrl } from "../common/assetUrls"
16
+ import { formatCitationReferenceSummary, moveReferenceSectionsToEnd } from "../common/citationNumbering"
13
17
  import { resolveQuizPreviewOptions, type ResolvedQuizPreviewOptions } from "../common/quiz-preview"
14
18
  import {
15
19
  createAimdRendererMessages,
@@ -48,6 +52,7 @@ export type ElementRenderer = (
48
52
  export interface AimdRendererContext extends RenderContext {
49
53
  locale: AimdRendererLocale
50
54
  messages: AimdRendererMessages
55
+ resolveAssetUrl?: AimdAssetUrlResolver
51
56
  }
52
57
 
53
58
  export interface AimdStepCardRendererOptions {
@@ -159,6 +164,66 @@ function buildScaleBandChildren(quizNode: AimdQuizNode): VNodeChild[] {
159
164
  ]
160
165
  }
161
166
 
167
+ function getReferenceContainer(entry: AimdReferenceEntry): string | undefined {
168
+ return entry.journal || entry.booktitle || entry.publisher
169
+ }
170
+
171
+ function buildReferenceChildren(entry: AimdReferenceEntry): VNodeChild[] {
172
+ const children: VNodeChild[] = []
173
+ const pushText = (value: string | undefined, suffix = "") => {
174
+ const normalized = value?.trim()
175
+ if (!normalized) {
176
+ return
177
+ }
178
+ if (children.length > 0) {
179
+ children.push(" ")
180
+ }
181
+ children.push(`${normalized}${suffix}`)
182
+ }
183
+
184
+ pushText(entry.author)
185
+ pushText(entry.year ? `(${entry.year})` : undefined)
186
+ pushText(entry.title, entry.title && !/[.!?]$/.test(entry.title) ? "." : "")
187
+ pushText(getReferenceContainer(entry), ".")
188
+
189
+ if (entry.doi) {
190
+ if (children.length > 0) {
191
+ children.push(" ")
192
+ }
193
+ const doiHref = entry.doi.startsWith("http")
194
+ ? entry.doi
195
+ : `https://doi.org/${entry.doi}`
196
+ children.push(h("a", { class: "aimd-refs__doi", href: doiHref }, `doi:${entry.doi}`))
197
+ }
198
+ else if (entry.url) {
199
+ if (children.length > 0) {
200
+ children.push(" ")
201
+ }
202
+ children.push(h("a", { class: "aimd-refs__url", href: entry.url }, entry.url))
203
+ }
204
+
205
+ return children.length > 0 ? children : [entry.id]
206
+ }
207
+
208
+ function getCitationReferenceDisplays(node: AimdNode): AimdCitationReferenceDisplay[] {
209
+ const citationRefs = (node as any).citationRefs
210
+ if (Array.isArray(citationRefs) && citationRefs.length > 0) {
211
+ return citationRefs
212
+ .map(ref => ({
213
+ id: String(ref?.id || "").trim(),
214
+ label: String(ref?.label || ref?.id || "").trim(),
215
+ summary: String(ref?.summary || ref?.id || "").trim(),
216
+ }))
217
+ .filter(ref => ref.id && ref.label)
218
+ }
219
+
220
+ const refs: unknown[] = Array.isArray((node as any).refs) ? (node as any).refs : [node.id]
221
+ return refs
222
+ .map((ref: unknown) => String(ref).trim())
223
+ .filter(Boolean)
224
+ .map((ref: string) => ({ id: ref, label: ref, summary: formatCitationReferenceSummary(undefined, ref) }))
225
+ }
226
+
162
227
  interface FieldMetadataHelp {
163
228
  tooltip: string
164
229
  description?: string
@@ -560,7 +625,10 @@ const defaultAimdRenderers: Record<string, AimdComponentRenderer> = {
560
625
  "class": "aimd-ref aimd-ref--step",
561
626
  "data-aimd-type": "ref_step",
562
627
  "data-aimd-ref": refTarget,
628
+ "data-aimd-ref-target": refTarget,
629
+ "data-aimd-ref-kind": "step",
563
630
  "data-aimd-step-sequence": stepSequence,
631
+ "tabindex": 0,
564
632
  "title": refTarget,
565
633
  }, [
566
634
  h("span", { class: "aimd-ref__content" }, [
@@ -580,6 +648,9 @@ const defaultAimdRenderers: Record<string, AimdComponentRenderer> = {
580
648
  "class": "aimd-ref aimd-ref--var",
581
649
  "data-aimd-type": "ref_var",
582
650
  "data-aimd-ref": refTarget,
651
+ "data-aimd-ref-target": refTarget,
652
+ "data-aimd-ref-kind": "var",
653
+ "tabindex": 0,
583
654
  "title": refTarget,
584
655
  }, [
585
656
  h("span", { class: "aimd-ref__content" }, [
@@ -607,36 +678,64 @@ const defaultAimdRenderers: Record<string, AimdComponentRenderer> = {
607
678
  // Display figure number if available, otherwise show ID
608
679
  const displayText = ctx.messages.figure.reference(figureNumber !== undefined ? figureNumber : refTarget)
609
680
 
610
- // Render as link reference to the figure
611
- return h("a", {
681
+ return h("span", {
612
682
  "class": "aimd-ref aimd-ref--fig",
613
683
  "data-aimd-type": "ref_fig",
614
684
  "data-aimd-ref": refTarget,
615
- "href": `#fig-${refTarget}`,
685
+ "data-aimd-ref-target": refTarget,
686
+ "data-aimd-ref-kind": "fig",
687
+ "tabindex": 0,
688
+ "title": refTarget,
616
689
  }, [
617
690
  h("span", { class: "aimd-ref__content" }, displayText),
618
691
  ])
619
692
  },
620
693
 
621
694
  cite: (node, ctx) => {
622
- const refs = "refs" in node ? (node as any).refs : [node.id]
695
+ const citationRefs = getCitationReferenceDisplays(node)
623
696
 
624
697
  return h("span", {
625
698
  "class": "aimd-cite",
626
699
  "data-aimd-type": "cite",
627
- "data-aimd-refs": refs.join(","),
700
+ "data-aimd-refs": citationRefs.map(ref => ref.id).join(","),
701
+ "data-aimd-citation-labels": citationRefs.map(ref => ref.label).join(","),
628
702
  }, [
629
- h("span", { class: "aimd-cite__refs" }, `[${refs.join(", ")}]`),
703
+ "[",
704
+ ...citationRefs.flatMap((ref, index) => [
705
+ index > 0 ? ", " : "",
706
+ h("span", {
707
+ class: "aimd-cite__ref",
708
+ role: "doc-noteref",
709
+ tabindex: 0,
710
+ title: ref.summary,
711
+ "data-aimd-ref-id": ref.id,
712
+ "data-aimd-ref-summary": ref.summary,
713
+ "aria-label": ref.label === ref.id
714
+ ? `Reference ${ref.summary}`
715
+ : `Reference ${ref.label}: ${ref.summary}`,
716
+ }, [
717
+ h("span", { class: "aimd-cite__label" }, ref.label),
718
+ h("span", {
719
+ class: "aimd-cite__popover",
720
+ role: "tooltip",
721
+ }, ref.summary),
722
+ ]),
723
+ ]),
724
+ "]",
630
725
  ])
631
726
  },
632
727
 
633
728
  fig: (node, ctx) => {
634
729
  const figNode = node as any
635
730
  const figId = figNode.id || node.id
636
- const figSrc = figNode.src || ""
637
731
  const figTitle = figNode.title
638
732
  const figLegend = figNode.legend
639
733
  const figSequence = figNode.sequence
734
+ const figSrc = resolveAimdAssetUrl(figNode.src || "", ctx.resolveAssetUrl, {
735
+ kind: "fig",
736
+ id: figId,
737
+ title: figTitle,
738
+ }) || ""
640
739
 
641
740
  const children: VNodeChild[] = []
642
741
 
@@ -684,6 +783,29 @@ const defaultAimdRenderers: Record<string, AimdComponentRenderer> = {
684
783
  "id": `fig-${figId}`,
685
784
  }, children)
686
785
  },
786
+
787
+ refs: (node, ctx) => {
788
+ const entries = Array.isArray((node as any).entries)
789
+ ? (node as any).entries as AimdReferenceEntry[]
790
+ : []
791
+
792
+ return h("section", {
793
+ "class": "aimd-refs",
794
+ "data-aimd-type": "refs",
795
+ "data-aimd-id": node.id,
796
+ "id": "refs",
797
+ }, [
798
+ h("div", { class: "aimd-refs__title" }, ctx.messages.references.title),
799
+ h("ol", { class: "aimd-refs__list" }, entries.map(entry =>
800
+ h("li", {
801
+ id: `ref-${entry.id}`,
802
+ class: "aimd-refs__item",
803
+ "data-aimd-ref-id": entry.id,
804
+ "data-aimd-ref-type": entry.entry_type,
805
+ }, buildReferenceChildren(entry)),
806
+ )),
807
+ ])
808
+ },
687
809
  }
688
810
 
689
811
  /**
@@ -709,7 +831,11 @@ export interface VueRendererOptions {
709
831
  /**
710
832
  * Render context
711
833
  */
712
- context?: RenderContext & Partial<Pick<AimdRendererContext, "locale" | "messages">>
834
+ context?: RenderContext & Partial<Pick<AimdRendererContext, "locale" | "messages" | "resolveAssetUrl">>
835
+ /**
836
+ * Resolve protocol-local figure assets to displayable URLs.
837
+ */
838
+ resolveAssetUrl?: AimdAssetUrlResolver
713
839
  /**
714
840
  * Custom AIMD component renderers
715
841
  * Override default renderers or add new ones
@@ -751,6 +877,7 @@ function resolveRenderContext(options: VueRendererOptions): AimdRendererContext
751
877
  quizPreview: context?.quizPreview ?? topLevelQuizPreview ?? undefined,
752
878
  locale,
753
879
  messages,
880
+ resolveAssetUrl: context?.resolveAssetUrl ?? options.resolveAssetUrl,
754
881
  }
755
882
  }
756
883
 
@@ -817,6 +944,29 @@ function preprocessFigures(node: HastRoot | RootContent, figCtx: FigureContext):
817
944
  /**
818
945
  * Parse AIMD node from HAST element properties
819
946
  */
947
+ function splitCsvProp(value: unknown): string[] {
948
+ return String(value || "")
949
+ .split(",")
950
+ .map(part => part.trim())
951
+ .filter(Boolean)
952
+ }
953
+
954
+ function parseStringArrayProp(value: unknown): string[] {
955
+ if (typeof value !== "string" || !value.trim()) {
956
+ return []
957
+ }
958
+
959
+ try {
960
+ const parsed = JSON.parse(value)
961
+ return Array.isArray(parsed)
962
+ ? parsed.map(item => String(item || "").trim()).filter(Boolean)
963
+ : []
964
+ }
965
+ catch {
966
+ return []
967
+ }
968
+ }
969
+
820
970
  function parseAimdFromProps(props: Record<string, unknown>): AimdNode | undefined {
821
971
  let parsedFromJson: Record<string, unknown> | undefined
822
972
 
@@ -850,10 +1000,23 @@ function parseAimdFromProps(props: Record<string, unknown>): AimdNode | undefine
850
1000
  }
851
1001
 
852
1002
  const stepSequence = props["data-aimd-step-sequence"] || props.dataAimdStepSequence
1003
+ const citationRefIds = splitCsvProp(props["data-aimd-refs"] || props.dataAimdRefs)
1004
+ const citationLabels = splitCsvProp(props["data-aimd-citation-labels"] || props.dataAimdCitationLabels)
1005
+ const citationSummaries = parseStringArrayProp(props["data-aimd-citation-summaries"] || props.dataAimdCitationSummaries)
1006
+ const citationRefs = citationRefIds.map((refId, index) => ({
1007
+ id: refId,
1008
+ label: citationLabels[index] || refId,
1009
+ summary: citationSummaries[index] || refId,
1010
+ }))
1011
+
853
1012
  if (parsedFromJson) {
854
1013
  if (typeof stepSequence === "string" && stepSequence.trim()) {
855
1014
  parsedFromJson.stepSequence = stepSequence
856
1015
  }
1016
+ if (fieldType === "cite" && citationRefs.length > 0) {
1017
+ parsedFromJson.refs = citationRefs.map(ref => ref.id)
1018
+ parsedFromJson.citationRefs = citationRefs
1019
+ }
857
1020
  return parsedFromJson as unknown as AimdNode
858
1021
  }
859
1022
 
@@ -888,6 +1051,16 @@ function parseAimdFromProps(props: Record<string, unknown>): AimdNode | undefine
888
1051
  } as AimdNode
889
1052
  }
890
1053
 
1054
+ if (fieldType === "cite") {
1055
+ const refs = citationRefIds.length > 0 ? citationRefIds : [id]
1056
+ return {
1057
+ ...baseNode,
1058
+ fieldType: "cite",
1059
+ refs,
1060
+ ...(citationRefs.length > 0 ? { citationRefs } : {}),
1061
+ } as unknown as AimdNode
1062
+ }
1063
+
891
1064
  // Add quiz-specific properties
892
1065
  if (fieldType === "quiz") {
893
1066
  const quizType = (props["data-aimd-quiz-type"] || props.dataAimdQuizType || "open") as string
@@ -1145,6 +1318,7 @@ export function renderToVNodes(
1145
1318
  tree: HastRoot,
1146
1319
  options: VueRendererOptions = {},
1147
1320
  ): VNode[] {
1321
+ moveReferenceSectionsToEnd(tree)
1148
1322
  const result = hastToVue(tree, options)
1149
1323
 
1150
1324
  if (result === null || result === undefined) {