@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,810 @@
|
|
|
1
|
+
import type { Element } from "hast"
|
|
2
|
+
/**
|
|
3
|
+
* Unified-based token renderer
|
|
4
|
+
* Provides compatibility layer for migrating from markdown-it tokenRenderer
|
|
5
|
+
*/
|
|
6
|
+
import type { Component, VNode } from "vue"
|
|
7
|
+
import type {
|
|
8
|
+
AimdCheckNode,
|
|
9
|
+
AimdQuizNode,
|
|
10
|
+
AimdStepNode,
|
|
11
|
+
AimdVarNode,
|
|
12
|
+
AimdVarTableNode,
|
|
13
|
+
} from "@airalogy/aimd-core/types"
|
|
14
|
+
import type { AimdNode, QuizPreviewOptions, RenderContext } from "@airalogy/aimd-core/types"
|
|
15
|
+
import type { ExtractedAimdFields } from "@airalogy/aimd-core/types"
|
|
16
|
+
import type { AimdRendererI18nOptions, AimdRendererMessages } from "../locales"
|
|
17
|
+
import type { AimdComponentRenderer, ElementRenderer, ShikiHighlighter, VueRendererOptions } from "../vue/vue-renderer"
|
|
18
|
+
import type { AimdRendererOptions, RenderResult } from "./processor"
|
|
19
|
+
import { resolveQuizPreviewOptions, type ResolvedQuizPreviewOptions } from "./quiz-preview"
|
|
20
|
+
import { h } from "vue"
|
|
21
|
+
import {
|
|
22
|
+
createAimdRendererMessages,
|
|
23
|
+
getAimdRendererQuizTypeLabel,
|
|
24
|
+
getAimdRendererScopeLabel,
|
|
25
|
+
resolveAimdRendererLocale,
|
|
26
|
+
} from "../locales"
|
|
27
|
+
import { parseAndExtract, renderToVue } from "./processor"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Token props interface (compatible with IAIMDItemProps)
|
|
31
|
+
*/
|
|
32
|
+
export interface TokenProps {
|
|
33
|
+
scope: string
|
|
34
|
+
prop: string
|
|
35
|
+
type?: string
|
|
36
|
+
value?: unknown
|
|
37
|
+
label?: string
|
|
38
|
+
disabled?: boolean
|
|
39
|
+
title?: string
|
|
40
|
+
required?: boolean
|
|
41
|
+
[key: string]: unknown
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Token-like object for getTokenProps compatibility
|
|
46
|
+
*/
|
|
47
|
+
export interface TokenLike {
|
|
48
|
+
meta?: {
|
|
49
|
+
node?: {
|
|
50
|
+
id: string
|
|
51
|
+
scope: string
|
|
52
|
+
type?: string
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Asset response interface
|
|
59
|
+
*/
|
|
60
|
+
export interface AssetResponse {
|
|
61
|
+
url: string
|
|
62
|
+
[key: string]: unknown
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render mode type
|
|
67
|
+
*/
|
|
68
|
+
export type RenderMode = "preview" | "edit" | "timeline" | "report"
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unified token renderer options
|
|
72
|
+
* Compatible with TokenRendererBaseOptions from markdown-it version
|
|
73
|
+
*/
|
|
74
|
+
export interface UnifiedTokenRendererOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Get props for a field from external data source
|
|
77
|
+
*/
|
|
78
|
+
getTokenProps?: (token: TokenLike) => Promise<TokenProps | null>
|
|
79
|
+
/**
|
|
80
|
+
* Get static research assets (images, files)
|
|
81
|
+
*/
|
|
82
|
+
getStaticResearchAssets?: (id: string) => Promise<AssetResponse | null>
|
|
83
|
+
/**
|
|
84
|
+
* Render mode
|
|
85
|
+
*/
|
|
86
|
+
mode: RenderMode | (() => RenderMode)
|
|
87
|
+
/**
|
|
88
|
+
* Shiki highlighter for code blocks
|
|
89
|
+
*/
|
|
90
|
+
highlighter?: ShikiHighlighter | null | (() => ShikiHighlighter | null)
|
|
91
|
+
/**
|
|
92
|
+
* Custom Vue components
|
|
93
|
+
*/
|
|
94
|
+
components?: {
|
|
95
|
+
AIMDItem?: Component
|
|
96
|
+
AIMDTag?: Component
|
|
97
|
+
AIMDStepRef?: Component
|
|
98
|
+
StepRenderer?: Component
|
|
99
|
+
CheckRenderer?: Component
|
|
100
|
+
PreviewRenderer?: Component
|
|
101
|
+
AssetRenderer?: Component
|
|
102
|
+
EmbeddedRenderer?: Component
|
|
103
|
+
MermaidBlock?: Component
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Quiz preview visibility policy
|
|
107
|
+
*/
|
|
108
|
+
quizPreview?: QuizPreviewOptions
|
|
109
|
+
/**
|
|
110
|
+
* Built-in renderer locale
|
|
111
|
+
*/
|
|
112
|
+
locale?: AimdRendererI18nOptions["locale"]
|
|
113
|
+
/**
|
|
114
|
+
* Optional overrides for built-in renderer copy
|
|
115
|
+
*/
|
|
116
|
+
messages?: AimdRendererI18nOptions["messages"]
|
|
117
|
+
/**
|
|
118
|
+
* Assigner block visibility policy in rendered output.
|
|
119
|
+
*/
|
|
120
|
+
assignerVisibility?: AimdRendererOptions["assignerVisibility"]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get scope display key
|
|
125
|
+
*/
|
|
126
|
+
function getScopeKey(scope: string): string {
|
|
127
|
+
return scope === "var_table" ? "table" : scope
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const BLANK_PLACEHOLDER_PATTERN = /\[\[([^\[\]\s]+)\]\]/g
|
|
131
|
+
|
|
132
|
+
function buildQuizStemChildren(
|
|
133
|
+
quizType: AimdQuizNode["quizType"],
|
|
134
|
+
stem: string,
|
|
135
|
+
): VNode[] {
|
|
136
|
+
if (quizType !== "blank") {
|
|
137
|
+
return [h("span", stem)]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const children: VNode[] = []
|
|
141
|
+
let lastIndex = 0
|
|
142
|
+
|
|
143
|
+
for (const match of stem.matchAll(BLANK_PLACEHOLDER_PATTERN)) {
|
|
144
|
+
const start = match.index ?? 0
|
|
145
|
+
const fullMatch = match[0]
|
|
146
|
+
const key = match[1]
|
|
147
|
+
|
|
148
|
+
if (start > lastIndex) {
|
|
149
|
+
children.push(h("span", stem.slice(lastIndex, start)))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
children.push(
|
|
153
|
+
h("span", {
|
|
154
|
+
class: "aimd-quiz__blank-placeholder",
|
|
155
|
+
"data-blank-key": key,
|
|
156
|
+
}, key),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
lastIndex = start + fullMatch.length
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (lastIndex < stem.length) {
|
|
163
|
+
children.push(h("span", stem.slice(lastIndex)))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (children.length === 0) {
|
|
167
|
+
children.push(h("span", stem))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return children
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatScaleOptionLabel(option: NonNullable<AimdQuizNode["options"]>[number]): string {
|
|
174
|
+
if (typeof option.points === "number" && Number.isFinite(option.points)) {
|
|
175
|
+
return `${option.text} (${option.points})`
|
|
176
|
+
}
|
|
177
|
+
return option.text
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildScalePreviewChildren(quizNode: AimdQuizNode): VNode[] {
|
|
181
|
+
if (!Array.isArray(quizNode.items) || quizNode.items.length === 0 || !Array.isArray(quizNode.options) || quizNode.options.length === 0) {
|
|
182
|
+
return [h("div", { class: "aimd-scale__empty" }, "Scale definition is incomplete.")]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (quizNode.display === "list") {
|
|
186
|
+
return [
|
|
187
|
+
h("div", { class: "aimd-scale__list" }, quizNode.items.map(item =>
|
|
188
|
+
h("div", { class: "aimd-scale__list-item" }, [
|
|
189
|
+
h("div", { class: "aimd-scale__item-stem" }, item.stem),
|
|
190
|
+
h("ul", { class: "aimd-scale__item-options" }, quizNode.options!.map(option =>
|
|
191
|
+
h("li", formatScaleOptionLabel(option)),
|
|
192
|
+
)),
|
|
193
|
+
]),
|
|
194
|
+
)),
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [
|
|
199
|
+
h("table", { class: "aimd-scale__table" }, [
|
|
200
|
+
h("thead", [
|
|
201
|
+
h("tr", [
|
|
202
|
+
h("th", { class: "aimd-scale__item-header" }, "Item"),
|
|
203
|
+
...quizNode.options.map(option => h("th", formatScaleOptionLabel(option))),
|
|
204
|
+
]),
|
|
205
|
+
]),
|
|
206
|
+
h("tbody", quizNode.items.map(item =>
|
|
207
|
+
h("tr", [
|
|
208
|
+
h("th", { class: "aimd-scale__item-stem", scope: "row" }, item.stem),
|
|
209
|
+
...quizNode.options!.map(() => h("td", { class: "aimd-scale__cell" }, "○")),
|
|
210
|
+
]),
|
|
211
|
+
)),
|
|
212
|
+
]),
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildScaleBandChildren(quizNode: AimdQuizNode): VNode[] {
|
|
217
|
+
const bands = Array.isArray((quizNode.grading as any)?.bands) ? (quizNode.grading as any).bands : []
|
|
218
|
+
if (bands.length === 0) {
|
|
219
|
+
return []
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return [
|
|
223
|
+
h("ul", { class: "aimd-scale__bands" }, bands.map((band: any) =>
|
|
224
|
+
h("li", `${band.min}-${band.max}: ${band.label}${band.interpretation ? ` · ${band.interpretation}` : ""}`),
|
|
225
|
+
)),
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Render preview tag for AIMD field
|
|
231
|
+
*/
|
|
232
|
+
function renderPreviewTag(
|
|
233
|
+
scope: string,
|
|
234
|
+
id: string,
|
|
235
|
+
messages: AimdRendererMessages,
|
|
236
|
+
columns?: string[],
|
|
237
|
+
): VNode {
|
|
238
|
+
const scopeKey = getScopeKey(scope)
|
|
239
|
+
const scopeLabel = getAimdRendererScopeLabel(scope, messages)
|
|
240
|
+
|
|
241
|
+
// var_table: render tag with table preview inside
|
|
242
|
+
if (scope === "var_table") {
|
|
243
|
+
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
|
+
]),
|
|
248
|
+
]
|
|
249
|
+
// Add table preview inside the container
|
|
250
|
+
if (columns && columns.length > 0) {
|
|
251
|
+
children.push(
|
|
252
|
+
h("table", { class: "aimd-field__table-preview" }, [
|
|
253
|
+
h("thead", [
|
|
254
|
+
h("tr", columns.map(col => h("th", col))),
|
|
255
|
+
]),
|
|
256
|
+
h("tbody", [
|
|
257
|
+
h("tr", columns.map(() => h("td", "..."))),
|
|
258
|
+
]),
|
|
259
|
+
]),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
return h("div", {
|
|
263
|
+
"class": "aimd-field aimd-field--var-table",
|
|
264
|
+
"data-aimd-type": "var_table",
|
|
265
|
+
"data-aimd-id": id,
|
|
266
|
+
}, children)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const classSuffix = scopeKey === "table" ? "var-table" : scopeKey
|
|
270
|
+
|
|
271
|
+
return h("span", {
|
|
272
|
+
"class": `aimd-field aimd-field--${classSuffix}`,
|
|
273
|
+
"data-aimd-type": scopeKey,
|
|
274
|
+
"data-aimd-id": id,
|
|
275
|
+
}, [
|
|
276
|
+
h("span", { class: "aimd-field__scope" }, scopeLabel),
|
|
277
|
+
h("span", { class: "aimd-field__name" }, id),
|
|
278
|
+
])
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Create AIMD renderers based on options
|
|
283
|
+
*/
|
|
284
|
+
function createAimdRenderers(options: UnifiedTokenRendererOptions): Record<string, AimdComponentRenderer> {
|
|
285
|
+
const { getTokenProps, mode, components = {} } = options
|
|
286
|
+
const messages = createAimdRendererMessages(options.locale, options.messages)
|
|
287
|
+
const {
|
|
288
|
+
AIMDItem,
|
|
289
|
+
AIMDTag,
|
|
290
|
+
AIMDStepRef,
|
|
291
|
+
StepRenderer,
|
|
292
|
+
CheckRenderer,
|
|
293
|
+
PreviewRenderer,
|
|
294
|
+
} = components
|
|
295
|
+
|
|
296
|
+
const getMode = (): RenderMode => typeof mode === "function" ? mode() : mode
|
|
297
|
+
const isPreview = () => getMode() === "preview"
|
|
298
|
+
const getQuizPreview = (): ResolvedQuizPreviewOptions =>
|
|
299
|
+
resolveQuizPreviewOptions(getMode(), options.quizPreview)
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
var: async (node, ctx, children) => {
|
|
303
|
+
const varNode = node as AimdVarNode
|
|
304
|
+
const { id, scope } = varNode
|
|
305
|
+
|
|
306
|
+
if (isPreview()) {
|
|
307
|
+
if (PreviewRenderer) {
|
|
308
|
+
return h(PreviewRenderer, { type: "var" }, {
|
|
309
|
+
default: () => children,
|
|
310
|
+
name: () => id,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
return renderPreviewTag(scope, id, messages)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Edit mode
|
|
317
|
+
if (getTokenProps && AIMDItem) {
|
|
318
|
+
const item = await getTokenProps({ meta: { node: { id, scope } } })
|
|
319
|
+
if (item) {
|
|
320
|
+
return h("span", {
|
|
321
|
+
"class": "aimd-field-wrapper aimd-field-wrapper--inline",
|
|
322
|
+
"id": `${scope}-${id}`,
|
|
323
|
+
"data-has-variable": "true",
|
|
324
|
+
}, [h(AIMDItem, item)])
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return renderPreviewTag(scope, id, messages)
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
var_table: async (node, ctx, children) => {
|
|
332
|
+
const tableNode = node as AimdVarTableNode
|
|
333
|
+
const { id, scope, columns } = tableNode
|
|
334
|
+
|
|
335
|
+
if (isPreview()) {
|
|
336
|
+
if (PreviewRenderer) {
|
|
337
|
+
return h(PreviewRenderer, { type: "var_table" }, {
|
|
338
|
+
default: () => children,
|
|
339
|
+
name: () => id,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
// Preview mode: render inline tag with columns info
|
|
343
|
+
return renderPreviewTag(scope, id, messages, columns)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Edit mode
|
|
347
|
+
if (getTokenProps && AIMDTag) {
|
|
348
|
+
const item = await getTokenProps({ meta: { node: { id, scope } } })
|
|
349
|
+
return h(AIMDTag, { ...item, props: columns })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return renderPreviewTag(scope, id, messages, columns)
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
quiz: async (node, ctx, children) => {
|
|
356
|
+
const quizNode = node as AimdQuizNode
|
|
357
|
+
const { id, scope, quizType, stem, score } = quizNode
|
|
358
|
+
const typeLabel = getAimdRendererQuizTypeLabel(quizType, quizNode.mode, messages)
|
|
359
|
+
|
|
360
|
+
if (isPreview()) {
|
|
361
|
+
if (PreviewRenderer) {
|
|
362
|
+
return h(PreviewRenderer, { type: "quiz" }, {
|
|
363
|
+
default: () => children,
|
|
364
|
+
name: () => id,
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
const previewChildren: VNode[] = [
|
|
368
|
+
h("div", { class: "aimd-quiz__meta" }, [
|
|
369
|
+
h("span", { class: "aimd-field__scope" }, getAimdRendererScopeLabel(scope, messages)),
|
|
370
|
+
h("span", { class: "aimd-field__name" }, id),
|
|
371
|
+
h("span", { class: "aimd-field__type" }, `(${typeLabel})`),
|
|
372
|
+
score !== undefined ? h("span", { class: "aimd-quiz__score" }, messages.quiz.score(score)) : null,
|
|
373
|
+
]),
|
|
374
|
+
h("div", { class: "aimd-quiz__stem" }, buildQuizStemChildren(quizType, stem || id)),
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
if (typeof quizNode.title === "string" && quizNode.title.trim()) {
|
|
378
|
+
previewChildren.splice(1, 0, h("div", { class: "aimd-quiz__title" }, quizNode.title))
|
|
379
|
+
}
|
|
380
|
+
if (typeof quizNode.description === "string" && quizNode.description.trim()) {
|
|
381
|
+
previewChildren.push(h("div", { class: "aimd-quiz__description" }, quizNode.description))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (quizType === "choice" && Array.isArray(quizNode.options) && quizNode.options.length > 0) {
|
|
385
|
+
previewChildren.push(
|
|
386
|
+
h("ul", { class: "aimd-quiz__options" }, quizNode.options.map(opt =>
|
|
387
|
+
h("li", `${opt.key}. ${opt.text}`),
|
|
388
|
+
)),
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (quizType === "scale") {
|
|
393
|
+
previewChildren.push(...buildScalePreviewChildren(quizNode))
|
|
394
|
+
previewChildren.push(...buildScaleBandChildren(quizNode))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const quizPreview = getQuizPreview()
|
|
398
|
+
|
|
399
|
+
if (quizPreview.showAnswers && quizType === "choice" && quizNode.answer !== undefined) {
|
|
400
|
+
const answerText = Array.isArray(quizNode.answer)
|
|
401
|
+
? quizNode.answer.join(", ")
|
|
402
|
+
: String(quizNode.answer)
|
|
403
|
+
if (answerText.trim()) {
|
|
404
|
+
previewChildren.push(
|
|
405
|
+
h("div", { class: "aimd-quiz__answer" }, messages.quiz.answer(answerText)),
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (quizPreview.showAnswers && quizType === "blank" && Array.isArray(quizNode.blanks) && quizNode.blanks.length > 0) {
|
|
411
|
+
previewChildren.push(
|
|
412
|
+
h("ul", { class: "aimd-quiz__blanks" }, quizNode.blanks.map(blank =>
|
|
413
|
+
h("li", `${blank.key}: ${blank.answer}`),
|
|
414
|
+
)),
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (quizPreview.showRubric && quizType === "open" && typeof quizNode.rubric === "string" && quizNode.rubric.trim()) {
|
|
419
|
+
previewChildren.push(
|
|
420
|
+
h("div", { class: "aimd-quiz__rubric" }, messages.quiz.rubric(quizNode.rubric)),
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return h("div", {
|
|
425
|
+
"class": "aimd-field aimd-field--quiz",
|
|
426
|
+
"data-aimd-type": "quiz",
|
|
427
|
+
"data-aimd-id": id,
|
|
428
|
+
}, previewChildren)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Edit mode
|
|
432
|
+
if (getTokenProps && AIMDItem) {
|
|
433
|
+
const item = await getTokenProps({ meta: { node: { id, scope } } })
|
|
434
|
+
if (item) {
|
|
435
|
+
return h("span", {
|
|
436
|
+
"class": "aimd-field-wrapper aimd-field-wrapper--inline",
|
|
437
|
+
"id": `${scope}-${id}`,
|
|
438
|
+
"data-has-variable": "true",
|
|
439
|
+
}, [h(AIMDItem, item)])
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return h("div", {
|
|
444
|
+
"class": "aimd-field aimd-field--quiz",
|
|
445
|
+
"data-aimd-type": "quiz",
|
|
446
|
+
"data-aimd-id": id,
|
|
447
|
+
}, [
|
|
448
|
+
h("div", { class: "aimd-quiz__meta" }, [
|
|
449
|
+
h("span", { class: "aimd-field__scope" }, getAimdRendererScopeLabel(scope, messages)),
|
|
450
|
+
h("span", { class: "aimd-field__name" }, id),
|
|
451
|
+
h("span", { class: "aimd-field__type" }, `(${typeLabel})`),
|
|
452
|
+
]),
|
|
453
|
+
...(typeof quizNode.title === "string" && quizNode.title.trim()
|
|
454
|
+
? [h("div", { class: "aimd-quiz__title" }, quizNode.title)]
|
|
455
|
+
: []),
|
|
456
|
+
h("div", { class: "aimd-quiz__stem" }, buildQuizStemChildren(quizType, stem || id)),
|
|
457
|
+
...(typeof quizNode.description === "string" && quizNode.description.trim()
|
|
458
|
+
? [h("div", { class: "aimd-quiz__description" }, quizNode.description)]
|
|
459
|
+
: []),
|
|
460
|
+
...(quizType === "scale" ? [...buildScalePreviewChildren(quizNode), ...buildScaleBandChildren(quizNode)] : []),
|
|
461
|
+
])
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
step: async (node, ctx, children) => {
|
|
465
|
+
const stepNode = node as AimdStepNode
|
|
466
|
+
const { id, scope, step, check } = stepNode
|
|
467
|
+
|
|
468
|
+
if (isPreview()) {
|
|
469
|
+
if (PreviewRenderer) {
|
|
470
|
+
return h(PreviewRenderer, { type: "step" }, {
|
|
471
|
+
default: () => children,
|
|
472
|
+
name: () => id,
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
return h("span", { class: "research-step__sequence" }, messages.step.sequence(step))
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Edit mode
|
|
479
|
+
if (getTokenProps && StepRenderer) {
|
|
480
|
+
const item = await getTokenProps({ meta: { node: { id, scope } } })
|
|
481
|
+
const annotationItem = await getTokenProps({ meta: { node: { id, scope, type: "step-annotation" } } })
|
|
482
|
+
|
|
483
|
+
return h(StepRenderer, {
|
|
484
|
+
item,
|
|
485
|
+
annotationItem,
|
|
486
|
+
name: id,
|
|
487
|
+
step: String(step),
|
|
488
|
+
check,
|
|
489
|
+
}, {
|
|
490
|
+
default: () => children,
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return h("span", { class: "research-step__sequence" }, messages.step.sequence(step))
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
check: async (node, ctx, children) => {
|
|
498
|
+
const checkNode = node as AimdCheckNode
|
|
499
|
+
const { id, scope, label } = checkNode
|
|
500
|
+
|
|
501
|
+
if (isPreview()) {
|
|
502
|
+
if (PreviewRenderer) {
|
|
503
|
+
return h(PreviewRenderer, { type: "check" }, {
|
|
504
|
+
default: () => children,
|
|
505
|
+
name: () => id,
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
return renderPreviewTag(scope, id, messages)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Edit mode
|
|
512
|
+
if (getTokenProps && CheckRenderer) {
|
|
513
|
+
const item = await getTokenProps({ meta: { node: { id, scope } } })
|
|
514
|
+
const annotationItem = await getTokenProps({ meta: { node: { id, scope, type: "check-annotation" } } })
|
|
515
|
+
|
|
516
|
+
return h(CheckRenderer, {
|
|
517
|
+
item,
|
|
518
|
+
annotationItem,
|
|
519
|
+
name: id,
|
|
520
|
+
}, {
|
|
521
|
+
default: () => children,
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return renderPreviewTag(scope, id, messages)
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
ref_step: (node, ctx) => {
|
|
529
|
+
const { id } = node
|
|
530
|
+
const refTarget = "refTarget" in node ? node.refTarget : id
|
|
531
|
+
const stepSequence = "stepSequence" in node && typeof (node as any).stepSequence === "string"
|
|
532
|
+
? (node as any).stepSequence
|
|
533
|
+
: undefined
|
|
534
|
+
const displayText = stepSequence ? messages.step.reference(stepSequence) : refTarget
|
|
535
|
+
|
|
536
|
+
if (AIMDStepRef) {
|
|
537
|
+
return h(AIMDStepRef, { name: refTarget, type: "step", stepSequence })
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return h("a", {
|
|
541
|
+
class: "aimd-ref aimd-ref--step",
|
|
542
|
+
href: `#step-${refTarget}`,
|
|
543
|
+
"data-aimd-step-sequence": stepSequence,
|
|
544
|
+
title: refTarget,
|
|
545
|
+
}, [
|
|
546
|
+
h("span", { class: "aimd-ref__content" }, [
|
|
547
|
+
h("span", { class: "aimd-field aimd-field--step aimd-field--readonly" }, [
|
|
548
|
+
h("span", { class: "research-step__sequence" }, displayText),
|
|
549
|
+
]),
|
|
550
|
+
]),
|
|
551
|
+
])
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
ref_var: (node, ctx) => {
|
|
555
|
+
const { id } = node
|
|
556
|
+
const refTarget = "refTarget" in node ? node.refTarget : id
|
|
557
|
+
const referencedValue = ctx.mode === "edit" ? getReferencedVarDisplayValue(ctx.value, refTarget) : null
|
|
558
|
+
|
|
559
|
+
if (AIMDStepRef) {
|
|
560
|
+
return h(AIMDStepRef, {
|
|
561
|
+
name: refTarget,
|
|
562
|
+
type: "var",
|
|
563
|
+
contextValue: ctx.value,
|
|
564
|
+
displayValue: referencedValue ?? undefined,
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (ctx.mode !== "edit") {
|
|
569
|
+
return h("a", {
|
|
570
|
+
class: "aimd-ref aimd-ref--var",
|
|
571
|
+
href: `#var-${refTarget}`,
|
|
572
|
+
title: refTarget,
|
|
573
|
+
}, [
|
|
574
|
+
h("span", { class: "aimd-ref__icon" }, "📌"),
|
|
575
|
+
h("span", { class: "aimd-ref__name" }, refTarget),
|
|
576
|
+
])
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return h("span", {
|
|
580
|
+
class: "aimd-ref aimd-ref--var",
|
|
581
|
+
"data-aimd-ref": refTarget,
|
|
582
|
+
title: refTarget,
|
|
583
|
+
}, [
|
|
584
|
+
h("span", { class: "aimd-ref__content" }, [
|
|
585
|
+
referencedValue !== null
|
|
586
|
+
? h("span", {
|
|
587
|
+
class: "aimd-field aimd-field--var aimd-field--readonly",
|
|
588
|
+
"data-aimd-id": refTarget,
|
|
589
|
+
"data-aimd-scope": "var",
|
|
590
|
+
}, [
|
|
591
|
+
h("span", { class: "aimd-field__value" }, referencedValue),
|
|
592
|
+
])
|
|
593
|
+
: h("span", { class: "aimd-field aimd-field--var" }, [
|
|
594
|
+
h("span", { class: "aimd-field__scope" }, messages.scope.var),
|
|
595
|
+
h("span", { class: "aimd-field__name" }, refTarget),
|
|
596
|
+
]),
|
|
597
|
+
]),
|
|
598
|
+
])
|
|
599
|
+
},
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
604
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function getReferencedVarDisplayValue(
|
|
608
|
+
value: RenderContext["value"] | undefined,
|
|
609
|
+
refTarget: string,
|
|
610
|
+
): string | null {
|
|
611
|
+
const fieldData = value?.var?.[refTarget]
|
|
612
|
+
const resolvedValue = isPlainObject(fieldData) && "value" in fieldData
|
|
613
|
+
? fieldData.value
|
|
614
|
+
: fieldData
|
|
615
|
+
|
|
616
|
+
if (resolvedValue === undefined || resolvedValue === null || resolvedValue === "") {
|
|
617
|
+
return null
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (Array.isArray(resolvedValue)) {
|
|
621
|
+
return resolvedValue.map(item => String(item)).join(", ")
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (isPlainObject(resolvedValue)) {
|
|
625
|
+
try {
|
|
626
|
+
return JSON.stringify(resolvedValue)
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return String(resolvedValue)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Create element renderers based on options
|
|
638
|
+
*/
|
|
639
|
+
function createElementRenderers(options: UnifiedTokenRendererOptions): Record<string, ElementRenderer> {
|
|
640
|
+
const { getStaticResearchAssets, highlighter, components = {} } = options
|
|
641
|
+
const { AssetRenderer, EmbeddedRenderer, MermaidBlock } = components
|
|
642
|
+
|
|
643
|
+
const renderers: Record<string, ElementRenderer> = {}
|
|
644
|
+
|
|
645
|
+
// Image renderer
|
|
646
|
+
if (AssetRenderer || getStaticResearchAssets) {
|
|
647
|
+
renderers.img = (node, children, ctx) => {
|
|
648
|
+
const { src, alt } = node.properties || {}
|
|
649
|
+
|
|
650
|
+
if (AssetRenderer) {
|
|
651
|
+
return h(AssetRenderer, {
|
|
652
|
+
src: src as string,
|
|
653
|
+
alt: alt as string,
|
|
654
|
+
getStaticResearchAssets,
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return h("img", { src, alt, class: "aimd-image", loading: "lazy" })
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Code block renderer with Mermaid support
|
|
663
|
+
renderers.pre = (node, children, ctx) => {
|
|
664
|
+
const codeNode = node.children.find(
|
|
665
|
+
(child): child is Element => child.type === "element" && child.tagName === "code",
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if (!codeNode) {
|
|
669
|
+
return h("pre", {}, children)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Get language
|
|
673
|
+
const className = codeNode.properties?.className
|
|
674
|
+
let lang = "text"
|
|
675
|
+
if (Array.isArray(className)) {
|
|
676
|
+
const langClass = className.find(c => typeof c === "string" && c.startsWith("language-"))
|
|
677
|
+
if (langClass && typeof langClass === "string") {
|
|
678
|
+
lang = langClass.replace("language-", "")
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Get code content
|
|
683
|
+
const codeContent = codeNode.children
|
|
684
|
+
.map(child => (child.type === "text" ? child.value : ""))
|
|
685
|
+
.join("")
|
|
686
|
+
|
|
687
|
+
// Check for Mermaid
|
|
688
|
+
const firstLine = codeContent.split(/\n/)[0].trim()
|
|
689
|
+
const isMermaid = lang === "mermaid"
|
|
690
|
+
|| firstLine === "gantt"
|
|
691
|
+
|| firstLine === "sequenceDiagram"
|
|
692
|
+
|| /^graph (?:TB|BT|RL|LR|TD);?$/.test(firstLine)
|
|
693
|
+
|
|
694
|
+
if (isMermaid && MermaidBlock) {
|
|
695
|
+
return h(MermaidBlock, { code: codeContent })
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Use Shiki if available
|
|
699
|
+
const hl = typeof highlighter === "function" ? highlighter() : highlighter
|
|
700
|
+
if (hl) {
|
|
701
|
+
try {
|
|
702
|
+
const highlightedHtml = hl.codeToHtml(codeContent, {
|
|
703
|
+
lang,
|
|
704
|
+
theme: "github-dark",
|
|
705
|
+
})
|
|
706
|
+
return h("div", {
|
|
707
|
+
"class": "shiki-code-block",
|
|
708
|
+
"data-lang": lang,
|
|
709
|
+
"innerHTML": highlightedHtml,
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
console.error("Failed to highlight code:", error)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Fallback
|
|
718
|
+
return h("pre", { class: `language-${lang}` }, h("code", { class: `language-${lang}` }, codeContent),
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Iframe renderer
|
|
723
|
+
if (EmbeddedRenderer) {
|
|
724
|
+
renderers.iframe = (node, children, ctx) => {
|
|
725
|
+
return h(EmbeddedRenderer, {
|
|
726
|
+
contentProps: { ...node.properties, credentialless: true },
|
|
727
|
+
component: "iframe",
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Video renderer
|
|
732
|
+
renderers.video = (node, children, ctx) => {
|
|
733
|
+
return h(EmbeddedRenderer, {
|
|
734
|
+
contentProps: node.properties,
|
|
735
|
+
component: "video",
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return renderers
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Unified token renderer context
|
|
745
|
+
* Compatible interface for migration from markdown-it
|
|
746
|
+
*/
|
|
747
|
+
export interface UnifiedRendererContext {
|
|
748
|
+
/**
|
|
749
|
+
* Render markdown/AIMD content to Vue VNodes
|
|
750
|
+
*/
|
|
751
|
+
render: (content: string) => Promise<RenderResult>
|
|
752
|
+
/**
|
|
753
|
+
* Extract fields from content
|
|
754
|
+
*/
|
|
755
|
+
extractFields: (content: string) => ExtractedAimdFields
|
|
756
|
+
/**
|
|
757
|
+
* Vue renderer options (for use with renderToVue)
|
|
758
|
+
*/
|
|
759
|
+
vueOptions: VueRendererOptions
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Create unified-based token renderer
|
|
764
|
+
* Provides API compatible with createDefaultTokenRenderer from markdown-it version
|
|
765
|
+
*/
|
|
766
|
+
export function createUnifiedTokenRenderer(options: UnifiedTokenRendererOptions): UnifiedRendererContext {
|
|
767
|
+
const getMode = (): RenderMode => typeof options.mode === "function" ? options.mode() : options.mode
|
|
768
|
+
const getQuizPreview = (): ResolvedQuizPreviewOptions =>
|
|
769
|
+
resolveQuizPreviewOptions(getMode(), options.quizPreview)
|
|
770
|
+
const locale = resolveAimdRendererLocale(options.locale)
|
|
771
|
+
const messages = createAimdRendererMessages(locale, options.messages)
|
|
772
|
+
|
|
773
|
+
const aimdRenderers = createAimdRenderers(options)
|
|
774
|
+
const elementRenderers = createElementRenderers(options)
|
|
775
|
+
|
|
776
|
+
const vueOptions: VueRendererOptions = {
|
|
777
|
+
locale,
|
|
778
|
+
messages,
|
|
779
|
+
context: {
|
|
780
|
+
mode: getMode() === "timeline" ? "preview" : getMode() as "preview" | "edit" | "report",
|
|
781
|
+
readonly: getMode() === "preview",
|
|
782
|
+
quizPreview: getQuizPreview(),
|
|
783
|
+
locale,
|
|
784
|
+
messages,
|
|
785
|
+
},
|
|
786
|
+
aimdRenderers,
|
|
787
|
+
elementRenderers,
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
async render(content: string): Promise<RenderResult> {
|
|
792
|
+
return renderToVue(content, {
|
|
793
|
+
gfm: true,
|
|
794
|
+
math: true,
|
|
795
|
+
breaks: true,
|
|
796
|
+
assignerVisibility: options.assignerVisibility,
|
|
797
|
+
...vueOptions,
|
|
798
|
+
})
|
|
799
|
+
},
|
|
800
|
+
|
|
801
|
+
extractFields(content: string): ExtractedAimdFields {
|
|
802
|
+
return parseAndExtract(content)
|
|
803
|
+
},
|
|
804
|
+
|
|
805
|
+
vueOptions,
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Re-export types for convenience
|
|
810
|
+
export type { ExtractedAimdFields, RenderResult, VueRendererOptions }
|