@airalogy/aimd-renderer 2.4.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 +117 -0
- package/README.zh-CN.md +101 -0
- package/dist/aimd-renderer.css +1 -0
- package/dist/html.js +11 -0
- package/dist/index.js +439 -0
- package/dist/processor-Cv8E7QsA.js +11539 -0
- package/dist/vue.js +17 -0
- package/package.json +84 -0
- package/src/__tests__/renderer.test.ts +388 -0
- package/src/common/annotateStepReferences.ts +110 -0
- package/src/common/assignerHighlighting.ts +159 -0
- package/src/common/assignerVisibility.ts +289 -0
- package/src/common/eventKeys.ts +10 -0
- package/src/common/figureNumbering.ts +126 -0
- package/src/common/processor.ts +1554 -0
- package/src/common/quiz-preview.ts +22 -0
- package/src/common/unified-token-renderer.ts +810 -0
- package/src/css.d.ts +1 -0
- package/src/html/index.ts +33 -0
- package/src/index.ts +114 -0
- package/src/locales.ts +256 -0
- package/src/styles/katex.css +2 -0
- package/src/vue/index.ts +47 -0
- package/src/vue/vue-renderer.ts +1449 -0
|
@@ -0,0 +1,1449 @@
|
|
|
1
|
+
import type { Element, Root as HastRoot, Text as HastText, RootContent } from "hast"
|
|
2
|
+
import type { Component, VNode, VNodeChild } from "vue"
|
|
3
|
+
import type { AimdNode, AimdQuizNode, AimdStepNode, RenderContext } from "@airalogy/aimd-core/types"
|
|
4
|
+
import { Fragment, h } from "vue"
|
|
5
|
+
import type { AimdRendererI18nOptions, AimdRendererLocale, AimdRendererMessages } from "../locales"
|
|
6
|
+
import { resolveQuizPreviewOptions, type ResolvedQuizPreviewOptions } from "../common/quiz-preview"
|
|
7
|
+
import {
|
|
8
|
+
createAimdRendererMessages,
|
|
9
|
+
DEFAULT_AIMD_RENDERER_LOCALE,
|
|
10
|
+
getAimdRendererQuizTypeLabel,
|
|
11
|
+
getAimdRendererScopeLabel,
|
|
12
|
+
resolveAimdRendererLocale,
|
|
13
|
+
} from "../locales"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extended Element data type
|
|
17
|
+
*/
|
|
18
|
+
export interface AimdElementData {
|
|
19
|
+
aimd?: AimdNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* AIMD component renderer function type
|
|
24
|
+
* Can return VNode directly or a Promise for async rendering
|
|
25
|
+
*/
|
|
26
|
+
export type AimdComponentRenderer = (
|
|
27
|
+
node: AimdNode,
|
|
28
|
+
ctx: AimdRendererContext,
|
|
29
|
+
children?: VNodeChild[]
|
|
30
|
+
) => VNode | Promise<VNode> | null
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Element renderer function type
|
|
34
|
+
*/
|
|
35
|
+
export type ElementRenderer = (
|
|
36
|
+
node: Element,
|
|
37
|
+
children: VNodeChild[],
|
|
38
|
+
ctx: AimdRendererContext
|
|
39
|
+
) => VNode | null
|
|
40
|
+
|
|
41
|
+
export interface AimdRendererContext extends RenderContext {
|
|
42
|
+
locale: AimdRendererLocale
|
|
43
|
+
messages: AimdRendererMessages
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AimdStepCardRendererOptions {
|
|
47
|
+
showScopeLabel?: boolean
|
|
48
|
+
showCheckBadge?: boolean
|
|
49
|
+
bodyClassName?: string
|
|
50
|
+
className?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveQuizPreviewOptionsFromContext(ctx: RenderContext): ResolvedQuizPreviewOptions {
|
|
54
|
+
return resolveQuizPreviewOptions(ctx.mode, ctx.quizPreview)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const BLANK_PLACEHOLDER_PATTERN = /\[\[([^\[\]\s]+)\]\]/g
|
|
58
|
+
|
|
59
|
+
function buildQuizStemChildren(
|
|
60
|
+
quizType: AimdQuizNode["quizType"],
|
|
61
|
+
stem: string,
|
|
62
|
+
): VNodeChild[] {
|
|
63
|
+
if (quizType !== "blank") {
|
|
64
|
+
return [stem]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const children: VNodeChild[] = []
|
|
68
|
+
let lastIndex = 0
|
|
69
|
+
|
|
70
|
+
for (const match of stem.matchAll(BLANK_PLACEHOLDER_PATTERN)) {
|
|
71
|
+
const start = match.index ?? 0
|
|
72
|
+
const fullMatch = match[0]
|
|
73
|
+
const key = match[1]
|
|
74
|
+
|
|
75
|
+
if (start > lastIndex) {
|
|
76
|
+
children.push(stem.slice(lastIndex, start))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
children.push(
|
|
80
|
+
h("span", {
|
|
81
|
+
class: "aimd-quiz__blank-placeholder",
|
|
82
|
+
"data-blank-key": key,
|
|
83
|
+
}, key),
|
|
84
|
+
)
|
|
85
|
+
lastIndex = start + fullMatch.length
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (lastIndex < stem.length) {
|
|
89
|
+
children.push(stem.slice(lastIndex))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (children.length === 0) {
|
|
93
|
+
children.push(stem)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return children
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatScaleOptionLabel(option: NonNullable<AimdQuizNode["options"]>[number]): string {
|
|
100
|
+
if (typeof option.points === "number" && Number.isFinite(option.points)) {
|
|
101
|
+
return `${option.text} (${option.points})`
|
|
102
|
+
}
|
|
103
|
+
return option.text
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildScalePreviewChildren(quizNode: AimdQuizNode): VNodeChild[] {
|
|
107
|
+
if (!Array.isArray(quizNode.items) || quizNode.items.length === 0 || !Array.isArray(quizNode.options) || quizNode.options.length === 0) {
|
|
108
|
+
return [h("div", { class: "aimd-scale__empty" }, "Scale definition is incomplete.")]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (quizNode.display === "list") {
|
|
112
|
+
return [
|
|
113
|
+
h("div", { class: "aimd-scale__list" }, quizNode.items.map(item =>
|
|
114
|
+
h("div", { class: "aimd-scale__list-item" }, [
|
|
115
|
+
h("div", { class: "aimd-scale__item-stem" }, item.stem),
|
|
116
|
+
h("ul", { class: "aimd-scale__item-options" }, quizNode.options!.map(option =>
|
|
117
|
+
h("li", formatScaleOptionLabel(option)),
|
|
118
|
+
)),
|
|
119
|
+
]),
|
|
120
|
+
)),
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
h("table", { class: "aimd-scale__table" }, [
|
|
126
|
+
h("thead", [
|
|
127
|
+
h("tr", [
|
|
128
|
+
h("th", { class: "aimd-scale__item-header" }, "Item"),
|
|
129
|
+
...quizNode.options.map(option => h("th", formatScaleOptionLabel(option))),
|
|
130
|
+
]),
|
|
131
|
+
]),
|
|
132
|
+
h("tbody", quizNode.items.map(item =>
|
|
133
|
+
h("tr", [
|
|
134
|
+
h("th", { class: "aimd-scale__item-stem", scope: "row" }, item.stem),
|
|
135
|
+
...quizNode.options!.map(() => h("td", { class: "aimd-scale__cell" }, "○")),
|
|
136
|
+
]),
|
|
137
|
+
)),
|
|
138
|
+
]),
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildScaleBandChildren(quizNode: AimdQuizNode): VNodeChild[] {
|
|
143
|
+
const bands = Array.isArray((quizNode.grading as any)?.bands) ? (quizNode.grading as any).bands : []
|
|
144
|
+
if (bands.length === 0) {
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [
|
|
149
|
+
h("ul", { class: "aimd-scale__bands" }, bands.map((band: any) =>
|
|
150
|
+
h("li", `${band.min}-${band.max}: ${band.label}${band.interpretation ? ` · ${band.interpretation}` : ""}`),
|
|
151
|
+
)),
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isStepBodyVNode(node: unknown): node is VNode {
|
|
156
|
+
if (!node || typeof node !== "object") {
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const props = (node as VNode).props as Record<string, unknown> | null | undefined
|
|
161
|
+
if (!props) {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const classValue = props.class
|
|
166
|
+
const classNames = Array.isArray(classValue)
|
|
167
|
+
? classValue
|
|
168
|
+
: typeof classValue === "string"
|
|
169
|
+
? [classValue]
|
|
170
|
+
: []
|
|
171
|
+
|
|
172
|
+
return props["data-aimd-step-body"] === "true"
|
|
173
|
+
|| props["data-aimd-step-body"] === true
|
|
174
|
+
|| props.dataAimdStepBody === "true"
|
|
175
|
+
|| props.dataAimdStepBody === true
|
|
176
|
+
|| classNames.some((className) => typeof className === "string" && className.includes("aimd-step-body"))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeStepCardBodyChildren(children?: VNodeChild[]): VNodeChild[] {
|
|
180
|
+
if (!children || children.length === 0) {
|
|
181
|
+
return []
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const groupedBody = children.find((child) => isStepBodyVNode(child))
|
|
185
|
+
if (groupedBody && typeof groupedBody === "object" && groupedBody !== null) {
|
|
186
|
+
const groupedChildren = (groupedBody as VNode).children
|
|
187
|
+
if (Array.isArray(groupedChildren)) {
|
|
188
|
+
return groupedChildren as VNodeChild[]
|
|
189
|
+
}
|
|
190
|
+
if (groupedChildren !== null && groupedChildren !== undefined) {
|
|
191
|
+
return [groupedChildren as VNodeChild]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return children
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Default AIMD component renderers
|
|
200
|
+
*/
|
|
201
|
+
const defaultAimdRenderers: Record<string, AimdComponentRenderer> = {
|
|
202
|
+
var: (node, ctx) => {
|
|
203
|
+
const { id, scope } = node
|
|
204
|
+
const definition = "definition" in node ? node.definition : undefined
|
|
205
|
+
|
|
206
|
+
if (ctx.mode === "preview") {
|
|
207
|
+
return h("span", {
|
|
208
|
+
"class": "aimd-field aimd-field--var",
|
|
209
|
+
"data-aimd-type": "var",
|
|
210
|
+
"data-aimd-id": id,
|
|
211
|
+
"data-aimd-scope": scope,
|
|
212
|
+
}, [
|
|
213
|
+
h("span", { class: "aimd-field__scope" }, getAimdRendererScopeLabel(scope, ctx.messages)),
|
|
214
|
+
h("span", { class: "aimd-field__name" }, id),
|
|
215
|
+
definition?.type ? h("span", { class: "aimd-field__type" }, `: ${definition.type}`) : null,
|
|
216
|
+
])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Edit mode - render as editable field with value display
|
|
220
|
+
const fieldData = ctx.value?.[scope]?.[id]
|
|
221
|
+
const value = typeof fieldData === "object" && fieldData !== null && "value" in fieldData
|
|
222
|
+
? (fieldData as { value: unknown }).value
|
|
223
|
+
: fieldData
|
|
224
|
+
const displayValue = value !== undefined && value !== null && value !== "" ? String(value) : id
|
|
225
|
+
|
|
226
|
+
return h("span", {
|
|
227
|
+
"class": "aimd-field aimd-field--var aimd-field--editable",
|
|
228
|
+
"data-aimd-type": "var",
|
|
229
|
+
"data-aimd-id": id,
|
|
230
|
+
"data-aimd-scope": scope,
|
|
231
|
+
"data-has-variable": "true",
|
|
232
|
+
"id": `${scope}-${id}`,
|
|
233
|
+
}, [
|
|
234
|
+
h("span", { class: "aimd-field__value" }, displayValue),
|
|
235
|
+
])
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
var_table: (node, ctx) => {
|
|
239
|
+
const { id } = node
|
|
240
|
+
const columns = "columns" in node ? node.columns : []
|
|
241
|
+
|
|
242
|
+
if (ctx.mode === "preview") {
|
|
243
|
+
// Preview mode: render tag with table preview inside
|
|
244
|
+
const children: VNodeChild[] = [
|
|
245
|
+
h("div", { class: "aimd-field__header" }, [
|
|
246
|
+
h("span", { class: "aimd-field__scope" }, ctx.messages.scope.table),
|
|
247
|
+
h("span", { class: "aimd-field__name" }, id),
|
|
248
|
+
]),
|
|
249
|
+
]
|
|
250
|
+
// Add table preview inside the container
|
|
251
|
+
if (columns && columns.length > 0) {
|
|
252
|
+
children.push(
|
|
253
|
+
h("table", { class: "aimd-field__table-preview" }, [
|
|
254
|
+
h("thead", [
|
|
255
|
+
h("tr", columns.map(col => h("th", col))),
|
|
256
|
+
]),
|
|
257
|
+
h("tbody", [
|
|
258
|
+
h("tr", columns.map(() => h("td", "..."))),
|
|
259
|
+
]),
|
|
260
|
+
]),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
return h("div", {
|
|
264
|
+
"class": "aimd-field aimd-field--var-table",
|
|
265
|
+
"data-aimd-type": "var_table",
|
|
266
|
+
"data-aimd-id": id,
|
|
267
|
+
}, children)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Edit mode: render empty container (will be replaced by AIMDTag component)
|
|
271
|
+
return h("div", {
|
|
272
|
+
"class": "aimd-field aimd-field--var-table aimd-field--editable",
|
|
273
|
+
"data-aimd-type": "var_table",
|
|
274
|
+
"data-aimd-id": id,
|
|
275
|
+
"id": `var_table-${id}`,
|
|
276
|
+
})
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
quiz: (node, ctx) => {
|
|
280
|
+
const quizNode = node as AimdQuizNode
|
|
281
|
+
const { id, scope, quizType, stem, score } = quizNode
|
|
282
|
+
const typeLabel = getAimdRendererQuizTypeLabel(quizType, quizNode.mode, ctx.messages)
|
|
283
|
+
|
|
284
|
+
if (ctx.mode === "preview") {
|
|
285
|
+
const previewChildren: VNodeChild[] = [
|
|
286
|
+
h("div", { class: "aimd-quiz__meta" }, [
|
|
287
|
+
h("span", { class: "aimd-field__scope" }, getAimdRendererScopeLabel(scope, ctx.messages)),
|
|
288
|
+
h("span", { class: "aimd-field__name" }, id),
|
|
289
|
+
h("span", { class: "aimd-field__type" }, `(${typeLabel})`),
|
|
290
|
+
score !== undefined ? h("span", { class: "aimd-quiz__score" }, ctx.messages.quiz.score(score)) : null,
|
|
291
|
+
]),
|
|
292
|
+
typeof quizNode.title === "string" && quizNode.title.trim()
|
|
293
|
+
? h("div", { class: "aimd-quiz__title" }, quizNode.title)
|
|
294
|
+
: null,
|
|
295
|
+
h("div", { class: "aimd-quiz__stem" }, buildQuizStemChildren(quizType, stem || id)),
|
|
296
|
+
typeof quizNode.description === "string" && quizNode.description.trim()
|
|
297
|
+
? h("div", { class: "aimd-quiz__description" }, quizNode.description)
|
|
298
|
+
: null,
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
if (quizType === "choice" && Array.isArray(quizNode.options) && quizNode.options.length > 0) {
|
|
302
|
+
previewChildren.push(
|
|
303
|
+
h("ul", { class: "aimd-quiz__options" }, quizNode.options.map(opt =>
|
|
304
|
+
h("li", `${opt.key}. ${opt.text}`),
|
|
305
|
+
)),
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (quizType === "scale") {
|
|
310
|
+
previewChildren.push(...buildScalePreviewChildren(quizNode))
|
|
311
|
+
previewChildren.push(...buildScaleBandChildren(quizNode))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const quizPreview = resolveQuizPreviewOptionsFromContext(ctx)
|
|
315
|
+
|
|
316
|
+
if (quizPreview.showAnswers && quizType === "choice" && quizNode.answer !== undefined) {
|
|
317
|
+
const answerText = Array.isArray(quizNode.answer)
|
|
318
|
+
? quizNode.answer.join(", ")
|
|
319
|
+
: String(quizNode.answer)
|
|
320
|
+
if (answerText.trim()) {
|
|
321
|
+
previewChildren.push(
|
|
322
|
+
h("div", { class: "aimd-quiz__answer" }, ctx.messages.quiz.answer(answerText)),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (quizPreview.showAnswers && quizType === "blank" && Array.isArray(quizNode.blanks) && quizNode.blanks.length > 0) {
|
|
328
|
+
previewChildren.push(
|
|
329
|
+
h("ul", { class: "aimd-quiz__blanks" }, quizNode.blanks.map(blank =>
|
|
330
|
+
h("li", `${blank.key}: ${blank.answer}`),
|
|
331
|
+
)),
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (quizPreview.showRubric && quizType === "open" && typeof quizNode.rubric === "string" && quizNode.rubric.trim()) {
|
|
336
|
+
previewChildren.push(
|
|
337
|
+
h("div", { class: "aimd-quiz__rubric" }, ctx.messages.quiz.rubric(quizNode.rubric)),
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return h("div", {
|
|
342
|
+
"class": "aimd-field aimd-field--quiz",
|
|
343
|
+
"data-aimd-type": "quiz",
|
|
344
|
+
"data-aimd-id": id,
|
|
345
|
+
"data-aimd-scope": scope,
|
|
346
|
+
"id": `quiz-${id}`,
|
|
347
|
+
}, previewChildren)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const fieldData = ctx.value?.[scope]?.[id]
|
|
351
|
+
let displayValue: string
|
|
352
|
+
if (Array.isArray(fieldData)) {
|
|
353
|
+
displayValue = fieldData.join(", ")
|
|
354
|
+
}
|
|
355
|
+
else if (isPlainObject(fieldData)) {
|
|
356
|
+
displayValue = JSON.stringify(fieldData)
|
|
357
|
+
}
|
|
358
|
+
else if (fieldData === undefined || fieldData === null || fieldData === "") {
|
|
359
|
+
displayValue = id
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
displayValue = String(fieldData)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return h("div", {
|
|
366
|
+
"class": "aimd-field aimd-field--quiz aimd-field--editable",
|
|
367
|
+
"data-aimd-type": "quiz",
|
|
368
|
+
"data-aimd-id": id,
|
|
369
|
+
"data-aimd-scope": scope,
|
|
370
|
+
"id": `quiz-${id}`,
|
|
371
|
+
}, [
|
|
372
|
+
h("span", { class: "aimd-field__value" }, displayValue),
|
|
373
|
+
])
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
step: (node, ctx, children) => {
|
|
377
|
+
const { id, scope } = node
|
|
378
|
+
const stepNode = node as AimdStepNode
|
|
379
|
+
const stepNum = stepNode.step || "1"
|
|
380
|
+
|
|
381
|
+
if (ctx.mode === "preview") {
|
|
382
|
+
// Preview mode: render as localized step label + sequence + id
|
|
383
|
+
return h("span", {
|
|
384
|
+
"class": "aimd-field aimd-field--step",
|
|
385
|
+
"data-aimd-type": "step",
|
|
386
|
+
"data-aimd-id": id,
|
|
387
|
+
"data-aimd-step": stepNum,
|
|
388
|
+
"id": `step-${id}`,
|
|
389
|
+
}, [
|
|
390
|
+
h("span", { class: "aimd-field__scope" }, getAimdRendererScopeLabel(scope, ctx.messages)),
|
|
391
|
+
h("span", { class: "aimd-field__step-num" }, stepNum),
|
|
392
|
+
h("span", { class: "aimd-field__name" }, id),
|
|
393
|
+
])
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Edit mode - render step container with children
|
|
397
|
+
return h("div", {
|
|
398
|
+
"class": "aimd-field aimd-field--step aimd-field--editable research-step__item",
|
|
399
|
+
"data-aimd-type": "step",
|
|
400
|
+
"data-aimd-id": id,
|
|
401
|
+
"data-aimd-step": stepNum,
|
|
402
|
+
"data-aimd-level": stepNode.level,
|
|
403
|
+
"id": `step-${id}`,
|
|
404
|
+
}, [
|
|
405
|
+
h("div", { class: "research-step__header" }, [
|
|
406
|
+
h("span", { class: "research-step__sequence" }, ctx.messages.step.sequence(stepNum)),
|
|
407
|
+
]),
|
|
408
|
+
children && children.length > 0
|
|
409
|
+
? h("div", { class: "research-step__content" }, children)
|
|
410
|
+
: null,
|
|
411
|
+
])
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
check: (node, ctx, children) => {
|
|
415
|
+
const { id, scope } = node
|
|
416
|
+
const label = "label" in node ? node.label : id
|
|
417
|
+
|
|
418
|
+
if (ctx.mode === "preview") {
|
|
419
|
+
// Preview mode: render with checkbox (disabled)
|
|
420
|
+
return h("label", {
|
|
421
|
+
"class": "aimd-field aimd-field--check",
|
|
422
|
+
"data-aimd-type": "check",
|
|
423
|
+
"data-aimd-id": id,
|
|
424
|
+
"id": `check-${id}`,
|
|
425
|
+
}, [
|
|
426
|
+
h("input", {
|
|
427
|
+
type: "checkbox",
|
|
428
|
+
disabled: true,
|
|
429
|
+
class: "aimd-checkbox",
|
|
430
|
+
}),
|
|
431
|
+
h("span", { class: "aimd-field__label" }, label),
|
|
432
|
+
])
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const scopeValue = ctx.value?.[scope]?.[id]
|
|
436
|
+
const checked = typeof scopeValue === "object" && scopeValue !== null && "checked" in scopeValue
|
|
437
|
+
? Boolean((scopeValue as Record<string, unknown>).checked)
|
|
438
|
+
: false
|
|
439
|
+
|
|
440
|
+
return h("label", {
|
|
441
|
+
"class": "aimd-field aimd-field--check aimd-field--editable",
|
|
442
|
+
"data-aimd-type": "check",
|
|
443
|
+
"data-aimd-id": id,
|
|
444
|
+
"id": `check-${id}`,
|
|
445
|
+
}, [
|
|
446
|
+
h("input", {
|
|
447
|
+
type: "checkbox",
|
|
448
|
+
checked,
|
|
449
|
+
disabled: ctx.readonly,
|
|
450
|
+
class: "aimd-checkbox",
|
|
451
|
+
onChange: (e: Event) => {
|
|
452
|
+
if (!ctx.value)
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
const scopeValues = (ctx.value[scope] ??= {})
|
|
456
|
+
const nextChecked = (e.target as HTMLInputElement).checked
|
|
457
|
+
const currentValue = scopeValues[id]
|
|
458
|
+
|
|
459
|
+
if (typeof currentValue === "object" && currentValue !== null) {
|
|
460
|
+
(currentValue as Record<string, unknown>).checked = nextChecked
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
scopeValues[id] = { checked: nextChecked }
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
h("span", { class: "aimd-field__label" }, label),
|
|
468
|
+
children && children.length > 0 ? children : null,
|
|
469
|
+
])
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
ref_step: (node, ctx) => {
|
|
473
|
+
const { id } = node
|
|
474
|
+
const refTarget = "refTarget" in node ? node.refTarget : id
|
|
475
|
+
const stepSequence = "stepSequence" in node && typeof (node as any).stepSequence === "string"
|
|
476
|
+
? (node as any).stepSequence
|
|
477
|
+
: undefined
|
|
478
|
+
const displayText = stepSequence ? ctx.messages.step.reference(stepSequence) : refTarget
|
|
479
|
+
|
|
480
|
+
return h("span", {
|
|
481
|
+
"class": "aimd-ref aimd-ref--step",
|
|
482
|
+
"data-aimd-type": "ref_step",
|
|
483
|
+
"data-aimd-ref": refTarget,
|
|
484
|
+
"data-aimd-step-sequence": stepSequence,
|
|
485
|
+
"title": refTarget,
|
|
486
|
+
}, [
|
|
487
|
+
h("span", { class: "aimd-ref__content" }, [
|
|
488
|
+
h("span", { class: "aimd-field aimd-field--step aimd-field--readonly" }, [
|
|
489
|
+
h("span", { class: "research-step__sequence" }, displayText),
|
|
490
|
+
]),
|
|
491
|
+
]),
|
|
492
|
+
])
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
ref_var: (node, ctx) => {
|
|
496
|
+
const { id } = node
|
|
497
|
+
const refTarget = "refTarget" in node ? node.refTarget : id
|
|
498
|
+
const referencedValue = ctx.mode === "edit" ? getReferencedVarDisplayValue(ctx.value, refTarget) : null
|
|
499
|
+
|
|
500
|
+
return h("span", {
|
|
501
|
+
"class": "aimd-ref aimd-ref--var",
|
|
502
|
+
"data-aimd-type": "ref_var",
|
|
503
|
+
"data-aimd-ref": refTarget,
|
|
504
|
+
"title": refTarget,
|
|
505
|
+
}, [
|
|
506
|
+
h("span", { class: "aimd-ref__content" }, [
|
|
507
|
+
referencedValue !== null
|
|
508
|
+
? h("span", {
|
|
509
|
+
class: "aimd-field aimd-field--var aimd-field--readonly",
|
|
510
|
+
"data-aimd-id": refTarget,
|
|
511
|
+
"data-aimd-scope": "var",
|
|
512
|
+
}, [
|
|
513
|
+
h("span", { class: "aimd-field__value" }, referencedValue),
|
|
514
|
+
])
|
|
515
|
+
: h("span", { class: "aimd-field aimd-field--var" }, [
|
|
516
|
+
h("span", { class: "aimd-field__scope" }, ctx.messages.scope.var),
|
|
517
|
+
h("span", { class: "aimd-field__name" }, refTarget),
|
|
518
|
+
]),
|
|
519
|
+
]),
|
|
520
|
+
])
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
ref_fig: (node, ctx) => {
|
|
524
|
+
const { id } = node
|
|
525
|
+
const refTarget = "refTarget" in node ? node.refTarget : id
|
|
526
|
+
const figureNumber = "figureNumber" in node ? (node as any).figureNumber : undefined
|
|
527
|
+
|
|
528
|
+
// Display figure number if available, otherwise show ID
|
|
529
|
+
const displayText = ctx.messages.figure.reference(figureNumber !== undefined ? figureNumber : refTarget)
|
|
530
|
+
|
|
531
|
+
// Render as link reference to the figure
|
|
532
|
+
return h("a", {
|
|
533
|
+
"class": "aimd-ref aimd-ref--fig",
|
|
534
|
+
"data-aimd-type": "ref_fig",
|
|
535
|
+
"data-aimd-ref": refTarget,
|
|
536
|
+
"href": `#fig-${refTarget}`,
|
|
537
|
+
}, [
|
|
538
|
+
h("span", { class: "aimd-ref__content" }, displayText),
|
|
539
|
+
])
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
cite: (node, ctx) => {
|
|
543
|
+
const refs = "refs" in node ? (node as any).refs : [node.id]
|
|
544
|
+
|
|
545
|
+
return h("span", {
|
|
546
|
+
"class": "aimd-cite",
|
|
547
|
+
"data-aimd-type": "cite",
|
|
548
|
+
"data-aimd-refs": refs.join(","),
|
|
549
|
+
}, [
|
|
550
|
+
h("span", { class: "aimd-cite__refs" }, `[${refs.join(", ")}]`),
|
|
551
|
+
])
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
fig: (node, ctx) => {
|
|
555
|
+
const figNode = node as any
|
|
556
|
+
const figId = figNode.id || node.id
|
|
557
|
+
const figSrc = figNode.src || ""
|
|
558
|
+
const figTitle = figNode.title
|
|
559
|
+
const figLegend = figNode.legend
|
|
560
|
+
const figSequence = figNode.sequence
|
|
561
|
+
|
|
562
|
+
const children: VNodeChild[] = []
|
|
563
|
+
|
|
564
|
+
// Image element
|
|
565
|
+
children.push(
|
|
566
|
+
h("img", {
|
|
567
|
+
class: "aimd-figure__image",
|
|
568
|
+
src: figSrc,
|
|
569
|
+
alt: figTitle || figId,
|
|
570
|
+
loading: "lazy",
|
|
571
|
+
}),
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
// Caption (title + legend)
|
|
575
|
+
if (figTitle || figLegend || figSequence !== undefined) {
|
|
576
|
+
const captionChildren: VNodeChild[] = []
|
|
577
|
+
|
|
578
|
+
// Figure number and title
|
|
579
|
+
if (figSequence !== undefined || figTitle) {
|
|
580
|
+
const titleText = figSequence !== undefined
|
|
581
|
+
? ctx.messages.figure.captionTitle(figSequence + 1, figTitle)
|
|
582
|
+
: figTitle
|
|
583
|
+
captionChildren.push(
|
|
584
|
+
h("div", { class: "aimd-figure__title" }, titleText),
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Legend
|
|
589
|
+
if (figLegend) {
|
|
590
|
+
captionChildren.push(
|
|
591
|
+
h("div", { class: "aimd-figure__legend" }, figLegend),
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
children.push(
|
|
596
|
+
h("figcaption", { class: "aimd-figure__caption" }, captionChildren),
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return h("figure", {
|
|
601
|
+
"class": "aimd-figure",
|
|
602
|
+
"data-aimd-type": "fig",
|
|
603
|
+
"data-aimd-fig-id": figId,
|
|
604
|
+
"data-aimd-fig-src": figSrc,
|
|
605
|
+
"id": `fig-${figId}`,
|
|
606
|
+
}, children)
|
|
607
|
+
},
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Render options
|
|
612
|
+
*/
|
|
613
|
+
export interface VueRendererOptions {
|
|
614
|
+
/**
|
|
615
|
+
* Built-in renderer locale
|
|
616
|
+
*/
|
|
617
|
+
locale?: AimdRendererI18nOptions["locale"]
|
|
618
|
+
/**
|
|
619
|
+
* Optional overrides for built-in renderer copy
|
|
620
|
+
*/
|
|
621
|
+
messages?: AimdRendererI18nOptions["messages"]
|
|
622
|
+
/**
|
|
623
|
+
* Render mode shorthand (used when context is not provided)
|
|
624
|
+
*/
|
|
625
|
+
mode?: RenderContext["mode"]
|
|
626
|
+
/**
|
|
627
|
+
* Quiz preview visibility shorthand (used when context is not provided)
|
|
628
|
+
*/
|
|
629
|
+
quizPreview?: RenderContext["quizPreview"]
|
|
630
|
+
/**
|
|
631
|
+
* Render context
|
|
632
|
+
*/
|
|
633
|
+
context?: RenderContext & Partial<Pick<AimdRendererContext, "locale" | "messages">>
|
|
634
|
+
/**
|
|
635
|
+
* Custom AIMD component renderers
|
|
636
|
+
* Override default renderers or add new ones
|
|
637
|
+
*/
|
|
638
|
+
aimdRenderers?: Record<string, AimdComponentRenderer>
|
|
639
|
+
/**
|
|
640
|
+
* Custom HTML element renderers
|
|
641
|
+
* Render specific HTML tags with custom components
|
|
642
|
+
*/
|
|
643
|
+
elementRenderers?: Record<string, ElementRenderer>
|
|
644
|
+
/**
|
|
645
|
+
* Component map for rendering Vue components by tag name
|
|
646
|
+
* e.g., { 'step-renderer': StepRendererComponent }
|
|
647
|
+
*/
|
|
648
|
+
componentMap?: Record<string, Component>
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Default render context
|
|
653
|
+
*/
|
|
654
|
+
const defaultContext: AimdRendererContext = {
|
|
655
|
+
mode: "preview",
|
|
656
|
+
readonly: false,
|
|
657
|
+
locale: DEFAULT_AIMD_RENDERER_LOCALE,
|
|
658
|
+
messages: createAimdRendererMessages(DEFAULT_AIMD_RENDERER_LOCALE),
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function resolveRenderContext(options: VueRendererOptions): AimdRendererContext {
|
|
662
|
+
const topLevelMode = options.mode
|
|
663
|
+
const topLevelQuizPreview = options.quizPreview
|
|
664
|
+
const context = options.context
|
|
665
|
+
const locale = context?.locale ?? resolveAimdRendererLocale(options.locale)
|
|
666
|
+
const messages = context?.messages ?? createAimdRendererMessages(locale, options.messages)
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
...defaultContext,
|
|
670
|
+
...(context ?? {}),
|
|
671
|
+
mode: context?.mode ?? topLevelMode ?? defaultContext.mode,
|
|
672
|
+
quizPreview: context?.quizPreview ?? topLevelQuizPreview ?? undefined,
|
|
673
|
+
locale,
|
|
674
|
+
messages,
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Figure context for tracking figure numbers and references
|
|
680
|
+
*/
|
|
681
|
+
export interface FigureContext {
|
|
682
|
+
/** Map from fig ID to sequence number */
|
|
683
|
+
figureNumbers: Map<string, number>
|
|
684
|
+
/** Current figure sequence counter */
|
|
685
|
+
sequence: number
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Create initial figure context
|
|
690
|
+
*/
|
|
691
|
+
function createFigureContext(): FigureContext {
|
|
692
|
+
return {
|
|
693
|
+
figureNumbers: new Map(),
|
|
694
|
+
sequence: 0,
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Pre-process HAST tree to assign figure numbers
|
|
700
|
+
*/
|
|
701
|
+
function preprocessFigures(node: HastRoot | RootContent, figCtx: FigureContext): void {
|
|
702
|
+
if (node.type === "root") {
|
|
703
|
+
for (const child of node.children) {
|
|
704
|
+
preprocessFigures(child, figCtx)
|
|
705
|
+
}
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (node.type === "element") {
|
|
710
|
+
const element = node as Element
|
|
711
|
+
const aimdType = element.properties?.["data-aimd-type"] || element.properties?.dataAimdType
|
|
712
|
+
|
|
713
|
+
// Process fig nodes
|
|
714
|
+
if (aimdType === "fig") {
|
|
715
|
+
const figId = element.properties?.["data-aimd-fig-id"] || element.properties?.dataAimdFigId
|
|
716
|
+
if (figId && typeof figId === "string") {
|
|
717
|
+
// Assign sequence number if not already assigned
|
|
718
|
+
if (!figCtx.figureNumbers.has(figId)) {
|
|
719
|
+
figCtx.figureNumbers.set(figId, figCtx.sequence)
|
|
720
|
+
figCtx.sequence++
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Update AIMD node data if present
|
|
724
|
+
const aimdData = (element.data as AimdElementData | undefined)?.aimd
|
|
725
|
+
if (aimdData && "sequence" in aimdData) {
|
|
726
|
+
(aimdData as any).sequence = figCtx.figureNumbers.get(figId)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Recursively process children
|
|
732
|
+
for (const child of element.children || []) {
|
|
733
|
+
preprocessFigures(child, figCtx)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Parse AIMD node from HAST element properties
|
|
740
|
+
*/
|
|
741
|
+
function parseAimdFromProps(props: Record<string, unknown>): AimdNode | undefined {
|
|
742
|
+
let parsedFromJson: Record<string, unknown> | undefined
|
|
743
|
+
|
|
744
|
+
// Try to get from JSON attribute first
|
|
745
|
+
const jsonData = props["data-aimd-json"] || props.dataAimdJson
|
|
746
|
+
if (jsonData && typeof jsonData === "string") {
|
|
747
|
+
try {
|
|
748
|
+
parsedFromJson = JSON.parse(jsonData) as Record<string, unknown>
|
|
749
|
+
}
|
|
750
|
+
catch (e) {
|
|
751
|
+
console.warn("Failed to parse AIMD JSON:", e)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Reconstruct from individual properties
|
|
756
|
+
const fieldType = (props["data-aimd-type"] || props.dataAimdType) as string
|
|
757
|
+
const id = (props["data-aimd-id"] || props.dataAimdId) as string
|
|
758
|
+
const scope = (props["data-aimd-scope"] || props.dataAimdScope) as string
|
|
759
|
+
const raw = (props["data-aimd-raw"] || props.dataAimdRaw) as string
|
|
760
|
+
|
|
761
|
+
if (!fieldType || !id) {
|
|
762
|
+
return undefined
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const baseNode: Record<string, unknown> = {
|
|
766
|
+
type: "aimd" as const,
|
|
767
|
+
fieldType: fieldType as AimdNode["fieldType"],
|
|
768
|
+
id,
|
|
769
|
+
scope: (scope || "var") as AimdNode["scope"],
|
|
770
|
+
raw: raw || `{{${fieldType}|${id}}}`,
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const stepSequence = props["data-aimd-step-sequence"] || props.dataAimdStepSequence
|
|
774
|
+
if (parsedFromJson) {
|
|
775
|
+
if (typeof stepSequence === "string" && stepSequence.trim()) {
|
|
776
|
+
parsedFromJson.stepSequence = stepSequence
|
|
777
|
+
}
|
|
778
|
+
return parsedFromJson as unknown as AimdNode
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (typeof stepSequence === "string" && stepSequence.trim()) {
|
|
782
|
+
baseNode.stepSequence = stepSequence
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Add step-specific properties
|
|
786
|
+
if (fieldType === "step") {
|
|
787
|
+
const step = (props["data-aimd-step"] || props.dataAimdStep) as string
|
|
788
|
+
const level = Number.parseInt((props["data-aimd-level"] || props.dataAimdLevel) as string || "0", 10)
|
|
789
|
+
return {
|
|
790
|
+
...baseNode,
|
|
791
|
+
fieldType: "step",
|
|
792
|
+
level,
|
|
793
|
+
sequence: 0,
|
|
794
|
+
step: step || "1",
|
|
795
|
+
} as AimdNode
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Add fig-specific properties
|
|
799
|
+
if (fieldType === "fig") {
|
|
800
|
+
const figId = (props["data-aimd-fig-id"] || props.dataAimdFigId) as string
|
|
801
|
+
const figSrc = (props["data-aimd-fig-src"] || props.dataAimdFigSrc) as string
|
|
802
|
+
return {
|
|
803
|
+
...baseNode,
|
|
804
|
+
fieldType: "fig",
|
|
805
|
+
id: figId || id,
|
|
806
|
+
src: figSrc || "",
|
|
807
|
+
title: undefined,
|
|
808
|
+
legend: undefined,
|
|
809
|
+
} as AimdNode
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Add quiz-specific properties
|
|
813
|
+
if (fieldType === "quiz") {
|
|
814
|
+
const quizType = (props["data-aimd-quiz-type"] || props.dataAimdQuizType || "open") as string
|
|
815
|
+
const stem = (props["data-aimd-quiz-stem"] || props.dataAimdQuizStem || "") as string
|
|
816
|
+
return {
|
|
817
|
+
...baseNode,
|
|
818
|
+
fieldType: "quiz",
|
|
819
|
+
quizType: quizType as AimdQuizNode["quizType"],
|
|
820
|
+
stem,
|
|
821
|
+
} as AimdNode
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return baseNode as unknown as AimdNode
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Convert HAST element properties to Vue props
|
|
829
|
+
*/
|
|
830
|
+
function convertProperties(properties: Record<string, unknown>): Record<string, unknown> {
|
|
831
|
+
const props: Record<string, unknown> = {}
|
|
832
|
+
|
|
833
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
834
|
+
// Convert className to class
|
|
835
|
+
if (key === "className") {
|
|
836
|
+
props.class = Array.isArray(value) ? value.join(" ") : value
|
|
837
|
+
}
|
|
838
|
+
else if (key === "htmlFor") {
|
|
839
|
+
props.for = value
|
|
840
|
+
}
|
|
841
|
+
else if (key.startsWith("data")) {
|
|
842
|
+
// Convert camelCase data attributes to kebab-case
|
|
843
|
+
const dataKey = key.replace(/([A-Z])/g, "-$1").toLowerCase()
|
|
844
|
+
props[dataKey] = value
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
props[key] = value
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return props
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function isPromiseLike<T>(value: unknown): value is PromiseLike<T> {
|
|
855
|
+
return typeof value === "object" && value !== null && "then" in value
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
859
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function getReferencedVarDisplayValue(
|
|
863
|
+
value: RenderContext["value"] | undefined,
|
|
864
|
+
refTarget: string,
|
|
865
|
+
): string | null {
|
|
866
|
+
const fieldData = value?.var?.[refTarget]
|
|
867
|
+
const resolvedValue = isPlainObject(fieldData) && "value" in fieldData
|
|
868
|
+
? fieldData.value
|
|
869
|
+
: fieldData
|
|
870
|
+
|
|
871
|
+
if (resolvedValue === undefined || resolvedValue === null || resolvedValue === "") {
|
|
872
|
+
return null
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (Array.isArray(resolvedValue)) {
|
|
876
|
+
return resolvedValue.map(item => String(item)).join(", ")
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (isPlainObject(resolvedValue)) {
|
|
880
|
+
try {
|
|
881
|
+
return JSON.stringify(resolvedValue)
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
return null
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return String(resolvedValue)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Void elements that don't have children
|
|
893
|
+
*/
|
|
894
|
+
const VOID_ELEMENTS = new Set([
|
|
895
|
+
"img",
|
|
896
|
+
"br",
|
|
897
|
+
"hr",
|
|
898
|
+
"input",
|
|
899
|
+
"meta",
|
|
900
|
+
"link",
|
|
901
|
+
"area",
|
|
902
|
+
"base",
|
|
903
|
+
"col",
|
|
904
|
+
"embed",
|
|
905
|
+
"param",
|
|
906
|
+
"source",
|
|
907
|
+
"track",
|
|
908
|
+
"wbr",
|
|
909
|
+
])
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Convert HAST node to Vue VNode
|
|
913
|
+
*/
|
|
914
|
+
export function hastToVue(
|
|
915
|
+
node: HastRoot | RootContent,
|
|
916
|
+
options: VueRendererOptions = {},
|
|
917
|
+
figCtx?: FigureContext,
|
|
918
|
+
): VNodeChild {
|
|
919
|
+
const {
|
|
920
|
+
aimdRenderers = {},
|
|
921
|
+
elementRenderers = {},
|
|
922
|
+
componentMap = {},
|
|
923
|
+
} = options
|
|
924
|
+
const context = resolveRenderContext(options)
|
|
925
|
+
|
|
926
|
+
// Merge custom renderers with defaults
|
|
927
|
+
const mergedAimdRenderers = { ...defaultAimdRenderers, ...aimdRenderers }
|
|
928
|
+
|
|
929
|
+
// Initialize figure context on root
|
|
930
|
+
if (!figCtx && node.type === "root") {
|
|
931
|
+
figCtx = createFigureContext()
|
|
932
|
+
preprocessFigures(node, figCtx)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Handle root node
|
|
936
|
+
if (node.type === "root") {
|
|
937
|
+
const children = node.children
|
|
938
|
+
.map(child => hastToVue(child, options, figCtx))
|
|
939
|
+
.filter(Boolean)
|
|
940
|
+
return h(Fragment, null, children)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Handle text node
|
|
944
|
+
if (node.type === "text") {
|
|
945
|
+
return (node as HastText).value
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Handle element node
|
|
949
|
+
if (node.type === "element") {
|
|
950
|
+
const element = node as Element
|
|
951
|
+
const { tagName, properties = {}, children = [] } = element
|
|
952
|
+
|
|
953
|
+
// Check for AIMD node
|
|
954
|
+
const aimdType = properties["data-aimd-type"] || properties.dataAimdType
|
|
955
|
+
if (aimdType) {
|
|
956
|
+
// Try to get AIMD data from node.data first
|
|
957
|
+
let aimdData = (element.data as AimdElementData | undefined)?.aimd
|
|
958
|
+
|
|
959
|
+
// If not found, parse from properties
|
|
960
|
+
if (!aimdData) {
|
|
961
|
+
aimdData = parseAimdFromProps(properties as Record<string, unknown>)
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (aimdData) {
|
|
965
|
+
// Add figure sequence number if this is a fig node
|
|
966
|
+
if (aimdData.fieldType === "fig" && figCtx) {
|
|
967
|
+
const figId = aimdData.id
|
|
968
|
+
const sequence = figCtx.figureNumbers.get(figId)
|
|
969
|
+
if (sequence !== undefined) {
|
|
970
|
+
(aimdData as any).sequence = sequence
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Add figure number to ref_fig references
|
|
975
|
+
if (aimdData.fieldType === "ref_fig" && figCtx) {
|
|
976
|
+
const refTarget = aimdData.refTarget
|
|
977
|
+
const sequence = figCtx.figureNumbers.get(refTarget)
|
|
978
|
+
if (sequence !== undefined) {
|
|
979
|
+
(aimdData as any).figureNumber = sequence + 1
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const renderer = mergedAimdRenderers[aimdData.fieldType]
|
|
984
|
+
if (renderer) {
|
|
985
|
+
// Process children for container elements (step, check)
|
|
986
|
+
const childVNodes = children
|
|
987
|
+
.map(child => hastToVue(child, options, figCtx))
|
|
988
|
+
.filter(Boolean)
|
|
989
|
+
const result = renderer(aimdData, context, childVNodes)
|
|
990
|
+
if (isPromiseLike<VNode>(result)) {
|
|
991
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
992
|
+
console.warn(
|
|
993
|
+
`[aimd-renderer] Async renderer for "${aimdData.fieldType}" returned a Promise, `
|
|
994
|
+
+ `but hastToVue() is synchronous. The node will be skipped. `
|
|
995
|
+
+ `Use a synchronous renderer or handle async rendering at a higher level.`,
|
|
996
|
+
)
|
|
997
|
+
}
|
|
998
|
+
return null
|
|
999
|
+
}
|
|
1000
|
+
if (result) {
|
|
1001
|
+
return result
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Check for component map
|
|
1008
|
+
if (componentMap[tagName]) {
|
|
1009
|
+
const childVNodes = children
|
|
1010
|
+
.map(child => hastToVue(child, options, figCtx))
|
|
1011
|
+
.filter(Boolean)
|
|
1012
|
+
return h(componentMap[tagName], convertProperties(properties as Record<string, unknown>), childVNodes)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Check custom element renderer
|
|
1016
|
+
if (elementRenderers[tagName]) {
|
|
1017
|
+
const childVNodes = children
|
|
1018
|
+
.map(child => hastToVue(child, options, figCtx))
|
|
1019
|
+
.filter(Boolean)
|
|
1020
|
+
const rendered = elementRenderers[tagName](element, childVNodes, context)
|
|
1021
|
+
if (rendered !== null && rendered !== undefined) {
|
|
1022
|
+
return rendered
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Convert properties
|
|
1027
|
+
const vueProps = convertProperties(properties as Record<string, unknown>)
|
|
1028
|
+
|
|
1029
|
+
// Handle void elements
|
|
1030
|
+
if (VOID_ELEMENTS.has(tagName)) {
|
|
1031
|
+
return h(tagName, vueProps)
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Recursively process children
|
|
1035
|
+
const childVNodes = children
|
|
1036
|
+
.map(child => hastToVue(child, options, figCtx))
|
|
1037
|
+
.filter(Boolean)
|
|
1038
|
+
|
|
1039
|
+
return h(tagName, vueProps, childVNodes)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Handle comment node (ignore)
|
|
1043
|
+
if (node.type === "comment") {
|
|
1044
|
+
return null
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Handle doctype (ignore)
|
|
1048
|
+
if (node.type === "doctype") {
|
|
1049
|
+
return null
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Handle raw node
|
|
1053
|
+
if (node.type === "raw") {
|
|
1054
|
+
return h("span", {
|
|
1055
|
+
innerHTML: (node as any).value,
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return null
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Convert HAST tree to Vue VNode array
|
|
1064
|
+
*/
|
|
1065
|
+
export function renderToVNodes(
|
|
1066
|
+
tree: HastRoot,
|
|
1067
|
+
options: VueRendererOptions = {},
|
|
1068
|
+
): VNode[] {
|
|
1069
|
+
const result = hastToVue(tree, options)
|
|
1070
|
+
|
|
1071
|
+
if (result === null || result === undefined) {
|
|
1072
|
+
return []
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (Array.isArray(result)) {
|
|
1076
|
+
return result.filter((v): v is VNode => v !== null && typeof v === "object")
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (typeof result === "object" && "type" in result) {
|
|
1080
|
+
return [result as VNode]
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Wrap text node in span
|
|
1084
|
+
return [h("span", null, String(result))]
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Create a custom AIMD renderer that wraps a Vue component
|
|
1089
|
+
*/
|
|
1090
|
+
export function createComponentRenderer(
|
|
1091
|
+
component: Component,
|
|
1092
|
+
propsMapper?: (node: AimdNode, ctx: AimdRendererContext) => Record<string, unknown>,
|
|
1093
|
+
): AimdComponentRenderer {
|
|
1094
|
+
return (node, ctx, children) => {
|
|
1095
|
+
const props = propsMapper ? propsMapper(node, ctx) : { node, ctx }
|
|
1096
|
+
return h(component, props, children ? { default: () => children } : undefined)
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export function createStepCardRenderer(
|
|
1101
|
+
options: AimdStepCardRendererOptions = {},
|
|
1102
|
+
): AimdComponentRenderer {
|
|
1103
|
+
const {
|
|
1104
|
+
showScopeLabel = true,
|
|
1105
|
+
showCheckBadge = true,
|
|
1106
|
+
bodyClassName = "",
|
|
1107
|
+
className = "",
|
|
1108
|
+
} = options
|
|
1109
|
+
|
|
1110
|
+
return (node, ctx, children) => {
|
|
1111
|
+
const stepNode = node as AimdStepNode
|
|
1112
|
+
const stepLabel = stepNode.step || "?"
|
|
1113
|
+
const title = stepNode.title || stepNode.id
|
|
1114
|
+
const subtitle = stepNode.subtitle || ""
|
|
1115
|
+
const isResult = Boolean(stepNode.result)
|
|
1116
|
+
const hasCheck = Boolean(stepNode.check)
|
|
1117
|
+
const level = Number(stepNode.level || 1)
|
|
1118
|
+
const bodyChildren = normalizeStepCardBodyChildren(children)
|
|
1119
|
+
|
|
1120
|
+
const rootClasses = [
|
|
1121
|
+
"aimd-step-card",
|
|
1122
|
+
className,
|
|
1123
|
+
level > 1 ? `aimd-step-card--level-${level}` : "",
|
|
1124
|
+
isResult ? "aimd-step-card--result" : "",
|
|
1125
|
+
hasCheck ? "aimd-step-card--checkable" : "",
|
|
1126
|
+
].filter(Boolean)
|
|
1127
|
+
|
|
1128
|
+
return h("article", {
|
|
1129
|
+
class: rootClasses,
|
|
1130
|
+
"data-aimd-step-id": stepNode.id,
|
|
1131
|
+
"data-aimd-step-level": String(level),
|
|
1132
|
+
style: {
|
|
1133
|
+
display: "grid",
|
|
1134
|
+
gap: "14px",
|
|
1135
|
+
padding: level > 1 ? "16px 18px 16px 20px" : "18px 20px",
|
|
1136
|
+
margin: "12px 0",
|
|
1137
|
+
borderRadius: "18px",
|
|
1138
|
+
border: "1px solid rgba(26, 39, 31, 0.12)",
|
|
1139
|
+
background: isResult
|
|
1140
|
+
? "linear-gradient(180deg, rgba(255,248,233,0.98) 0%, rgba(255,252,245,0.98) 100%)"
|
|
1141
|
+
: "linear-gradient(180deg, rgba(248,252,249,0.98) 0%, rgba(255,255,255,0.98) 100%)",
|
|
1142
|
+
boxShadow: "0 18px 40px rgba(15, 31, 23, 0.08)",
|
|
1143
|
+
position: "relative",
|
|
1144
|
+
overflow: "hidden",
|
|
1145
|
+
},
|
|
1146
|
+
}, [
|
|
1147
|
+
h("header", {
|
|
1148
|
+
class: "aimd-step-card__header",
|
|
1149
|
+
style: {
|
|
1150
|
+
display: "flex",
|
|
1151
|
+
alignItems: "flex-start",
|
|
1152
|
+
justifyContent: "space-between",
|
|
1153
|
+
gap: "14px",
|
|
1154
|
+
},
|
|
1155
|
+
}, [
|
|
1156
|
+
h("div", {
|
|
1157
|
+
style: {
|
|
1158
|
+
display: "flex",
|
|
1159
|
+
gap: "14px",
|
|
1160
|
+
alignItems: "flex-start",
|
|
1161
|
+
minWidth: "0",
|
|
1162
|
+
},
|
|
1163
|
+
}, [
|
|
1164
|
+
h("div", {
|
|
1165
|
+
class: "aimd-step-card__badge",
|
|
1166
|
+
style: {
|
|
1167
|
+
minWidth: "42px",
|
|
1168
|
+
height: "42px",
|
|
1169
|
+
borderRadius: "14px",
|
|
1170
|
+
display: "inline-flex",
|
|
1171
|
+
alignItems: "center",
|
|
1172
|
+
justifyContent: "center",
|
|
1173
|
+
fontWeight: "700",
|
|
1174
|
+
fontSize: "14px",
|
|
1175
|
+
color: "#0d5139",
|
|
1176
|
+
background: hasCheck
|
|
1177
|
+
? "linear-gradient(180deg, rgba(212,242,227,1) 0%, rgba(184,233,208,1) 100%)"
|
|
1178
|
+
: "linear-gradient(180deg, rgba(232,241,236,1) 0%, rgba(216,232,222,1) 100%)",
|
|
1179
|
+
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.65)",
|
|
1180
|
+
flexShrink: 0,
|
|
1181
|
+
},
|
|
1182
|
+
}, stepLabel),
|
|
1183
|
+
h("div", {
|
|
1184
|
+
style: {
|
|
1185
|
+
display: "grid",
|
|
1186
|
+
gap: "6px",
|
|
1187
|
+
minWidth: "0",
|
|
1188
|
+
},
|
|
1189
|
+
}, [
|
|
1190
|
+
showScopeLabel
|
|
1191
|
+
? h("div", {
|
|
1192
|
+
style: {
|
|
1193
|
+
fontSize: "11px",
|
|
1194
|
+
fontWeight: "700",
|
|
1195
|
+
letterSpacing: "0.12em",
|
|
1196
|
+
textTransform: "uppercase",
|
|
1197
|
+
color: "#5d7066",
|
|
1198
|
+
},
|
|
1199
|
+
}, ctx.messages.scope.step)
|
|
1200
|
+
: null,
|
|
1201
|
+
h("div", {
|
|
1202
|
+
class: "aimd-step-card__title",
|
|
1203
|
+
style: {
|
|
1204
|
+
fontSize: "18px",
|
|
1205
|
+
fontWeight: "700",
|
|
1206
|
+
lineHeight: "1.2",
|
|
1207
|
+
color: "#1b2b22",
|
|
1208
|
+
wordBreak: "break-word",
|
|
1209
|
+
},
|
|
1210
|
+
}, title),
|
|
1211
|
+
subtitle
|
|
1212
|
+
? h("div", {
|
|
1213
|
+
class: "aimd-step-card__subtitle",
|
|
1214
|
+
style: {
|
|
1215
|
+
fontSize: "13px",
|
|
1216
|
+
lineHeight: "1.45",
|
|
1217
|
+
color: "#5c6f65",
|
|
1218
|
+
},
|
|
1219
|
+
}, subtitle)
|
|
1220
|
+
: null,
|
|
1221
|
+
]),
|
|
1222
|
+
]),
|
|
1223
|
+
h("div", {
|
|
1224
|
+
style: {
|
|
1225
|
+
display: "flex",
|
|
1226
|
+
gap: "8px",
|
|
1227
|
+
alignItems: "center",
|
|
1228
|
+
flexWrap: "wrap",
|
|
1229
|
+
justifyContent: "flex-end",
|
|
1230
|
+
},
|
|
1231
|
+
}, [
|
|
1232
|
+
showCheckBadge && hasCheck
|
|
1233
|
+
? h("span", {
|
|
1234
|
+
style: {
|
|
1235
|
+
display: "inline-flex",
|
|
1236
|
+
alignItems: "center",
|
|
1237
|
+
gap: "6px",
|
|
1238
|
+
padding: "6px 10px",
|
|
1239
|
+
borderRadius: "999px",
|
|
1240
|
+
background: "rgba(22, 114, 79, 0.10)",
|
|
1241
|
+
color: "#0d5139",
|
|
1242
|
+
fontSize: "11px",
|
|
1243
|
+
fontWeight: "700",
|
|
1244
|
+
letterSpacing: "0.04em",
|
|
1245
|
+
textTransform: "uppercase",
|
|
1246
|
+
},
|
|
1247
|
+
}, "Check")
|
|
1248
|
+
: null,
|
|
1249
|
+
isResult
|
|
1250
|
+
? h("span", {
|
|
1251
|
+
style: {
|
|
1252
|
+
display: "inline-flex",
|
|
1253
|
+
alignItems: "center",
|
|
1254
|
+
padding: "6px 10px",
|
|
1255
|
+
borderRadius: "999px",
|
|
1256
|
+
background: "rgba(181, 118, 0, 0.10)",
|
|
1257
|
+
color: "#8a4f00",
|
|
1258
|
+
fontSize: "11px",
|
|
1259
|
+
fontWeight: "700",
|
|
1260
|
+
letterSpacing: "0.04em",
|
|
1261
|
+
textTransform: "uppercase",
|
|
1262
|
+
},
|
|
1263
|
+
}, "Result")
|
|
1264
|
+
: null,
|
|
1265
|
+
]),
|
|
1266
|
+
]),
|
|
1267
|
+
bodyChildren.length > 0
|
|
1268
|
+
? h("div", {
|
|
1269
|
+
class: ["aimd-step-card__body", bodyClassName].filter(Boolean),
|
|
1270
|
+
style: {
|
|
1271
|
+
display: "grid",
|
|
1272
|
+
gap: "10px",
|
|
1273
|
+
color: "#24352c",
|
|
1274
|
+
fontSize: "14px",
|
|
1275
|
+
lineHeight: "1.75",
|
|
1276
|
+
},
|
|
1277
|
+
}, bodyChildren)
|
|
1278
|
+
: null,
|
|
1279
|
+
])
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Shiki highlighter type (compatible with @shikijs/core)
|
|
1285
|
+
*/
|
|
1286
|
+
export interface ShikiHighlighter {
|
|
1287
|
+
codeToHtml: (code: string, options: { lang: string, theme: string }) => string
|
|
1288
|
+
codeToTokensWithThemes?: (code: string, options: { lang: string, themes: Record<string, string> }) => Array<Array<{ content: string, variants: Record<string, { color: string }> }>>
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Create code block element renderer with Shiki support
|
|
1293
|
+
* @param highlighter - Shiki highlighter instance (can be reactive ref)
|
|
1294
|
+
* @param defaultTheme - Default theme to use
|
|
1295
|
+
*/
|
|
1296
|
+
export function createCodeBlockRenderer(
|
|
1297
|
+
highlighter: ShikiHighlighter | null | (() => ShikiHighlighter | null),
|
|
1298
|
+
defaultTheme = "github-dark",
|
|
1299
|
+
): ElementRenderer {
|
|
1300
|
+
return (node, children, ctx) => {
|
|
1301
|
+
// Find code element inside pre
|
|
1302
|
+
const codeNode = node.children.find(
|
|
1303
|
+
(child): child is Element => child.type === "element" && child.tagName === "code",
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
if (!codeNode) {
|
|
1307
|
+
return h("pre", {}, children)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Get language from class
|
|
1311
|
+
const className = codeNode.properties?.className
|
|
1312
|
+
let lang = "text"
|
|
1313
|
+
if (Array.isArray(className)) {
|
|
1314
|
+
const langClass = className.find(c => typeof c === "string" && c.startsWith("language-"))
|
|
1315
|
+
if (langClass && typeof langClass === "string") {
|
|
1316
|
+
lang = langClass.replace("language-", "")
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
else if (typeof className === "string" && className.startsWith("language-")) {
|
|
1320
|
+
lang = className.replace("language-", "")
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Get code content
|
|
1324
|
+
const codeContent = codeNode.children
|
|
1325
|
+
.map(child => (child.type === "text" ? child.value : ""))
|
|
1326
|
+
.join("")
|
|
1327
|
+
|
|
1328
|
+
// Get highlighter
|
|
1329
|
+
const hl = typeof highlighter === "function" ? highlighter() : highlighter
|
|
1330
|
+
|
|
1331
|
+
// Use Shiki if available
|
|
1332
|
+
if (hl) {
|
|
1333
|
+
try {
|
|
1334
|
+
const highlightedHtml = hl.codeToHtml(codeContent, {
|
|
1335
|
+
lang,
|
|
1336
|
+
theme: defaultTheme,
|
|
1337
|
+
})
|
|
1338
|
+
|
|
1339
|
+
return h("div", {
|
|
1340
|
+
"class": "shiki-code-block",
|
|
1341
|
+
"data-lang": lang,
|
|
1342
|
+
"innerHTML": highlightedHtml,
|
|
1343
|
+
})
|
|
1344
|
+
}
|
|
1345
|
+
catch (error) {
|
|
1346
|
+
console.error("Failed to highlight code:", error)
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Fallback: render without highlighting
|
|
1351
|
+
return h("pre", { class: `language-${lang}` }, h("code", { class: `language-${lang}` }, codeContent),
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Create Mermaid diagram element renderer
|
|
1358
|
+
* @param MermaidComponent - Vue component to render Mermaid diagrams
|
|
1359
|
+
*/
|
|
1360
|
+
export function createMermaidRenderer(
|
|
1361
|
+
MermaidComponent: Component,
|
|
1362
|
+
): ElementRenderer {
|
|
1363
|
+
return (node, children, ctx) => {
|
|
1364
|
+
// Find code element inside pre
|
|
1365
|
+
const codeNode = node.children.find(
|
|
1366
|
+
(child): child is Element => child.type === "element" && child.tagName === "code",
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
if (!codeNode) {
|
|
1370
|
+
return null
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Check if it's a mermaid block
|
|
1374
|
+
const className = codeNode.properties?.className
|
|
1375
|
+
const isMermaid = Array.isArray(className)
|
|
1376
|
+
? className.some(c => typeof c === "string" && c.includes("mermaid"))
|
|
1377
|
+
: typeof className === "string" && className.includes("mermaid")
|
|
1378
|
+
|
|
1379
|
+
if (!isMermaid) {
|
|
1380
|
+
return null // Not a mermaid block, use default rendering
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Get code content
|
|
1384
|
+
const codeContent = codeNode.children
|
|
1385
|
+
.map(child => (child.type === "text" ? child.value : ""))
|
|
1386
|
+
.join("")
|
|
1387
|
+
|
|
1388
|
+
return h(MermaidComponent, {
|
|
1389
|
+
code: codeContent,
|
|
1390
|
+
// Keep compatibility with MermaidBlock-style component APIs.
|
|
1391
|
+
attrs: {},
|
|
1392
|
+
})
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Asset resolver function type
|
|
1398
|
+
*/
|
|
1399
|
+
export type AssetResolver = (id: string) => Promise<{ url: string } | null>
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Create image element renderer with asset resolution
|
|
1403
|
+
* @param AssetComponent - Vue component to render assets (optional)
|
|
1404
|
+
* @param getAsset - Function to resolve asset ID to URL
|
|
1405
|
+
*/
|
|
1406
|
+
export function createAssetRenderer(
|
|
1407
|
+
getAsset?: AssetResolver,
|
|
1408
|
+
AssetComponent?: Component,
|
|
1409
|
+
): ElementRenderer {
|
|
1410
|
+
return (node, children, ctx) => {
|
|
1411
|
+
const { src, alt, title } = node.properties || {}
|
|
1412
|
+
|
|
1413
|
+
// If AssetComponent is provided, use it
|
|
1414
|
+
if (AssetComponent) {
|
|
1415
|
+
return h(AssetComponent, {
|
|
1416
|
+
src: src as string,
|
|
1417
|
+
alt: alt as string,
|
|
1418
|
+
title: title as string,
|
|
1419
|
+
getStaticResearchAssets: getAsset,
|
|
1420
|
+
})
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Default image rendering
|
|
1424
|
+
return h("img", {
|
|
1425
|
+
src: src as string,
|
|
1426
|
+
alt: alt as string,
|
|
1427
|
+
title: title as string,
|
|
1428
|
+
class: "aimd-image",
|
|
1429
|
+
loading: "lazy",
|
|
1430
|
+
})
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Create iframe/video element renderer for embedded content
|
|
1436
|
+
* @param EmbeddedComponent - Vue component to render embedded content
|
|
1437
|
+
*/
|
|
1438
|
+
export function createEmbeddedRenderer(
|
|
1439
|
+
EmbeddedComponent: Component,
|
|
1440
|
+
): ElementRenderer {
|
|
1441
|
+
return (node, children, ctx) => {
|
|
1442
|
+
const props = node.properties || {}
|
|
1443
|
+
|
|
1444
|
+
return h(EmbeddedComponent, {
|
|
1445
|
+
contentProps: props,
|
|
1446
|
+
component: node.tagName,
|
|
1447
|
+
})
|
|
1448
|
+
}
|
|
1449
|
+
}
|