@airalogy/aimd-renderer 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/README.zh-CN.md +38 -0
- package/dist/aimd-renderer.css +1 -1
- package/dist/common/criticMarkup.d.ts +10 -0
- package/dist/common/criticMarkup.d.ts.map +1 -0
- package/dist/common/processor.d.ts.map +1 -1
- package/dist/html.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +80 -73
- package/dist/{processor-CHbNEcN8.js → processor-BOCQYqXE.js} +1899 -1573
- package/dist/readonly-record-renderer-CkzY7UvT.js +711 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/readonly-record-renderer.d.ts +42 -0
- package/dist/vue/readonly-record-renderer.d.ts.map +1 -0
- package/dist/vue.js +20 -13
- package/package.json +2 -2
- package/src/__tests__/renderer.test.ts +244 -1
- package/src/common/criticMarkup.ts +97 -0
- package/src/common/processor.ts +13 -2
- package/src/index.ts +18 -0
- package/src/styles/katex.css +65 -0
- package/src/vue/index.ts +18 -0
- package/src/vue/readonly-record-renderer.ts +747 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AimdCheckNode,
|
|
3
|
+
AimdFigNode,
|
|
4
|
+
AimdNode,
|
|
5
|
+
AimdQuizNode,
|
|
6
|
+
AimdVarNode,
|
|
7
|
+
AimdVarTableNode,
|
|
8
|
+
RenderContext,
|
|
9
|
+
} from '@airalogy/aimd-core/types'
|
|
10
|
+
import {
|
|
11
|
+
AIMD_FILE_BADGE_BY_KIND,
|
|
12
|
+
AIMD_FILE_REFERENCE_VALUE_KEYS,
|
|
13
|
+
AIMD_RECORD_DATA_SCOPES,
|
|
14
|
+
getAimdAssetMediaSource,
|
|
15
|
+
getAimdDisplayValue,
|
|
16
|
+
getAimdFieldDisplayLabel,
|
|
17
|
+
getAimdFileDisplayName,
|
|
18
|
+
getAimdFileValueId,
|
|
19
|
+
inferAimdAssetKind,
|
|
20
|
+
isAimdAiralogyFileId,
|
|
21
|
+
isAimdBooleanType,
|
|
22
|
+
isAimdCodeType,
|
|
23
|
+
isAimdDnaType,
|
|
24
|
+
isAimdFileLikeType,
|
|
25
|
+
isAimdMarkdownType,
|
|
26
|
+
isAimdPlainRecord,
|
|
27
|
+
normalizeAimdRecordDataValue,
|
|
28
|
+
normalizeAimdString,
|
|
29
|
+
stringifyAimdDisplayValue,
|
|
30
|
+
toAimdBooleanValue,
|
|
31
|
+
type AimdAssetKind,
|
|
32
|
+
type AimdAssetLike,
|
|
33
|
+
type AimdRecordDataScope,
|
|
34
|
+
type AimdRecordDataValue,
|
|
35
|
+
} from '@airalogy/aimd-core/utils'
|
|
36
|
+
import { defineComponent, h, ref, shallowRef, watch, type PropType, type VNode, type VNodeChild } from 'vue'
|
|
37
|
+
|
|
38
|
+
import type { AimdRendererOptions, RenderResult } from '../common/processor'
|
|
39
|
+
import { renderToVue } from '../common/processor'
|
|
40
|
+
import type { AimdComponentRenderer, AimdRendererContext, ElementRenderer, VueRendererOptions } from './vue-renderer'
|
|
41
|
+
|
|
42
|
+
export const AIMD_RECORD_RENDER_SCOPES = AIMD_RECORD_DATA_SCOPES
|
|
43
|
+
|
|
44
|
+
export type AimdRecordRenderScope = AimdRecordDataScope
|
|
45
|
+
|
|
46
|
+
export type ReadonlyRecordAssetKind = AimdAssetKind
|
|
47
|
+
|
|
48
|
+
export type AimdRecordRenderValue = AimdRecordDataValue
|
|
49
|
+
|
|
50
|
+
export interface ReadonlyRecordAsset extends AimdAssetLike {
|
|
51
|
+
url?: string
|
|
52
|
+
href?: string
|
|
53
|
+
name?: string
|
|
54
|
+
filename?: string
|
|
55
|
+
mimeType?: string
|
|
56
|
+
size?: number
|
|
57
|
+
kind?: ReadonlyRecordAssetKind
|
|
58
|
+
downloadName?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ReadonlyRecordAssetResolveContext {
|
|
62
|
+
fieldId: string
|
|
63
|
+
fieldPath: string
|
|
64
|
+
scope: string
|
|
65
|
+
node: AimdNode
|
|
66
|
+
value: unknown
|
|
67
|
+
normalizedValue: unknown
|
|
68
|
+
fileId?: string
|
|
69
|
+
recordValue: RenderContext['value']
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ReadonlyRecordAssetResolver = (
|
|
73
|
+
context: ReadonlyRecordAssetResolveContext,
|
|
74
|
+
) => ReadonlyRecordAsset | null | undefined
|
|
75
|
+
|
|
76
|
+
export type ReadonlyRecordRenderContextInput = Partial<RenderContext>
|
|
77
|
+
& Partial<Pick<AimdRendererContext, 'locale' | 'messages'>>
|
|
78
|
+
|
|
79
|
+
export interface ReadonlyRecordVueRendererOptions extends AimdRendererOptions, VueRendererOptions {
|
|
80
|
+
resolveAsset?: ReadonlyRecordAssetResolver
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ReadonlyRecordMarkdownRenderOptions = AimdRendererOptions
|
|
84
|
+
& Pick<VueRendererOptions, 'componentMap' | 'elementRenderers' | 'locale' | 'messages' | 'quizPreview'>
|
|
85
|
+
|
|
86
|
+
type ReadonlyRecordVarValueRendererKind = 'file' | 'boolean' | 'markdown' | 'code' | 'dna' | 'scalar'
|
|
87
|
+
|
|
88
|
+
interface ReadonlyRecordVarValueRendererContext {
|
|
89
|
+
node: AimdNode
|
|
90
|
+
varNode: AimdVarNode
|
|
91
|
+
definition: AimdVarNode['definition']
|
|
92
|
+
kwargs?: Record<string, unknown>
|
|
93
|
+
value: unknown
|
|
94
|
+
recordValue: RenderContext['value']
|
|
95
|
+
asset: ReadonlyRecordAsset | null | undefined
|
|
96
|
+
fileId?: string
|
|
97
|
+
resolveAsset?: ReadonlyRecordAssetResolver
|
|
98
|
+
markdownRenderOptions?: ReadonlyRecordMarkdownRenderOptions
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface ReadonlyRecordVarValueRendererEntry {
|
|
102
|
+
kind: ReadonlyRecordVarValueRendererKind
|
|
103
|
+
render: (context: ReadonlyRecordVarValueRendererContext) => VNode | null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fieldPathFor(scope: string, id: string): string {
|
|
107
|
+
return `data.${scope}.${id}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function fieldDisplayLabel(node: AimdNode): string {
|
|
111
|
+
if ((node.fieldType === 'var' || node.fieldType === 'var_table') && 'definition' in node) {
|
|
112
|
+
return getAimdFieldDisplayLabel(node.id, node.definition)
|
|
113
|
+
}
|
|
114
|
+
if (node.fieldType === 'check' && 'label' in node && typeof node.label === 'string' && node.label.trim()) {
|
|
115
|
+
return node.label.trim()
|
|
116
|
+
}
|
|
117
|
+
if (node.fieldType === 'quiz' && 'title' in node && typeof node.title === 'string' && node.title.trim()) {
|
|
118
|
+
return node.title.trim()
|
|
119
|
+
}
|
|
120
|
+
return node.id
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function fieldMetadataTitle(node: AimdNode): string {
|
|
124
|
+
const label = fieldDisplayLabel(node)
|
|
125
|
+
const path = fieldPathFor(node.scope, node.id)
|
|
126
|
+
return label === node.id ? path : `${label} · ${path}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function recordFieldProps(node: AimdNode, className: string): Record<string, unknown> {
|
|
130
|
+
const label = fieldDisplayLabel(node)
|
|
131
|
+
return {
|
|
132
|
+
class: className,
|
|
133
|
+
title: fieldMetadataTitle(node),
|
|
134
|
+
'aria-label': label,
|
|
135
|
+
'data-aimd-type': node.fieldType,
|
|
136
|
+
'data-aimd-id': node.id,
|
|
137
|
+
'data-aimd-scope': node.scope,
|
|
138
|
+
'data-aimd-field-label': label,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createAssetFallbackNode(id: string): AimdNode {
|
|
143
|
+
return {
|
|
144
|
+
type: 'aimd',
|
|
145
|
+
fieldType: 'fig',
|
|
146
|
+
scope: 'fig',
|
|
147
|
+
id,
|
|
148
|
+
raw: id,
|
|
149
|
+
src: id,
|
|
150
|
+
} as AimdNode
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveAssetForField(
|
|
154
|
+
node: AimdNode,
|
|
155
|
+
value: unknown,
|
|
156
|
+
recordValue: RenderContext['value'],
|
|
157
|
+
resolver: ReadonlyRecordAssetResolver | undefined,
|
|
158
|
+
): ReadonlyRecordAsset | null | undefined {
|
|
159
|
+
if (!resolver) {
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
const normalizedValue = getAimdDisplayValue(value)
|
|
163
|
+
const fileId = getAimdFileValueId(value, AIMD_FILE_REFERENCE_VALUE_KEYS)
|
|
164
|
+
return resolver({
|
|
165
|
+
fieldId: node.id,
|
|
166
|
+
fieldPath: fieldPathFor(node.scope, node.id),
|
|
167
|
+
scope: node.scope,
|
|
168
|
+
node,
|
|
169
|
+
value,
|
|
170
|
+
normalizedValue,
|
|
171
|
+
fileId,
|
|
172
|
+
recordValue,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveAssetForReference(
|
|
177
|
+
source: unknown,
|
|
178
|
+
recordValue: RenderContext['value'],
|
|
179
|
+
resolver: ReadonlyRecordAssetResolver | undefined,
|
|
180
|
+
node?: AimdNode,
|
|
181
|
+
): ReadonlyRecordAsset | null | undefined {
|
|
182
|
+
if (!resolver) {
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
const normalizedValue = getAimdDisplayValue(source)
|
|
186
|
+
const fileId = normalizeAimdString(normalizedValue)
|
|
187
|
+
const fallbackNode = node ?? createAssetFallbackNode(fileId ?? 'asset')
|
|
188
|
+
return resolver({
|
|
189
|
+
fieldId: fallbackNode.id,
|
|
190
|
+
fieldPath: node ? fieldPathFor(node.scope, node.id) : (fileId ?? ''),
|
|
191
|
+
scope: fallbackNode.scope,
|
|
192
|
+
node: fallbackNode,
|
|
193
|
+
value: source,
|
|
194
|
+
normalizedValue,
|
|
195
|
+
fileId,
|
|
196
|
+
recordValue,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderEmptyValue(node: AimdNode): VNode {
|
|
201
|
+
return h('span', recordFieldProps(node, 'aimd-record-field aimd-record-field--empty'), [
|
|
202
|
+
h('span', { class: 'aimd-record-field__missing-label' }, 'Missing'),
|
|
203
|
+
h('span', { class: 'aimd-record-field__missing-name' }, fieldDisplayLabel(node)),
|
|
204
|
+
])
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderScalarValue(node: AimdNode, value: unknown, className = ''): VNode {
|
|
208
|
+
const displayValue = stringifyAimdDisplayValue(value)
|
|
209
|
+
if (!displayValue) {
|
|
210
|
+
return renderEmptyValue(node)
|
|
211
|
+
}
|
|
212
|
+
return h('span', recordFieldProps(
|
|
213
|
+
node,
|
|
214
|
+
['aimd-record-field', 'aimd-record-field--scalar', className].filter(Boolean).join(' '),
|
|
215
|
+
), displayValue)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderBooleanValue(node: AimdNode, value: unknown): VNode {
|
|
219
|
+
const checked = toAimdBooleanValue(value)
|
|
220
|
+
return h('span', recordFieldProps(node, 'aimd-record-field aimd-record-field--boolean'), [
|
|
221
|
+
h('input', {
|
|
222
|
+
type: 'checkbox',
|
|
223
|
+
checked,
|
|
224
|
+
disabled: true,
|
|
225
|
+
class: 'aimd-checkbox',
|
|
226
|
+
}),
|
|
227
|
+
h('span', { class: 'aimd-record-field__value' }, checked ? 'Yes' : 'No'),
|
|
228
|
+
])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const ReadonlyRecordMarkdownValue = defineComponent({
|
|
232
|
+
name: 'ReadonlyRecordMarkdownValue',
|
|
233
|
+
props: {
|
|
234
|
+
content: {
|
|
235
|
+
type: String,
|
|
236
|
+
required: true,
|
|
237
|
+
},
|
|
238
|
+
fieldProps: {
|
|
239
|
+
type: Object as PropType<Record<string, unknown>>,
|
|
240
|
+
required: true,
|
|
241
|
+
},
|
|
242
|
+
recordValue: {
|
|
243
|
+
type: Object as PropType<RenderContext['value']>,
|
|
244
|
+
default: undefined,
|
|
245
|
+
},
|
|
246
|
+
resolveAsset: {
|
|
247
|
+
type: Function as PropType<ReadonlyRecordAssetResolver>,
|
|
248
|
+
default: undefined,
|
|
249
|
+
},
|
|
250
|
+
renderOptions: {
|
|
251
|
+
type: Object as PropType<ReadonlyRecordMarkdownRenderOptions>,
|
|
252
|
+
default: () => ({}),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
setup(props) {
|
|
256
|
+
const nodes = shallowRef<VNodeChild[]>([])
|
|
257
|
+
const renderError = ref('')
|
|
258
|
+
let requestId = 0
|
|
259
|
+
|
|
260
|
+
async function renderMarkdownContent() {
|
|
261
|
+
const currentRequestId = ++requestId
|
|
262
|
+
renderError.value = ''
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const nestedElementRenderers = createReadonlyRecordElementRenderers({
|
|
266
|
+
resolveAsset: props.resolveAsset,
|
|
267
|
+
})
|
|
268
|
+
const rendered = await renderToVue(props.content, {
|
|
269
|
+
...props.renderOptions,
|
|
270
|
+
mode: 'preview',
|
|
271
|
+
aimdRenderers: {
|
|
272
|
+
fig: createFigRenderer(props.resolveAsset),
|
|
273
|
+
},
|
|
274
|
+
elementRenderers: {
|
|
275
|
+
...nestedElementRenderers,
|
|
276
|
+
...(props.renderOptions.elementRenderers ?? {}),
|
|
277
|
+
},
|
|
278
|
+
context: {
|
|
279
|
+
mode: 'preview',
|
|
280
|
+
readonly: true,
|
|
281
|
+
value: props.recordValue,
|
|
282
|
+
quizPreview: props.renderOptions.quizPreview,
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
if (currentRequestId !== requestId) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
nodes.value = rendered.nodes
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (currentRequestId !== requestId) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
nodes.value = []
|
|
297
|
+
renderError.value = error instanceof Error ? error.message : String(error)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
watch(
|
|
302
|
+
() => [props.content, props.recordValue, props.resolveAsset, props.renderOptions] as const,
|
|
303
|
+
() => {
|
|
304
|
+
void renderMarkdownContent()
|
|
305
|
+
},
|
|
306
|
+
{ immediate: true },
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return () => {
|
|
310
|
+
if (renderError.value) {
|
|
311
|
+
return h('div', {
|
|
312
|
+
...props.fieldProps,
|
|
313
|
+
class: `${props.fieldProps.class ?? ''} aimd-record-field--markdown-error`,
|
|
314
|
+
'data-render-error': renderError.value,
|
|
315
|
+
}, [
|
|
316
|
+
h('pre', { class: 'aimd-record-field__markdown-fallback' }, props.content),
|
|
317
|
+
])
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return h('div', props.fieldProps, nodes.value.length
|
|
321
|
+
? nodes.value
|
|
322
|
+
: [h('pre', { class: 'aimd-record-field__markdown-fallback' }, props.content)])
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
function renderMarkdownValue(
|
|
328
|
+
node: AimdNode,
|
|
329
|
+
value: unknown,
|
|
330
|
+
recordValue: RenderContext['value'],
|
|
331
|
+
resolveAsset: ReadonlyRecordAssetResolver | undefined,
|
|
332
|
+
renderOptions: ReadonlyRecordMarkdownRenderOptions | undefined,
|
|
333
|
+
): VNode {
|
|
334
|
+
const displayValue = stringifyAimdDisplayValue(value)
|
|
335
|
+
if (!displayValue) {
|
|
336
|
+
return renderEmptyValue(node)
|
|
337
|
+
}
|
|
338
|
+
return h(ReadonlyRecordMarkdownValue, {
|
|
339
|
+
content: displayValue,
|
|
340
|
+
fieldProps: recordFieldProps(node, 'aimd-record-field aimd-record-field--markdown'),
|
|
341
|
+
recordValue,
|
|
342
|
+
resolveAsset,
|
|
343
|
+
renderOptions,
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function renderCodeValue(node: AimdNode, value: unknown, language?: unknown): VNode {
|
|
348
|
+
const lang = normalizeAimdString(language)
|
|
349
|
+
const displayValue = stringifyAimdDisplayValue(value)
|
|
350
|
+
if (!displayValue) {
|
|
351
|
+
return renderEmptyValue(node)
|
|
352
|
+
}
|
|
353
|
+
return h('pre', recordFieldProps(node, 'aimd-record-field aimd-record-field--code'), [
|
|
354
|
+
h('code', {
|
|
355
|
+
class: lang ? `language-${lang}` : undefined,
|
|
356
|
+
}, displayValue),
|
|
357
|
+
])
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function renderDnaValue(node: AimdNode, value: unknown): VNode {
|
|
361
|
+
const normalized = getAimdDisplayValue(value)
|
|
362
|
+
const sequence = isAimdPlainRecord(normalized)
|
|
363
|
+
? normalizeAimdString(normalized.sequence) ?? stringifyAimdDisplayValue(normalized)
|
|
364
|
+
: stringifyAimdDisplayValue(normalized)
|
|
365
|
+
if (!sequence) {
|
|
366
|
+
return renderEmptyValue(node)
|
|
367
|
+
}
|
|
368
|
+
const name = isAimdPlainRecord(normalized) ? normalizeAimdString(normalized.name) : undefined
|
|
369
|
+
return h('div', recordFieldProps(node, 'aimd-record-field aimd-record-field--dna'), [
|
|
370
|
+
name ? h('div', { class: 'aimd-record-field__label' }, name) : null,
|
|
371
|
+
h('pre', { class: 'aimd-record-field__sequence' }, sequence),
|
|
372
|
+
])
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function renderFileValue(
|
|
376
|
+
node: AimdNode,
|
|
377
|
+
value: unknown,
|
|
378
|
+
asset: ReadonlyRecordAsset | null | undefined,
|
|
379
|
+
kind: ReadonlyRecordAssetKind,
|
|
380
|
+
): VNode {
|
|
381
|
+
if (!asset && !getAimdFileValueId(value, AIMD_FILE_REFERENCE_VALUE_KEYS)) {
|
|
382
|
+
return renderEmptyValue(node)
|
|
383
|
+
}
|
|
384
|
+
const mediaSrc = getAimdAssetMediaSource(asset)
|
|
385
|
+
const href = asset?.href ?? asset?.url
|
|
386
|
+
const name = getAimdFileDisplayName(value, asset) || node.id
|
|
387
|
+
const badge = AIMD_FILE_BADGE_BY_KIND[kind]
|
|
388
|
+
const commonProps: Record<string, unknown> = {
|
|
389
|
+
...recordFieldProps(node, `aimd-record-field aimd-record-field--asset aimd-record-field--${kind}`),
|
|
390
|
+
'data-aimd-file-id': getAimdFileValueId(value, AIMD_FILE_REFERENCE_VALUE_KEYS),
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (kind === 'image' && mediaSrc) {
|
|
394
|
+
return h('figure', commonProps, [
|
|
395
|
+
h('img', {
|
|
396
|
+
src: mediaSrc,
|
|
397
|
+
alt: name,
|
|
398
|
+
loading: 'lazy',
|
|
399
|
+
class: 'aimd-record-field__image',
|
|
400
|
+
}),
|
|
401
|
+
h('figcaption', { class: 'aimd-record-field__caption' }, name),
|
|
402
|
+
])
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (kind === 'audio' && mediaSrc) {
|
|
406
|
+
return h('div', commonProps, [
|
|
407
|
+
h('audio', {
|
|
408
|
+
src: mediaSrc,
|
|
409
|
+
controls: true,
|
|
410
|
+
class: 'aimd-record-field__audio',
|
|
411
|
+
}),
|
|
412
|
+
h('span', { class: 'aimd-record-field__caption' }, name),
|
|
413
|
+
])
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (kind === 'video' && mediaSrc) {
|
|
417
|
+
return h('figure', commonProps, [
|
|
418
|
+
h('video', {
|
|
419
|
+
src: mediaSrc,
|
|
420
|
+
controls: true,
|
|
421
|
+
class: 'aimd-record-field__video',
|
|
422
|
+
}),
|
|
423
|
+
h('figcaption', { class: 'aimd-record-field__caption' }, name),
|
|
424
|
+
])
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const label = [
|
|
428
|
+
h('span', { class: 'aimd-record-field__badge' }, badge),
|
|
429
|
+
h('span', { class: 'aimd-record-field__filename' }, name),
|
|
430
|
+
]
|
|
431
|
+
if (href) {
|
|
432
|
+
return h('a', {
|
|
433
|
+
...commonProps,
|
|
434
|
+
href,
|
|
435
|
+
download: asset?.downloadName ?? asset?.filename ?? asset?.name ?? undefined,
|
|
436
|
+
}, label)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return h('span', {
|
|
440
|
+
...commonProps,
|
|
441
|
+
class: `${commonProps.class} aimd-record-field--asset-missing`,
|
|
442
|
+
}, label)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function createReadonlyRecordVarValueRendererEntries(): ReadonlyRecordVarValueRendererEntry[] {
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
kind: 'file',
|
|
449
|
+
render: ({ node, definition, kwargs, value, asset, fileId }) => {
|
|
450
|
+
if (!isAimdFileLikeType(definition?.type, kwargs) && !asset && !isAimdAiralogyFileId(fileId)) {
|
|
451
|
+
return null
|
|
452
|
+
}
|
|
453
|
+
const kind = inferAimdAssetKind(asset, value, definition?.type, kwargs)
|
|
454
|
+
return renderFileValue(node, value, asset, kind)
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
kind: 'boolean',
|
|
459
|
+
render: ({ node, definition, kwargs, value }) =>
|
|
460
|
+
isAimdBooleanType(definition?.type, kwargs) ? renderBooleanValue(node, value) : null,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
kind: 'markdown',
|
|
464
|
+
render: ({ node, definition, kwargs, value, recordValue, resolveAsset, markdownRenderOptions }) =>
|
|
465
|
+
isAimdMarkdownType(definition?.type, kwargs)
|
|
466
|
+
? renderMarkdownValue(node, value, recordValue, resolveAsset, markdownRenderOptions)
|
|
467
|
+
: null,
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
kind: 'code',
|
|
471
|
+
render: ({ node, definition, kwargs, value }) =>
|
|
472
|
+
isAimdCodeType(definition?.type, kwargs)
|
|
473
|
+
? renderCodeValue(node, value, kwargs?.language ?? kwargs?.lang ?? kwargs?.code_language ?? kwargs?.codeLanguage)
|
|
474
|
+
: null,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
kind: 'dna',
|
|
478
|
+
render: ({ node, definition, kwargs, value }) =>
|
|
479
|
+
isAimdDnaType(definition?.type, kwargs) ? renderDnaValue(node, value) : null,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
kind: 'scalar',
|
|
483
|
+
render: ({ node, value }) => renderScalarValue(node, value),
|
|
484
|
+
},
|
|
485
|
+
]
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const READONLY_RECORD_VAR_VALUE_RENDERERS = createReadonlyRecordVarValueRendererEntries()
|
|
489
|
+
|
|
490
|
+
function renderReadonlyRecordVarValue(context: ReadonlyRecordVarValueRendererContext): VNode {
|
|
491
|
+
for (const renderer of READONLY_RECORD_VAR_VALUE_RENDERERS) {
|
|
492
|
+
const vnode = renderer.render(context)
|
|
493
|
+
if (vnode) {
|
|
494
|
+
return vnode
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return renderScalarValue(context.node, context.value)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function createVarRenderer(
|
|
501
|
+
resolveRecordAsset?: ReadonlyRecordAssetResolver,
|
|
502
|
+
markdownRenderOptions?: ReadonlyRecordMarkdownRenderOptions,
|
|
503
|
+
): AimdComponentRenderer {
|
|
504
|
+
return (node, ctx) => {
|
|
505
|
+
const varNode = node as AimdVarNode
|
|
506
|
+
const definition = varNode.definition
|
|
507
|
+
const kwargs = definition?.kwargs
|
|
508
|
+
const value = ctx.value?.var?.[node.id]
|
|
509
|
+
const asset = resolveAssetForField(node, value, ctx.value, resolveRecordAsset)
|
|
510
|
+
const fileId = getAimdFileValueId(value, AIMD_FILE_REFERENCE_VALUE_KEYS)
|
|
511
|
+
|
|
512
|
+
return renderReadonlyRecordVarValue({
|
|
513
|
+
node,
|
|
514
|
+
varNode,
|
|
515
|
+
definition,
|
|
516
|
+
kwargs,
|
|
517
|
+
value,
|
|
518
|
+
recordValue: ctx.value,
|
|
519
|
+
asset,
|
|
520
|
+
fileId,
|
|
521
|
+
resolveAsset: resolveRecordAsset,
|
|
522
|
+
markdownRenderOptions,
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function normalizeTableRows(value: unknown): Record<string, unknown>[] {
|
|
528
|
+
const normalized = getAimdDisplayValue(value)
|
|
529
|
+
if (Array.isArray(normalized)) {
|
|
530
|
+
return normalized
|
|
531
|
+
.filter((row): row is Record<string, unknown> => isAimdPlainRecord(row))
|
|
532
|
+
.map(row => ({ ...row }))
|
|
533
|
+
}
|
|
534
|
+
if (isAimdPlainRecord(normalized)) {
|
|
535
|
+
return Object.values(normalized)
|
|
536
|
+
.filter((row): row is Record<string, unknown> => isAimdPlainRecord(row))
|
|
537
|
+
.map(row => ({ ...row }))
|
|
538
|
+
}
|
|
539
|
+
return []
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function createVarTableRenderer(): AimdComponentRenderer {
|
|
543
|
+
return (node, ctx) => {
|
|
544
|
+
const tableNode = node as AimdVarTableNode
|
|
545
|
+
const rows = normalizeTableRows(ctx.value?.var?.[node.id] ?? ctx.value?.var_table?.[node.id])
|
|
546
|
+
const columns = tableNode.columns.length
|
|
547
|
+
? tableNode.columns
|
|
548
|
+
: [...new Set(rows.flatMap(row => Object.keys(row)))]
|
|
549
|
+
const subvars = tableNode.definition?.subvars
|
|
550
|
+
|
|
551
|
+
if (!columns.length || !rows.length) {
|
|
552
|
+
return h('div', recordFieldProps(node, 'aimd-record-table aimd-record-table--empty'), [
|
|
553
|
+
h('span', { class: 'aimd-record-field__missing-label' }, 'Missing'),
|
|
554
|
+
h('span', { class: 'aimd-record-field__missing-name' }, fieldDisplayLabel(node)),
|
|
555
|
+
])
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return h('div', recordFieldProps(node, 'aimd-record-table'), [
|
|
559
|
+
h('table', [
|
|
560
|
+
h('thead', [
|
|
561
|
+
h('tr', columns.map(column => h('th', {
|
|
562
|
+
'data-column-id': column,
|
|
563
|
+
title: column,
|
|
564
|
+
}, getAimdFieldDisplayLabel(column, subvars?.[column])))),
|
|
565
|
+
]),
|
|
566
|
+
h('tbody', rows.map(row =>
|
|
567
|
+
h('tr', columns.map(column =>
|
|
568
|
+
h('td', stringifyAimdDisplayValue(row[column])),
|
|
569
|
+
)),
|
|
570
|
+
)),
|
|
571
|
+
]),
|
|
572
|
+
])
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function createCheckRenderer(): AimdComponentRenderer {
|
|
577
|
+
return (node, ctx, children) => {
|
|
578
|
+
const checkNode = node as AimdCheckNode
|
|
579
|
+
const value = ctx.value?.check?.[node.id]
|
|
580
|
+
const normalized = isAimdPlainRecord(value) && 'checked' in value ? value.checked : value
|
|
581
|
+
const checked = toAimdBooleanValue(normalized)
|
|
582
|
+
const label = checkNode.label ?? node.id
|
|
583
|
+
const bodyChildren = children && children.length > 0
|
|
584
|
+
? children
|
|
585
|
+
: [label]
|
|
586
|
+
return h('label', recordFieldProps(node, 'aimd-record-check'), [
|
|
587
|
+
h('input', {
|
|
588
|
+
type: 'checkbox',
|
|
589
|
+
checked,
|
|
590
|
+
disabled: true,
|
|
591
|
+
class: 'aimd-checkbox',
|
|
592
|
+
}),
|
|
593
|
+
h('span', { class: 'aimd-record-check__body' }, bodyChildren),
|
|
594
|
+
])
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function createQuizRenderer(): AimdComponentRenderer {
|
|
599
|
+
return (node, ctx) => {
|
|
600
|
+
const quizNode = node as AimdQuizNode
|
|
601
|
+
const value = ctx.value?.quiz?.[node.id]
|
|
602
|
+
const answer = stringifyAimdDisplayValue(value)
|
|
603
|
+
return h('div', recordFieldProps(node, 'aimd-record-quiz'), [
|
|
604
|
+
h('div', { class: 'aimd-record-quiz__stem' }, quizNode.stem || quizNode.title || node.id),
|
|
605
|
+
answer
|
|
606
|
+
? h('div', { class: 'aimd-record-quiz__answer' }, answer)
|
|
607
|
+
: h('div', { class: 'aimd-record-quiz__answer aimd-record-quiz__answer--empty' }, 'No answer recorded'),
|
|
608
|
+
])
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function mergeClassNames(...values: unknown[]): string | undefined {
|
|
613
|
+
const classNames = values.flatMap((value) => {
|
|
614
|
+
if (!value) {
|
|
615
|
+
return []
|
|
616
|
+
}
|
|
617
|
+
if (Array.isArray(value)) {
|
|
618
|
+
return value.map(item => String(item)).filter(Boolean)
|
|
619
|
+
}
|
|
620
|
+
return String(value).split(/\s+/).filter(Boolean)
|
|
621
|
+
})
|
|
622
|
+
return classNames.length ? [...new Set(classNames)].join(' ') : undefined
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function createFigRenderer(resolveRecordAsset?: ReadonlyRecordAssetResolver): AimdComponentRenderer {
|
|
626
|
+
return (node, ctx) => {
|
|
627
|
+
const figNode = node as AimdFigNode
|
|
628
|
+
const figId = figNode.id || node.id
|
|
629
|
+
const figSrc = figNode.src || ''
|
|
630
|
+
const asset = resolveAssetForReference(figSrc, ctx.value, resolveRecordAsset, figNode)
|
|
631
|
+
const resolvedSrc = getAimdAssetMediaSource(asset) ?? figSrc
|
|
632
|
+
const figTitle = figNode.title
|
|
633
|
+
const figLegend = figNode.legend
|
|
634
|
+
const figSequence = figNode.sequence
|
|
635
|
+
const captionChildren = [
|
|
636
|
+
figSequence !== undefined || figTitle
|
|
637
|
+
? h('div', { class: 'aimd-figure__title' }, figSequence !== undefined
|
|
638
|
+
? ctx.messages.figure.captionTitle(figSequence + 1, figTitle)
|
|
639
|
+
: figTitle)
|
|
640
|
+
: null,
|
|
641
|
+
figLegend ? h('div', { class: 'aimd-figure__legend' }, figLegend) : null,
|
|
642
|
+
].filter(Boolean)
|
|
643
|
+
|
|
644
|
+
return h('figure', {
|
|
645
|
+
class: 'aimd-figure',
|
|
646
|
+
'data-aimd-type': 'fig',
|
|
647
|
+
'data-aimd-fig-id': figId,
|
|
648
|
+
'data-aimd-fig-src': figSrc,
|
|
649
|
+
id: `fig-${figId}`,
|
|
650
|
+
}, [
|
|
651
|
+
h('img', {
|
|
652
|
+
class: 'aimd-figure__image',
|
|
653
|
+
src: resolvedSrc,
|
|
654
|
+
alt: figTitle || asset?.name || asset?.filename || figId,
|
|
655
|
+
loading: 'lazy',
|
|
656
|
+
}),
|
|
657
|
+
captionChildren.length
|
|
658
|
+
? h('figcaption', { class: 'aimd-figure__caption' }, captionChildren)
|
|
659
|
+
: null,
|
|
660
|
+
])
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function createImageElementRenderer(resolveRecordAsset?: ReadonlyRecordAssetResolver): ElementRenderer {
|
|
665
|
+
return (node, _children, ctx) => {
|
|
666
|
+
const properties = node.properties ?? {}
|
|
667
|
+
const src = normalizeAimdString(properties.src)
|
|
668
|
+
const asset = resolveAssetForReference(src, ctx.value, resolveRecordAsset)
|
|
669
|
+
const resolvedSrc = getAimdAssetMediaSource(asset) ?? src
|
|
670
|
+
const imgProps: Record<string, unknown> = {
|
|
671
|
+
...properties,
|
|
672
|
+
src: resolvedSrc,
|
|
673
|
+
alt: normalizeAimdString(properties.alt) ?? asset?.name ?? asset?.filename,
|
|
674
|
+
title: normalizeAimdString(properties.title),
|
|
675
|
+
loading: properties.loading ?? 'lazy',
|
|
676
|
+
class: mergeClassNames('aimd-image', properties.className, properties.class),
|
|
677
|
+
}
|
|
678
|
+
delete imgProps.className
|
|
679
|
+
return h('img', imgProps)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function createReadonlyRecordAimdRenderers(
|
|
684
|
+
options: Pick<ReadonlyRecordVueRendererOptions, 'resolveAsset'> & {
|
|
685
|
+
markdownRenderOptions?: ReadonlyRecordMarkdownRenderOptions
|
|
686
|
+
} = {},
|
|
687
|
+
): Record<string, AimdComponentRenderer> {
|
|
688
|
+
return {
|
|
689
|
+
var: createVarRenderer(options.resolveAsset, options.markdownRenderOptions),
|
|
690
|
+
var_table: createVarTableRenderer(),
|
|
691
|
+
check: createCheckRenderer(),
|
|
692
|
+
quiz: createQuizRenderer(),
|
|
693
|
+
fig: createFigRenderer(options.resolveAsset),
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function createReadonlyRecordElementRenderers(
|
|
698
|
+
options: Pick<ReadonlyRecordVueRendererOptions, 'resolveAsset'> = {},
|
|
699
|
+
): Record<string, ElementRenderer> {
|
|
700
|
+
return {
|
|
701
|
+
img: createImageElementRenderer(options.resolveAsset),
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function normalizeRecordRenderValue(recordData: unknown): RenderContext['value'] {
|
|
706
|
+
return normalizeAimdRecordDataValue(recordData) as RenderContext['value']
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function createReadonlyRecordRenderContext<T extends ReadonlyRecordRenderContextInput>(
|
|
710
|
+
recordData: unknown,
|
|
711
|
+
context?: T,
|
|
712
|
+
): T & RenderContext {
|
|
713
|
+
return {
|
|
714
|
+
...((context ?? {}) as T),
|
|
715
|
+
mode: 'edit',
|
|
716
|
+
readonly: true,
|
|
717
|
+
value: normalizeRecordRenderValue(recordData),
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export async function renderReadonlyRecordToVue(
|
|
722
|
+
content: string,
|
|
723
|
+
recordData: unknown,
|
|
724
|
+
options: ReadonlyRecordVueRendererOptions = {},
|
|
725
|
+
): Promise<RenderResult> {
|
|
726
|
+
const { context, resolveAsset: resolveRecordAsset, aimdRenderers, elementRenderers, ...renderOptions } = options
|
|
727
|
+
const readonlyAimdRenderers = createReadonlyRecordAimdRenderers({
|
|
728
|
+
resolveAsset: resolveRecordAsset,
|
|
729
|
+
markdownRenderOptions: {
|
|
730
|
+
...renderOptions,
|
|
731
|
+
elementRenderers,
|
|
732
|
+
},
|
|
733
|
+
})
|
|
734
|
+
const readonlyElementRenderers = createReadonlyRecordElementRenderers({ resolveAsset: resolveRecordAsset })
|
|
735
|
+
return renderToVue(content, {
|
|
736
|
+
...renderOptions,
|
|
737
|
+
aimdRenderers: {
|
|
738
|
+
...readonlyAimdRenderers,
|
|
739
|
+
...(aimdRenderers ?? {}),
|
|
740
|
+
},
|
|
741
|
+
elementRenderers: {
|
|
742
|
+
...readonlyElementRenderers,
|
|
743
|
+
...(elementRenderers ?? {}),
|
|
744
|
+
},
|
|
745
|
+
context: createReadonlyRecordRenderContext(recordData, context),
|
|
746
|
+
})
|
|
747
|
+
}
|