@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.
@@ -2,6 +2,7 @@
2
2
  * Vue rendering exports
3
3
  */
4
4
  export { type AimdComponentRenderer, type AimdRendererContext, type AssetResolver, createAssetRenderer, createCodeBlockRenderer, createComponentRenderer, createEmbeddedRenderer, createMermaidRenderer, createStepCardRenderer, loadShikiHighlighter, type CodeBlockRendererOptions, type ElementRenderer, hastToVue, renderToVNodes, type LoadShikiHighlighterOptions, type AimdStepCardRendererOptions, type ShikiHighlighter, type VueRendererOptions, } from './vue-renderer';
5
+ export { AIMD_RECORD_RENDER_SCOPES, createReadonlyRecordAimdRenderers, createReadonlyRecordElementRenderers, createReadonlyRecordRenderContext, normalizeRecordRenderValue, renderReadonlyRecordToVue, type AimdRecordRenderScope, type AimdRecordRenderValue, type ReadonlyRecordAsset, type ReadonlyRecordAssetKind, type ReadonlyRecordAssetResolveContext, type ReadonlyRecordAssetResolver, type ReadonlyRecordMarkdownRenderOptions, type ReadonlyRecordRenderContextInput, type ReadonlyRecordVueRendererOptions, } from './readonly-record-renderer';
5
6
  export { renderToVue, createRenderer, defaultRenderer, } from '../common/processor';
6
7
  export { createAimdRendererMessages, DEFAULT_AIMD_RENDERER_LOCALE, resolveAimdRendererLocale, } from '../locales';
7
8
  export type { RenderContext, RenderMode, ProcessorOptions, } from '@airalogy/aimd-core/types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vue/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,SAAS,EACT,cAAc,EACd,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,WAAW,EACX,cAAc,EACd,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,GACjB,MAAM,2BAA2B,CAAA;AAElC,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACpG,YAAY,EACV,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vue/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,SAAS,EACT,cAAc,EACd,KAAK,2BAA2B,EAChC,KAAK,2BAA2B,EAChC,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,yBAAyB,EACzB,iCAAiC,EACjC,oCAAoC,EACpC,iCAAiC,EACjC,0BAA0B,EAC1B,yBAAyB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,iCAAiC,EACtC,KAAK,2BAA2B,EAChC,KAAK,mCAAmC,EACxC,KAAK,gCAAgC,EACrC,KAAK,gCAAgC,GACtC,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EACL,WAAW,EACX,cAAc,EACd,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,GACjB,MAAM,2BAA2B,CAAA;AAElC,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACpG,YAAY,EACV,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,YAAY,CAAA"}
@@ -0,0 +1,42 @@
1
+ import type { AimdNode, RenderContext } from '@airalogy/aimd-core/types';
2
+ import { type AimdAssetKind, type AimdAssetLike, type AimdRecordDataScope, type AimdRecordDataValue } from '@airalogy/aimd-core/utils';
3
+ import type { AimdRendererOptions, RenderResult } from '../common/processor';
4
+ import type { AimdComponentRenderer, AimdRendererContext, ElementRenderer, VueRendererOptions } from './vue-renderer';
5
+ export declare const AIMD_RECORD_RENDER_SCOPES: readonly ["var", "var_table", "step", "check", "quiz"];
6
+ export type AimdRecordRenderScope = AimdRecordDataScope;
7
+ export type ReadonlyRecordAssetKind = AimdAssetKind;
8
+ export type AimdRecordRenderValue = AimdRecordDataValue;
9
+ export interface ReadonlyRecordAsset extends AimdAssetLike {
10
+ url?: string;
11
+ href?: string;
12
+ name?: string;
13
+ filename?: string;
14
+ mimeType?: string;
15
+ size?: number;
16
+ kind?: ReadonlyRecordAssetKind;
17
+ downloadName?: string;
18
+ }
19
+ export interface ReadonlyRecordAssetResolveContext {
20
+ fieldId: string;
21
+ fieldPath: string;
22
+ scope: string;
23
+ node: AimdNode;
24
+ value: unknown;
25
+ normalizedValue: unknown;
26
+ fileId?: string;
27
+ recordValue: RenderContext['value'];
28
+ }
29
+ export type ReadonlyRecordAssetResolver = (context: ReadonlyRecordAssetResolveContext) => ReadonlyRecordAsset | null | undefined;
30
+ export type ReadonlyRecordRenderContextInput = Partial<RenderContext> & Partial<Pick<AimdRendererContext, 'locale' | 'messages'>>;
31
+ export interface ReadonlyRecordVueRendererOptions extends AimdRendererOptions, VueRendererOptions {
32
+ resolveAsset?: ReadonlyRecordAssetResolver;
33
+ }
34
+ export type ReadonlyRecordMarkdownRenderOptions = AimdRendererOptions & Pick<VueRendererOptions, 'componentMap' | 'elementRenderers' | 'locale' | 'messages' | 'quizPreview'>;
35
+ export declare function createReadonlyRecordAimdRenderers(options?: Pick<ReadonlyRecordVueRendererOptions, 'resolveAsset'> & {
36
+ markdownRenderOptions?: ReadonlyRecordMarkdownRenderOptions;
37
+ }): Record<string, AimdComponentRenderer>;
38
+ export declare function createReadonlyRecordElementRenderers(options?: Pick<ReadonlyRecordVueRendererOptions, 'resolveAsset'>): Record<string, ElementRenderer>;
39
+ export declare function normalizeRecordRenderValue(recordData: unknown): RenderContext['value'];
40
+ export declare function createReadonlyRecordRenderContext<T extends ReadonlyRecordRenderContextInput>(recordData: unknown, context?: T): T & RenderContext;
41
+ export declare function renderReadonlyRecordToVue(content: string, recordData: unknown, options?: ReadonlyRecordVueRendererOptions): Promise<RenderResult>;
42
+ //# sourceMappingURL=readonly-record-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readonly-record-renderer.d.ts","sourceRoot":"","sources":["../../src/vue/readonly-record-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,QAAQ,EAIR,aAAa,EACd,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAqBL,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAA;AAGlC,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAE5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAErH,eAAO,MAAM,yBAAyB,wDAA0B,CAAA;AAEhE,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAA;AAEvD,MAAM,MAAM,uBAAuB,GAAG,aAAa,CAAA;AAEnD,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAA;AAEvD,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,uBAAuB,CAAA;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iCAAiC;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,CAAA;IACd,KAAK,EAAE,OAAO,CAAA;IACd,eAAe,EAAE,OAAO,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CACpC;AAED,MAAM,MAAM,2BAA2B,GAAG,CACxC,OAAO,EAAE,iCAAiC,KACvC,mBAAmB,GAAG,IAAI,GAAG,SAAS,CAAA;AAE3C,MAAM,MAAM,gCAAgC,GAAG,OAAO,CAAC,aAAa,CAAC,GACjE,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAA;AAE7D,MAAM,WAAW,gCAAiC,SAAQ,mBAAmB,EAAE,kBAAkB;IAC/F,YAAY,CAAC,EAAE,2BAA2B,CAAA;CAC3C;AAED,MAAM,MAAM,mCAAmC,GAAG,mBAAmB,GACjE,IAAI,CAAC,kBAAkB,EAAE,cAAc,GAAG,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAC,CAAA;AAulBzG,wBAAgB,iCAAiC,CAC/C,OAAO,GAAE,IAAI,CAAC,gCAAgC,EAAE,cAAc,CAAC,GAAG;IAChE,qBAAqB,CAAC,EAAE,mCAAmC,CAAA;CACvD,GACL,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAQvC;AAED,wBAAgB,oCAAoC,CAClD,OAAO,GAAE,IAAI,CAAC,gCAAgC,EAAE,cAAc,CAAM,GACnE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAIjC;AAED,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAEtF;AAED,wBAAgB,iCAAiC,CAAC,CAAC,SAAS,gCAAgC,EAC1F,UAAU,EAAE,OAAO,EACnB,OAAO,CAAC,EAAE,CAAC,GACV,CAAC,GAAG,aAAa,CAOnB;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,OAAO,EACnB,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,YAAY,CAAC,CAsBvB"}
package/dist/vue.js CHANGED
@@ -1,18 +1,25 @@
1
- import { D as a, c as s, f as d, g as n, h as t, i as o, j as R, k as c, l as i, m as l, n as m, o as A, q as E, s as h, e as g } from "./processor-CHbNEcN8.js";
1
+ import { D as a, c as d, f as s, g as R, h as n, i as o, j as t, k as c, l, m, n as E, o as i, q as A, s as C, e as D } from "./processor-BOCQYqXE.js";
2
+ import { A as h, c as u, a as T, b as V, n as f, r as g } from "./readonly-record-renderer-CkzY7UvT.js";
2
3
  export {
4
+ h as AIMD_RECORD_RENDER_SCOPES,
3
5
  a as DEFAULT_AIMD_RENDERER_LOCALE,
4
- s as createAimdRendererMessages,
5
- d as createAssetRenderer,
6
- n as createCodeBlockRenderer,
7
- t as createComponentRenderer,
6
+ d as createAimdRendererMessages,
7
+ s as createAssetRenderer,
8
+ R as createCodeBlockRenderer,
9
+ n as createComponentRenderer,
8
10
  o as createEmbeddedRenderer,
9
- R as createMermaidRenderer,
11
+ t as createMermaidRenderer,
12
+ u as createReadonlyRecordAimdRenderers,
13
+ T as createReadonlyRecordElementRenderers,
14
+ V as createReadonlyRecordRenderContext,
10
15
  c as createRenderer,
11
- i as createStepCardRenderer,
12
- l as defaultRenderer,
13
- m as hastToVue,
14
- A as loadShikiHighlighter,
15
- E as renderToVNodes,
16
- h as renderToVue,
17
- g as resolveAimdRendererLocale
16
+ l as createStepCardRenderer,
17
+ m as defaultRenderer,
18
+ E as hastToVue,
19
+ i as loadShikiHighlighter,
20
+ f as normalizeRecordRenderValue,
21
+ g as renderReadonlyRecordToVue,
22
+ A as renderToVNodes,
23
+ C as renderToVue,
24
+ D as resolveAimdRendererLocale
18
25
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@airalogy/aimd-renderer",
3
3
  "type": "module",
4
- "version": "2.6.0",
4
+ "version": "2.7.0",
5
5
  "description": "AIMD (Airalogy Markdown) rendering engines for HTML and Vue",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -64,7 +64,7 @@
64
64
  "shiki": "^2.5.0",
65
65
  "unified": "^11.0.5",
66
66
  "vfile": "^6.0.3",
67
- "@airalogy/aimd-core": "^2.8.0"
67
+ "@airalogy/aimd-core": "^2.9.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/node": "^24.3.0",
@@ -1,4 +1,6 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { mount } from '@vue/test-utils'
2
+ import { h } from 'vue'
3
+ import { describe, expect, it, vi } from 'vitest'
2
4
 
3
5
  import { resolveQuizPreviewOptions } from '../common/quiz-preview'
4
6
  import {
@@ -9,6 +11,11 @@ import {
9
11
  createRenderer,
10
12
  } from '../common/processor'
11
13
  import { getFinalIndent, parseFieldTag } from '../index'
14
+ import {
15
+ createReadonlyRecordRenderContext,
16
+ normalizeRecordRenderValue,
17
+ renderReadonlyRecordToVue,
18
+ } from '../vue/readonly-record-renderer'
12
19
  import { createCodeBlockRenderer, createStepCardRenderer } from '../vue/vue-renderer'
13
20
 
14
21
  function findVNodeByType(node: any, expectedType: string): any | null {
@@ -194,6 +201,36 @@ describe('renderToHtmlSync', () => {
194
201
  expect(html).toContain('Hello')
195
202
  })
196
203
 
204
+ it('renders CriticMarkup review marks to semantic HTML', () => {
205
+ const { html } = renderToHtmlSync('Add {++new++}, delete {--old--}, replace {~~old~>new~~}, comment {>>check units<<}, highlight {==important==}.')
206
+
207
+ expect(html).toContain('<ins class="aimd-critic aimd-critic--addition"')
208
+ expect(html).toContain('data-critic-kind="addition"')
209
+ expect(html).toContain('<del class="aimd-critic aimd-critic--deletion"')
210
+ expect(html).toContain('data-critic-kind="deletion"')
211
+ expect(html).toContain('<span class="aimd-critic aimd-critic--substitution"')
212
+ expect(html).toContain('aimd-critic--substitution-old')
213
+ expect(html).toContain('aimd-critic--substitution-new')
214
+ expect(html).toContain('<span class="aimd-critic aimd-critic--comment"')
215
+ expect(html).toContain('check units')
216
+ expect(html).toContain('<mark class="aimd-critic aimd-critic--highlight"')
217
+ expect(html).toContain('important')
218
+ expect(html).not.toContain('{++new++}')
219
+ expect(html).not.toContain('{--old--}')
220
+ expect(html).not.toContain('{~~old~>new~~}')
221
+ expect(html).not.toContain('{>>check units<<}')
222
+ expect(html).not.toContain('{==important==}')
223
+ })
224
+
225
+ it('does not parse CriticMarkup inside inline code', () => {
226
+ const { html } = renderToHtmlSync('Literal `{++not a mark++}` and `{~~old~>new~~}` remain code.')
227
+
228
+ expect(html).toContain('<code>{++not a mark++}</code>')
229
+ expect(html).toContain('<code>{~~old~>new~~}</code>')
230
+ expect(html).not.toContain('aimd-critic--addition')
231
+ expect(html).not.toContain('aimd-critic--substitution')
232
+ })
233
+
197
234
  it('renders AIMD var fields', () => {
198
235
  const { html, fields } = renderToHtmlSync('{{var|temperature}}')
199
236
  expect(fields.var).toContain('temperature')
@@ -543,6 +580,212 @@ describe('renderToVue', () => {
543
580
  })
544
581
  })
545
582
 
583
+ // ---------------------------------------------------------------------------
584
+ // readonly record rendering
585
+ // ---------------------------------------------------------------------------
586
+
587
+ describe('readonly record rendering', () => {
588
+ it('normalizes bare record data and full record payload wrappers', () => {
589
+ expect(normalizeRecordRenderValue({
590
+ var: { sample_id: { value: 'S-001' } },
591
+ step: { prepare: { checked: true } },
592
+ var_table: {
593
+ samples: [{ sample_id: 'S-001' }],
594
+ },
595
+ })).toEqual({
596
+ var: { sample_id: { value: 'S-001' } },
597
+ step: { prepare: { checked: true } },
598
+ check: {},
599
+ quiz: {},
600
+ var_table: {
601
+ samples: [{ sample_id: 'S-001' }],
602
+ },
603
+ })
604
+
605
+ expect(normalizeRecordRenderValue({
606
+ record_id: 'rec-001',
607
+ data: {
608
+ var: { sample_id: 'S-002' },
609
+ quiz: { qc: ['A', 'B'] },
610
+ },
611
+ })).toEqual({
612
+ var: { sample_id: 'S-002' },
613
+ var_table: {},
614
+ step: {},
615
+ check: {},
616
+ quiz: { qc: ['A', 'B'] },
617
+ })
618
+ })
619
+
620
+ it('creates an edit-mode readonly render context for record-backed documents', () => {
621
+ const context = createReadonlyRecordRenderContext(
622
+ { data: { var: { sample_id: 'S-003' } } },
623
+ { quizPreview: { showAnswers: true } },
624
+ )
625
+
626
+ expect(context.mode).toBe('edit')
627
+ expect(context.readonly).toBe(true)
628
+ expect(context.quizPreview).toEqual({ showAnswers: true })
629
+ expect(context.value?.var.sample_id).toBe('S-003')
630
+ })
631
+
632
+ it('renders record values into AIMD fields without enabling input editing', async () => {
633
+ const { nodes } = await renderReadonlyRecordToVue(
634
+ [
635
+ 'Sample {{var|sample_id: str}}',
636
+ '',
637
+ '{{check|prepared}} Prepared.',
638
+ ].join('\n'),
639
+ {
640
+ data: {
641
+ var: { sample_id: { value: 'S-004' } },
642
+ check: { prepared: { checked: true } },
643
+ },
644
+ },
645
+ {
646
+ groupCheckBodies: true,
647
+ },
648
+ )
649
+
650
+ expect(collectVNodeText(nodes)).toContain('S-004')
651
+ expect(collectVNodeText(nodes)).not.toContain('sample_id')
652
+ const input = findVNodeByType({ children: nodes }, 'input') as any
653
+ expect(input).toBeTruthy()
654
+ expect(input.props.checked).toBe(true)
655
+ expect(input.props.disabled).toBe(true)
656
+ })
657
+
658
+ it('shows readable missing labels only when record values are absent', async () => {
659
+ const { nodes } = await renderReadonlyRecordToVue(
660
+ 'Sample {{var|sample_id: str, title="Sample ID"}}',
661
+ {
662
+ data: {
663
+ var: {},
664
+ },
665
+ },
666
+ )
667
+
668
+ expect(collectVNodeText(nodes)).toContain('Missing')
669
+ expect(collectVNodeText(nodes)).toContain('Sample ID')
670
+ const missing = findVNodeByType({ children: nodes }, 'span') as any
671
+ expect(missing.props.title).toContain('data.var.sample_id')
672
+ })
673
+
674
+ it('renders file-backed image fields with resolved assets', async () => {
675
+ const { nodes } = await renderReadonlyRecordToVue(
676
+ 'Site photo: {{var|site_photo: FileIdPNG}}',
677
+ {
678
+ data: {
679
+ var: {
680
+ site_photo: 'airalogy.id.file.site-photo.png',
681
+ },
682
+ },
683
+ },
684
+ {
685
+ resolveAsset: context => context.fileId === 'airalogy.id.file.site-photo.png'
686
+ ? {
687
+ url: 'blob:site-photo',
688
+ filename: 'site-photo.png',
689
+ mimeType: 'image/png',
690
+ }
691
+ : null,
692
+ },
693
+ )
694
+
695
+ const img = findVNodeByType({ children: nodes }, 'img') as any
696
+ expect(img).toBeTruthy()
697
+ expect(img.props.src).toBe('blob:site-photo')
698
+ expect(img.props.alt).toBe('site-photo.png')
699
+ })
700
+
701
+ it('renders manifest-resolved file fields even when the protocol did not declare FileId type', async () => {
702
+ const { nodes } = await renderReadonlyRecordToVue(
703
+ 'Attachment: {{var|sample_file}}',
704
+ {
705
+ data: {
706
+ var: {
707
+ sample_file: 'airalogy.id.file.sample-note.txt',
708
+ },
709
+ },
710
+ },
711
+ {
712
+ resolveAsset: context => context.fieldPath === 'data.var.sample_file'
713
+ ? {
714
+ href: 'blob:sample-note',
715
+ filename: 'sample-note.txt',
716
+ mimeType: 'text/plain',
717
+ }
718
+ : null,
719
+ },
720
+ )
721
+
722
+ const link = findVNodeByType({ children: nodes }, 'a') as any
723
+ expect(link).toBeTruthy()
724
+ expect(link.props.href).toBe('blob:sample-note')
725
+ expect(collectVNodeText(link)).toContain('sample-note.txt')
726
+ })
727
+
728
+ it('resolves markdown image elements through readonly record assets', async () => {
729
+ const { nodes } = await renderReadonlyRecordToVue(
730
+ '![Calibration chart](airalogy.id.file.chart.svg)',
731
+ { data: { var: {} } },
732
+ {
733
+ resolveAsset: context => context.fileId === 'airalogy.id.file.chart.svg'
734
+ ? {
735
+ url: 'blob:chart',
736
+ filename: 'chart.svg',
737
+ mimeType: 'image/svg+xml',
738
+ }
739
+ : null,
740
+ },
741
+ )
742
+
743
+ const img = findVNodeByType({ children: nodes }, 'img') as any
744
+ expect(img).toBeTruthy()
745
+ expect(img.props.src).toBe('blob:chart')
746
+ expect(img.props.alt).toBe('Calibration chart')
747
+ })
748
+
749
+ it('renders AiralogyMarkdown record values through the AIMD Vue renderer', async () => {
750
+ const { nodes } = await renderReadonlyRecordToVue(
751
+ 'Report:\n\n{{var|report: AiralogyMarkdown}}',
752
+ {
753
+ data: {
754
+ var: {
755
+ report: [
756
+ '# Finding',
757
+ '',
758
+ '![Chart](airalogy.id.file.chart.svg)',
759
+ '',
760
+ 'Nested token: {{var|nested: str, title="Nested value"}}',
761
+ ].join('\n'),
762
+ },
763
+ },
764
+ },
765
+ {
766
+ resolveAsset: context => context.fileId === 'airalogy.id.file.chart.svg'
767
+ ? {
768
+ url: 'blob:chart',
769
+ filename: 'chart.svg',
770
+ mimeType: 'image/svg+xml',
771
+ }
772
+ : null,
773
+ },
774
+ )
775
+
776
+ const wrapper = mount({
777
+ render: () => h('div', nodes),
778
+ })
779
+
780
+ await vi.waitFor(() => {
781
+ expect(wrapper.find('.aimd-record-field--markdown h1').text()).toBe('Finding')
782
+ expect(wrapper.find('.aimd-record-field--markdown img').attributes('src')).toBe('blob:chart')
783
+ })
784
+ expect(wrapper.find('.aimd-record-field--markdown').text()).toContain('Nested value')
785
+ wrapper.unmount()
786
+ })
787
+ })
788
+
546
789
  // ---------------------------------------------------------------------------
547
790
  // createRenderer
548
791
  // ---------------------------------------------------------------------------
@@ -0,0 +1,97 @@
1
+ import type { Element, Text as HastText } from "hast"
2
+ import type {
3
+ AimdCriticMarkupBaseNode,
4
+ AimdCriticMarkupChild,
5
+ AimdCriticMarkupNode,
6
+ AimdCriticSubstitutionNode,
7
+ } from "@airalogy/aimd-core/types"
8
+
9
+ function createTextHast(value: string): HastText {
10
+ return { type: "text", value }
11
+ }
12
+
13
+ function isCriticMarkupNode(node: AimdCriticMarkupChild): node is AimdCriticMarkupNode {
14
+ return typeof node.type === "string" && node.type.startsWith("critic")
15
+ }
16
+
17
+ function renderInlineChildren(state: any, children: AimdCriticMarkupChild[] = []): Array<Element | HastText> {
18
+ return children.flatMap((child) => {
19
+ if (child.type === "text") {
20
+ return [createTextHast(child.value ?? "")] as Array<Element | HastText>
21
+ }
22
+
23
+ if (isCriticMarkupNode(child)) {
24
+ const handler = criticMarkupHandlers[child.type as keyof typeof criticMarkupHandlers] as
25
+ | ((state: any, node: any) => Element)
26
+ | undefined
27
+ if (handler) {
28
+ return [handler(state, child)]
29
+ }
30
+ }
31
+
32
+ const rendered = typeof state.one === "function" ? state.one(child, undefined) : null
33
+ return rendered ? [rendered as Element | HastText] : []
34
+ })
35
+ }
36
+
37
+ function createCriticElement(
38
+ tagName: string,
39
+ kind: string,
40
+ children: Array<Element | HastText>,
41
+ extraClassNames: string[] = [],
42
+ ): Element {
43
+ return {
44
+ type: "element",
45
+ tagName,
46
+ properties: {
47
+ className: ["aimd-critic", `aimd-critic--${kind}`, ...extraClassNames],
48
+ "data-critic-kind": kind,
49
+ },
50
+ children,
51
+ }
52
+ }
53
+
54
+ export const criticMarkupHandlers = {
55
+ criticAddition(state: any, node: AimdCriticMarkupBaseNode): Element {
56
+ return createCriticElement("ins", "addition", renderInlineChildren(state, node.children))
57
+ },
58
+
59
+ criticDeletion(state: any, node: AimdCriticMarkupBaseNode): Element {
60
+ return createCriticElement("del", "deletion", renderInlineChildren(state, node.children))
61
+ },
62
+
63
+ criticHighlight(state: any, node: AimdCriticMarkupBaseNode): Element {
64
+ return createCriticElement("mark", "highlight", renderInlineChildren(state, node.children))
65
+ },
66
+
67
+ criticComment(_state: any, node: AimdCriticMarkupBaseNode): Element {
68
+ const value = node.value.trim()
69
+ return createCriticElement("span", "comment", [
70
+ {
71
+ type: "element",
72
+ tagName: "span",
73
+ properties: { className: ["aimd-critic__comment-label"] },
74
+ children: [createTextHast("Comment")],
75
+ },
76
+ {
77
+ type: "element",
78
+ tagName: "span",
79
+ properties: { className: ["aimd-critic__comment-body"] },
80
+ children: [createTextHast(value)],
81
+ },
82
+ ], value ? [] : ["aimd-critic--empty-comment"])
83
+ },
84
+
85
+ criticSubstitution(state: any, node: AimdCriticSubstitutionNode): Element {
86
+ return createCriticElement("span", "substitution", [
87
+ createCriticElement("del", "deletion", renderInlineChildren(state, node.oldChildren), ["aimd-critic--substitution-old"]),
88
+ {
89
+ type: "element",
90
+ tagName: "span",
91
+ properties: { className: ["aimd-critic__replacement-arrow"], "aria-hidden": "true" },
92
+ children: [createTextHast("->")],
93
+ },
94
+ createCriticElement("ins", "addition", renderInlineChildren(state, node.newChildren), ["aimd-critic--substitution-new"]),
95
+ ])
96
+ },
97
+ }
@@ -21,6 +21,7 @@ import { resolveQuizPreviewOptions } from "./quiz-preview"
21
21
  import { remarkInsertVisibleAssigners, remarkStripAssignerCodeBlocks } from "./assignerVisibility"
22
22
  import { highlightVisibleAssigners } from "./assignerHighlighting"
23
23
  import { annotateStepReferenceSequence } from "./annotateStepReferences"
24
+ import { criticMarkupHandlers } from "./criticMarkup"
24
25
  import { buildFigureChildren, assignFigureSequenceNumbers } from "./figureNumbering"
25
26
 
26
27
  // ---------------------------------------------------------------------------
@@ -511,7 +512,13 @@ import remarkParse from "remark-parse"
511
512
  import remarkRehype from "remark-rehype"
512
513
  import { unified } from "unified"
513
514
 
514
- import { protectAimdInlineTemplates, remarkAimd } from "@airalogy/aimd-core/parser"
515
+ import {
516
+ CRITIC_MARKUP_SUBSTITUTIONS_DATA_KEY,
517
+ protectAimdInlineTemplates,
518
+ protectCriticMarkupSubstitutions,
519
+ remarkAimd,
520
+ remarkCriticMarkup,
521
+ } from "@airalogy/aimd-core/parser"
515
522
  import {
516
523
  formatAimdExampleValue,
517
524
  formatAimdExamples,
@@ -567,14 +574,16 @@ async function ensureMathStylesLoaded(mathEnabled: boolean | undefined): Promise
567
574
 
568
575
  function createAimdParseInput(content: string) {
569
576
  const { content: protectedContent, templates } = protectAimdInlineTemplates(content)
577
+ const { content: criticProtectedContent, substitutions } = protectCriticMarkupSubstitutions(protectedContent)
570
578
  const file: VFile = {
571
579
  data: {
572
580
  aimdInlineTemplates: templates,
581
+ [CRITIC_MARKUP_SUBSTITUTIONS_DATA_KEY]: substitutions,
573
582
  },
574
583
  } as unknown as VFile
575
584
 
576
585
  return {
577
- content: protectedContent,
586
+ content: criticProtectedContent,
578
587
  file,
579
588
  }
580
589
  }
@@ -1512,6 +1521,7 @@ function createBaseProcessor(options: AimdRendererOptions = {}) {
1512
1521
  // to properly parse multiline AIMD syntax like var_table with subvars
1513
1522
  processor.use(remarkAimd)
1514
1523
  processor.use(remarkStripAssignerCodeBlocks)
1524
+ processor.use(remarkCriticMarkup)
1515
1525
 
1516
1526
  // Single line break to <br> conversion (default enabled for AIMD)
1517
1527
  if (breaks) {
@@ -1534,6 +1544,7 @@ export function createHtmlProcessor(options: AimdRendererOptions = {}) {
1534
1544
  handlers: {
1535
1545
  // Custom handler for AIMD nodes
1536
1546
  aimd: aimdHandler,
1547
+ ...criticMarkupHandlers,
1537
1548
  },
1538
1549
  } as any)
1539
1550
  .use(rehypeRaw)
package/src/index.ts CHANGED
@@ -41,6 +41,24 @@ export {
41
41
  } from './common/unified-token-renderer'
42
42
 
43
43
  // Vue renderer exports
44
+ export {
45
+ AIMD_RECORD_RENDER_SCOPES,
46
+ createReadonlyRecordAimdRenderers,
47
+ createReadonlyRecordElementRenderers,
48
+ createReadonlyRecordRenderContext,
49
+ normalizeRecordRenderValue,
50
+ renderReadonlyRecordToVue,
51
+ type AimdRecordRenderScope,
52
+ type AimdRecordRenderValue,
53
+ type ReadonlyRecordAsset,
54
+ type ReadonlyRecordAssetKind,
55
+ type ReadonlyRecordAssetResolveContext,
56
+ type ReadonlyRecordAssetResolver,
57
+ type ReadonlyRecordMarkdownRenderOptions,
58
+ type ReadonlyRecordRenderContextInput,
59
+ type ReadonlyRecordVueRendererOptions,
60
+ } from './vue/readonly-record-renderer'
61
+
44
62
  export {
45
63
  type AimdComponentRenderer,
46
64
  type AimdRendererContext,
@@ -109,3 +109,68 @@
109
109
  visibility: visible;
110
110
  transform: translateY(0);
111
111
  }
112
+
113
+ .aimd-critic {
114
+ border-radius: 4px;
115
+ padding: 0 0.14em;
116
+ box-decoration-break: clone;
117
+ -webkit-box-decoration-break: clone;
118
+ }
119
+
120
+ .aimd-critic--addition {
121
+ background: rgba(22, 163, 74, 0.13);
122
+ color: #166534;
123
+ text-decoration: none;
124
+ }
125
+
126
+ .aimd-critic--deletion {
127
+ background: rgba(220, 38, 38, 0.1);
128
+ color: #991b1b;
129
+ text-decoration-line: line-through;
130
+ text-decoration-thickness: 0.08em;
131
+ }
132
+
133
+ .aimd-critic--highlight {
134
+ background: rgba(250, 204, 21, 0.36);
135
+ color: inherit;
136
+ }
137
+
138
+ .aimd-critic--substitution {
139
+ background: rgba(99, 102, 241, 0.08);
140
+ }
141
+
142
+ .aimd-critic--substitution .aimd-critic {
143
+ margin-inline: 0.04em;
144
+ }
145
+
146
+ .aimd-critic__replacement-arrow {
147
+ color: #64748b;
148
+ font-size: 0.86em;
149
+ font-weight: 600;
150
+ }
151
+
152
+ .aimd-critic--comment {
153
+ display: inline-flex;
154
+ align-items: baseline;
155
+ gap: 0.34em;
156
+ padding: 0.03em 0.44em;
157
+ border: 1px solid rgba(37, 99, 235, 0.22);
158
+ border-radius: 999px;
159
+ background: rgba(37, 99, 235, 0.09);
160
+ color: #1e3a8a;
161
+ font-size: 0.92em;
162
+ line-height: 1.5;
163
+ vertical-align: baseline;
164
+ }
165
+
166
+ .aimd-critic__comment-label {
167
+ color: #2563eb;
168
+ font-size: 0.78em;
169
+ font-weight: 700;
170
+ letter-spacing: 0.02em;
171
+ text-transform: uppercase;
172
+ }
173
+
174
+ .aimd-critic__comment-body {
175
+ color: inherit;
176
+ }
package/src/vue/index.ts CHANGED
@@ -23,6 +23,24 @@ export {
23
23
  type VueRendererOptions,
24
24
  } from './vue-renderer'
25
25
 
26
+ export {
27
+ AIMD_RECORD_RENDER_SCOPES,
28
+ createReadonlyRecordAimdRenderers,
29
+ createReadonlyRecordElementRenderers,
30
+ createReadonlyRecordRenderContext,
31
+ normalizeRecordRenderValue,
32
+ renderReadonlyRecordToVue,
33
+ type AimdRecordRenderScope,
34
+ type AimdRecordRenderValue,
35
+ type ReadonlyRecordAsset,
36
+ type ReadonlyRecordAssetKind,
37
+ type ReadonlyRecordAssetResolveContext,
38
+ type ReadonlyRecordAssetResolver,
39
+ type ReadonlyRecordMarkdownRenderOptions,
40
+ type ReadonlyRecordRenderContextInput,
41
+ type ReadonlyRecordVueRendererOptions,
42
+ } from './readonly-record-renderer'
43
+
26
44
  export {
27
45
  renderToVue,
28
46
  createRenderer,