@glossarist/concept-browser 0.7.113 → 0.7.114

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 CHANGED
@@ -293,6 +293,48 @@ The page appears in the sidebar navigation automatically.
293
293
 
294
294
  See [`site-config.example.yml`](site-config.example.yml) for all options including UI languages, fonts, features, and dataset groups.
295
295
 
296
+ ## Data/Deployment Boundary
297
+
298
+ Concept-browser enforces a strict separation between **data** (authored by dataset authors) and **deployment** (configured by deployers):
299
+
300
+ - **Dataset authors** write concepts, sources, bibliography, and inline mentions. They don't know where their dataset will be deployed or what other datasets will be co-deployed.
301
+ - **Deployers** write `site-config.yml` to register datasets, declare URI patterns, and optionally add routing entries for external datasets. They never edit dataset content.
302
+ - **Concept-browser** resolves every cross-reference at runtime via a fixed cascade, making multiple datasets behave as one coherent whole.
303
+
304
+ ### The resolution cascade
305
+
306
+ Every `{{cite:...}}` and `{{urn:...}}` mention walks this cascade at render time:
307
+
308
+ 1. **`uriPatterns`** — is there a co-deployed dataset that matches? → internal link (case 1)
309
+ 2. **`routing[]`** — is there a routing entry for the URI? → external link (case 2)
310
+ 3. **`citation.link`** — does the source have a canonical link? → flat bib record (case 3)
311
+ 4. **Unresolved** — plain text
312
+
313
+ The same YAML renders differently in different deployments. The data never changes.
314
+
315
+ ## Inline Content Syntax
316
+
317
+ All inline references in concept text use the unified `{{kind:target}}` notation:
318
+
319
+ | Kind | Example | What it does |
320
+ |---|---|---|
321
+ | `cite` | `{{cite:sourceId}}` | Cite a `ConceptSource` from this concept's `sources[]` — walks the full resolution cascade |
322
+ | `urn` | `{{urn:iso:std:iso:704}}` | Reference a concept via URN routing |
323
+ | `fig` | `{{fig:diagram_3}}` | Reference a figure entity in the same dataset |
324
+ | `table` | `{{table:units}}` | Reference a table entity |
325
+ | `formula` | `{{formula:ohm_law}}` | Reference a formula entity |
326
+ | `bib` | `{{bib:ref_1}}` | Reference a bibliography entry (case-3-only, no underlying concept) |
327
+ | `link` | `{{link:https://example.com}}` | External URL (canonical, deployment-independent) |
328
+ | `image` | `{{image:src, alt}}` | Inline image embed |
329
+ | *(none)* | `{{measurement unit}}` | Designation match in same dataset |
330
+ | *(none)* | `{{112-01-10}}` | Numeric ID match in same dataset |
331
+
332
+ Each kind accepts an optional label: `{{kind:target, label}}`.
333
+
334
+ **Deprecated:** `<<ref,title>>` (AsciiDoc xref syntax) emits a deprecation warning. Migrate to `{{kind:target}}`.
335
+
336
+ See `/learn/inline-content` on any deployed site for a full interactive reference.
337
+
296
338
  ## CLI Commands
297
339
 
298
340
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.113",
3
+ "version": "0.7.114",
4
4
  "description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { renderContent, cleanContent, type RenderOptions } from '../../utils/content-renderer';
3
+
4
+ /**
5
+ * Unified mention syntax contract — {{kind:target[, label]}}.
6
+ *
7
+ * Pins the dispatch for every kind and the deprecation behavior for
8
+ * the legacy <<ref,title>> AsciiDoc xref syntax.
9
+ */
10
+
11
+ describe('content-renderer — unified mention kinds', () => {
12
+
13
+ describe('{{link:URL}} — external link', () => {
14
+ it('renders bare URL as external anchor', () => {
15
+ const html = renderContent('{{link:https://example.com}}');
16
+ expect(html).toContain('<a href="https://example.com"');
17
+ expect(html).toContain('target="_blank"');
18
+ expect(html).toContain('rel="noopener"');
19
+ expect(html).toContain('https://example.com');
20
+ });
21
+
22
+ it('renders URL with label', () => {
23
+ const html = renderContent('{{link:https://example.com, click here}}');
24
+ expect(html).toContain('click here');
25
+ expect(html).toContain('href="https://example.com"');
26
+ });
27
+
28
+ it('delegates to linkResolver when provided', () => {
29
+ const opts: RenderOptions = { linkResolver: (url, label) => `<a href="${url}" class="custom">${label}</a>` };
30
+ const html = renderContent('{{link:https://example.com, test}}', opts);
31
+ expect(html).toContain('class="custom"');
32
+ expect(html).toContain('test');
33
+ });
34
+ });
35
+
36
+ describe('{{image:src}} — inline image embed', () => {
37
+ it('renders bare src as img tag', () => {
38
+ const html = renderContent('{{image:photo.png}}');
39
+ expect(html).toContain('<img');
40
+ expect(html).toContain('src="photo.png"');
41
+ });
42
+
43
+ it('renders src with alt text', () => {
44
+ const html = renderContent('{{image:photo.png, A photo of the setup}}');
45
+ expect(html).toContain('src="photo.png"');
46
+ expect(html).toContain('alt="A photo of the setup"');
47
+ });
48
+
49
+ it('delegates to imageResolver when provided', () => {
50
+ const opts: RenderOptions = { imageResolver: (src, alt) => `<img src="/base/${src}" alt="${alt}" class="custom-img" />` };
51
+ const html = renderContent('{{image:photo.png, test}}', opts);
52
+ expect(html).toContain('src="/base/photo.png"');
53
+ expect(html).toContain('class="custom-img"');
54
+ });
55
+ });
56
+
57
+ describe('{{bib:id}} — bibliography entry', () => {
58
+ it('renders id as bib-ref span when no resolver', () => {
59
+ const html = renderContent('{{bib:ref_1}}');
60
+ expect(html).toContain('bib-ref');
61
+ expect(html).toContain('ref_1');
62
+ });
63
+
64
+ it('renders label when provided', () => {
65
+ const html = renderContent('{{bib:ref_1, ISO 704}}');
66
+ expect(html).toContain('ISO 704');
67
+ });
68
+
69
+ it('delegates to bibResolver when provided', () => {
70
+ const opts: RenderOptions = { bibResolver: (id, title) => `<a href="/bib/${id}" class="bib-link">${title}</a>` };
71
+ const html = renderContent('{{bib:ref_1, ISO 704}}', opts);
72
+ expect(html).toContain('href="/bib/ref_1"');
73
+ expect(html).toContain('ISO 704');
74
+ });
75
+ });
76
+
77
+ describe('cleanContent — new kinds stripped correctly', () => {
78
+ it('strips {{link:URL}} to the URL', () => {
79
+ expect(cleanContent('{{link:https://example.com}}')).toBe('https://example.com');
80
+ });
81
+
82
+ it('strips {{link:URL, label}} to the label', () => {
83
+ expect(cleanContent('{{link:https://example.com, click here}}')).toBe('click here');
84
+ });
85
+
86
+ it('strips {{image:src}} to empty string', () => {
87
+ expect(cleanContent('{{image:photo.png}}')).toBe('');
88
+ });
89
+
90
+ it('strips {{image:src, alt}} to the alt text', () => {
91
+ expect(cleanContent('{{image:photo.png, A photo}}')).toBe('A photo');
92
+ });
93
+
94
+ it('strips {{bib:id}} to the id', () => {
95
+ expect(cleanContent('{{bib:ref_1}}')).toBe('ref_1');
96
+ });
97
+
98
+ it('strips {{bib:id, label}} to the label', () => {
99
+ expect(cleanContent('{{bib:ref_1, ISO 704}}')).toBe('ISO 704');
100
+ });
101
+ });
102
+ });
103
+
104
+ describe('content-renderer — legacy <<ref,title>> deprecation', () => {
105
+ afterEach(() => {
106
+ vi.restoreAllMocks();
107
+ });
108
+
109
+ it('emits a deprecation warning via console.warn', () => {
110
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
111
+ renderContent('See <<845-01-01, 845-01-01>> for details.');
112
+ expect(spy).toHaveBeenCalledTimes(1);
113
+ expect(spy.mock.calls[0][0]).toContain('deprecated');
114
+ expect(spy.mock.calls[0][0]).toContain('<<845-01-01,845-01-01>>');
115
+ });
116
+
117
+ it('renders the title as plain text in a legacy-xref span', () => {
118
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
119
+ const html = renderContent('See <<845-01-01, 845-01-01>> for details.');
120
+ expect(html).toContain('legacy-xref');
121
+ expect(html).toContain('845-01-01');
122
+ // Must NOT contain bib-ref or bib-link (the old wrong behavior)
123
+ expect(html).not.toContain('bib-ref');
124
+ expect(html).not.toContain('bib-link');
125
+ });
126
+
127
+ it('does NOT call bibResolver (cascade is not bypassed)', () => {
128
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
129
+ const bibResolver = vi.fn(() => '<a class="bib-link">mock</a>');
130
+ const opts: RenderOptions = { bibResolver };
131
+ const html = renderContent('<<845-01-01, 845-01-01>>', opts);
132
+ expect(bibResolver).not.toHaveBeenCalled();
133
+ expect(html).not.toContain('bib-link');
134
+ });
135
+
136
+ it('cleanContent extracts the title from <<ref,title>>', () => {
137
+ expect(cleanContent('See <<845-01-01, 845-01-01>>')).toContain('845-01-01');
138
+ });
139
+ });
@@ -17,6 +17,7 @@ export async function getStaticPaths() {
17
17
  { params: { path: 'learn/relationships' } },
18
18
  { params: { path: 'learn/designations' } },
19
19
  { params: { path: 'learn/statuses' } },
20
+ { params: { path: 'learn/inline-content' } },
20
21
  ];
21
22
  for (const ds of datasets) {
22
23
  paths.push({ params: { path: `dataset/${ds.data.id}/about` } });
@@ -118,6 +118,11 @@ export const routes: RouteRecordRaw[] = [
118
118
  name: 'learn-statuses',
119
119
  component: () => import('../views/learn/LearnStatusesView.vue'),
120
120
  },
121
+ {
122
+ path: '/learn/inline-content',
123
+ name: 'learn-inline-content',
124
+ component: () => import('../views/learn/LearnInlineContentView.vue'),
125
+ },
121
126
  {
122
127
  path: '/about',
123
128
  name: 'about-global',
@@ -5,14 +5,22 @@
5
5
  * math placeholders, tables, lists, and text formatting. This is the single
6
6
  * source of truth for content rendering in the browser.
7
7
  *
8
- * Mention kinds routed here (all dispatched by glossarist's `parseMention`):
9
- * - {{fig:id}} / {{fig:id, display}} → nonVerbalRefResolver (figure)
10
- * - {{table:id}} / {{table:id, display}} → nonVerbalRefResolver (table)
11
- * - {{formula:id}} / {{formula:id, display}} → nonVerbalRefResolver (formula)
12
- * - {{cite:key[, render term]}} → citeResolver
13
- * - {{urn:...[, render term]}} → xrefResolver
14
- * - {{concept_id[, render term]}} → conceptRefResolver
15
- * - {{designation[, render term]}} → conceptRefResolver
8
+ * Unified mention syntax: {{kind:target[, label]}}
9
+ * Every inline reference is a typed mention with a kind prefix:
10
+ *
11
+ * - {{cite:key[, label]}} → citeResolver (concept source citation)
12
+ * - {{urn:...[, label]}} → xrefResolver (URN routing)
13
+ * - {{fig:id[, label]}} → nonVerbalRefResolver (figure)
14
+ * - {{table:id[, label]}} → nonVerbalRefResolver (table)
15
+ * - {{formula:id[, label]}} → nonVerbalRefResolver (formula)
16
+ * - {{bib:id[, label]}} → bibResolver (bibliography entry, case-3-only)
17
+ * - {{link:URL[, label]}} → linkResolver (external URL)
18
+ * - {{image:src[, alt]}} → imageResolver (inline image embed)
19
+ * - {{designation[, label]}} → conceptRefResolver (designation match)
20
+ * - {{numeric_id[, label]}} → conceptRefResolver (numeric ID match)
21
+ *
22
+ * Legacy `<<ref,title>>` (AsciiDoc xref) is deprecated; emits a console
23
+ * warning and renders as plain text. Migrate to {{kind:target}} syntax.
16
24
  *
17
25
  * Math-specific helpers (replaceBracketed, mathPlaceholder) are internal.
18
26
  * The v-math directive upgrades the placeholders to Plurimath at runtime.
@@ -29,6 +37,8 @@ export type BibResolver = (refId: string, title: string) => string;
29
37
  export type CiteResolver = (key: string, label: string | null) => string;
30
38
  export type ConceptRefResolver = (conceptId: string, term: string) => string;
31
39
  export type NonVerbalRefResolver = (kind: NonVerbalKind, entityId: string, display?: string) => string;
40
+ export type LinkResolver = (url: string, label: string) => string;
41
+ export type ImageResolver = (src: string, alt: string) => string;
32
42
 
33
43
  export interface RenderOptions {
34
44
  xrefResolver?: XrefResolver;
@@ -36,6 +46,8 @@ export interface RenderOptions {
36
46
  conceptRefResolver?: ConceptRefResolver;
37
47
  citeResolver?: CiteResolver;
38
48
  nonVerbalRefResolver?: NonVerbalRefResolver;
49
+ linkResolver?: LinkResolver;
50
+ imageResolver?: ImageResolver;
39
51
  }
40
52
 
41
53
  // ── Math placeholders ────────────────────────────────────────────────────
@@ -144,12 +156,30 @@ function convertLists(text: string): string {
144
156
 
145
157
  // ── Inline reference resolution ──────────────────────────────────────────
146
158
 
147
- function resolveBibRefs(text: string, opts: RenderOptions): string {
159
+ /**
160
+ * Deprecated AsciiDoc xref syntax: <<refId, title>>
161
+ *
162
+ * This was historically overloaded for:
163
+ * 1. Non-concept entity xrefs (should use {{fig/table/formula:id, label}})
164
+ * 2. Bibliography lookups (should use {{bib:id}} or {{cite:id}})
165
+ * 3. Concept citations (should use {{cite:id}})
166
+ *
167
+ * All three uses are wrong. This function emits a deprecation warning and
168
+ * renders the title as plain text. Datasets should migrate to the unified
169
+ * {{kind:target}} syntax.
170
+ */
171
+ function resolveLegacyXref(text: string): string {
148
172
  return text.replace(/<<([^,>]+),([^>]+)>>/g, (_, refId, title) => {
149
- if (opts.bibResolver) {
150
- return opts.bibResolver(refId.trim(), title.trim());
173
+ const rid = refId.trim();
174
+ const lbl = title.trim();
175
+ if (typeof console !== 'undefined') {
176
+ console.warn(
177
+ `[glossarist] <<${rid},${lbl}>> is deprecated. ` +
178
+ `Use {{fig/table/formula:${rid}, ${lbl}}} for non-concept entities, ` +
179
+ `{{cite:${rid}}} for concept citations, or {{bib:${rid}}} for bibliography.`,
180
+ );
151
181
  }
152
- return `<span class="bib-ref">${escapeHtml(title.trim())}</span>`;
182
+ return `<span class="legacy-xref" title="Deprecated: use {{kind:target}} syntax">${escapeHtml(lbl)}</span>`;
153
183
  });
154
184
  }
155
185
 
@@ -180,6 +210,45 @@ function resolveUrnRefs(text: string, opts: RenderOptions): string {
180
210
 
181
211
  function resolveMentions(text: string, opts: RenderOptions): string {
182
212
  return text.replace(/\{\{([^{}]+?)\}\}/g, (_orig, body) => {
213
+
214
+ // ── New kinds: link, image, bib (pre-parsed before parseMention) ──
215
+ // These follow the same {{kind:target[, label]}} convention as
216
+ // fig/table/formula but are handled here because glossarist's
217
+ // parseMention doesn't recognize them yet. When glossarist-js adds
218
+ // support (per PROMPT-NOW.md), these pre-parse cases can be removed
219
+ // and the switch below will handle them natively.
220
+
221
+ const linkMatch = body.match(/^link:(.+)$/i);
222
+ if (linkMatch) {
223
+ const rest = linkMatch[1];
224
+ const commaIdx = rest.indexOf(',');
225
+ const url = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
226
+ const label = commaIdx > 0 ? rest.slice(commaIdx + 1).trim() : url;
227
+ if (opts.linkResolver) return opts.linkResolver(url, label);
228
+ return `<a href="${escapeAttr(url)}" target="_blank" rel="noopener" class="ext-link">${escapeHtml(label)}</a>`;
229
+ }
230
+
231
+ const imageMatch = body.match(/^image:(.+)$/i);
232
+ if (imageMatch) {
233
+ const rest = imageMatch[1];
234
+ const commaIdx = rest.indexOf(',');
235
+ const src = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
236
+ const alt = commaIdx > 0 ? rest.slice(commaIdx + 1).trim() : '';
237
+ if (opts.imageResolver) return opts.imageResolver(src, alt);
238
+ return `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}" class="inline-image" />`;
239
+ }
240
+
241
+ const bibMatch = body.match(/^bib:(.+)$/i);
242
+ if (bibMatch) {
243
+ const rest = bibMatch[1];
244
+ const commaIdx = rest.indexOf(',');
245
+ const id = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
246
+ const label = commaIdx > 0 ? rest.slice(commaIdx + 1).trim() : id;
247
+ if (opts.bibResolver) return opts.bibResolver(id, label);
248
+ return `<span class="bib-ref">${escapeHtml(label)}</span>`;
249
+ }
250
+
251
+ // ── Existing kinds: handled by glossarist's parseMention ──
183
252
  const parsed = parseMention(body);
184
253
  const p = parsed as unknown as Record<string, unknown>;
185
254
 
@@ -253,10 +322,10 @@ function resolveMentions(text: string, opts: RenderOptions): string {
253
322
  * 2. AsciiDoc tables
254
323
  * 3. Bullet and numbered lists
255
324
  * 4. Text formatting (bold, italic, subscript)
256
- * 5. Bibliography cross-references (<<ref,title>>)
257
- * 6. Single-brace URN inline references ({urn:...})
258
- * 7. Mention dispatcher — non-verbal (fig/table/formula), then parseMention
259
- * (cite-ref, urn-ref, numeric, designation)
325
+ * 5. Legacy AsciiDoc xrefs (<<ref,title>> → deprecated, renders as plain text)
326
+ * 6. URN inline references ({urn:...})
327
+ * 7. Mention dispatcher — link/image/bib (pre-parsed), then parseMention
328
+ * (fig/table/formula, cite-ref, urn-ref, numeric, designation)
260
329
  */
261
330
  export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
262
331
  if (!text) return '';
@@ -280,7 +349,7 @@ export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver |
280
349
  result = result.replace(/~([^~]+)~/g, '<sub>$1</sub>');
281
350
 
282
351
  // Stage 4: Reference resolution
283
- result = resolveBibRefs(result, opts);
352
+ result = resolveLegacyXref(result);
284
353
  result = resolveUrnRefs(result, opts);
285
354
 
286
355
  // Stage 5: Mention dispatcher (non-verbal first, then parseMention SSOT)
@@ -296,18 +365,28 @@ export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver |
296
365
  export function cleanContent(text: string): string {
297
366
  if (!text) return '';
298
367
  let result = text
368
+ // Legacy xrefs first (before HTML tag stripping eats <<)
369
+ .replace(/<<([^,>]+),([^>]+)>>/g, '$2')
299
370
  .replace(/<[^>]+>/g, '')
300
371
  .replace(/\*\*([^*]+)\*\*/g, '$1')
301
372
  .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '$1')
302
373
  .replace(/~([^~]+)~/g, '_$1')
303
374
  .replace(/\n[ \t]*\* /g, '; ')
304
- .replace(/<<([^,>]+),([^>]+)>>/g, '$2')
305
- // Non-verbal mentions: {{fig:id, display}} → display; {{fig:id}} → id
375
+ // Entity mentions: {{fig:id, display}} → display; {{fig:id}} → id
306
376
  .replace(/\{\{(?:fig|figure|table|tbl|formula|eq):([^,}]+),\s*([^}]+)\}\}/g, '$2')
307
377
  .replace(/\{\{(?:fig|figure|table|tbl|formula|eq):([^}]+)\}\}/g, '$1')
378
+ // Link mentions: {{link:URL, label}} → label; {{link:URL}} → URL
379
+ .replace(/\{\{link:([^,}]+),\s*([^}]+)\}\}/gi, '$2')
380
+ .replace(/\{\{link:([^}]+)\}\}/gi, '$1')
381
+ // Image mentions: {{image:src, alt}} → alt; {{image:src}} → empty
382
+ .replace(/\{\{image:[^,}]+,\s*([^}]+)\}\}/gi, '$1')
383
+ .replace(/\{\{image:([^}]+)\}\}/gi, '')
384
+ // Bib mentions: {{bib:id, label}} → label; {{bib:id}} → id
385
+ .replace(/\{\{bib:([^,}]+),\s*([^}]+)\}\}/gi, '$2')
386
+ .replace(/\{\{bib:([^}]+)\}\}/gi, '$1')
308
387
  // URN refs — show render term (second part for two-arg, third part for three-arg)
309
- .replace(/\{\{urn:[^,}]+,([^,}]+),([^}]+)\}\}/g, '$1') // three-arg: {{urn:...,term,display}} → term
310
- .replace(/\{\{urn:[^,}]+(?:,([^}]+))?\}\}/g, (_, label) => label ? label.trim() : '') // two-arg or bare
388
+ .replace(/\{\{urn:[^,}]+,([^,}]+),([^}]+)\}\}/g, '$1')
389
+ .replace(/\{\{urn:[^,}]+(?:,([^}]+))?\}\}/g, (_, label) => label ? label.trim() : '')
311
390
  .replace(/\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}/g, '$1')
312
391
  // Cite refs — show render term (or empty if bare)
313
392
  .replace(/\{\{cite:[^,}]+(?:,([^}]+))?\}\}/g, (_, label) => label ? label.trim() : '')
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * LearnInlineContentView — unified inline content syntax reference.
4
+ *
5
+ * Documents every {{kind:target}} mention kind with examples,
6
+ * explains the data/deployment boundary for citations, and shows
7
+ * the resolution cascade.
8
+ */
9
+ import { computed } from 'vue';
10
+ import { useI18n } from '../../i18n';
11
+ import LearnCard from '../../components/learn/LearnCard.vue';
12
+
13
+ const { t } = useI18n();
14
+
15
+ const introText = computed(() =>
16
+ t('learn.inlineContent.intro') ||
17
+ 'Every inline reference in concept text uses the unified mustache-kind:target notation. The first word (before the colon) tells you what kind of reference it is.',
18
+ );
19
+
20
+ const mentionKinds = [
21
+ { kind: 'cite', syntax: '{{cite:sourceId}}', example: '{{cite:iso7301}}', desc: 'Cite a ConceptSource from this concept\'s sources[] list. Walks the full resolution cascade (co-deployed → routed → bib).' },
22
+ { kind: 'cite + label', syntax: '{{cite:sourceId, label}}', example: '{{cite:iso7301, rice}}', desc: 'Same as cite, with an explicit display label.' },
23
+ { kind: 'urn', syntax: '{{urn:URN}}', example: '{{urn:iso:std:iso:704}}', desc: 'Reference a concept via URN routing. The deployment decides which dataset (if any) handles this URN.' },
24
+ { kind: 'fig', syntax: '{{fig:id}}', example: '{{fig:diagram_3}}', desc: 'Reference a figure entity in the same dataset.' },
25
+ { kind: 'table', syntax: '{{table:id}}', example: '{{table:unit_list}}', desc: 'Reference a table entity.' },
26
+ { kind: 'formula', syntax: '{{formula:id}}', example: '{{formula:ohm_law}}', desc: 'Reference a formula entity.' },
27
+ { kind: 'bib', syntax: '{{bib:id}}', example: '{{bib:ref_1}}', desc: 'Bibliography entry (case-3-only — no underlying concept). Renders as a flat reference.' },
28
+ { kind: 'link', syntax: '{{link:URL}}', example: '{{link:https://example.com}}', desc: 'External URL. The URL is canonical; the deployment has no say.' },
29
+ { kind: 'link + label', syntax: '{{link:URL, label}}', example: '{{link:https://example.com, click here}}', desc: 'External URL with display label.' },
30
+ { kind: 'image', syntax: '{{image:src}}', example: '{{image:diagram.png}}', desc: 'Inline image embed. Local images get basePath-prefixed.' },
31
+ { kind: 'image + alt', syntax: '{{image:src, alt}}', example: '{{image:diagram.png, The diagram}}', desc: 'Image with accessibility alt text.' },
32
+ { kind: 'designation', syntax: '{{designation}}', example: '{{measurement unit}}', desc: 'Reference a concept by matching its designation text (same dataset).' },
33
+ { kind: 'numeric', syntax: '{{numeric_id}}', example: '{{112-01-10}}', desc: 'Reference a concept by numeric ID (same dataset).' },
34
+ ];
35
+
36
+ const cascade = [
37
+ { step: 1, name: 'uriPatterns', desc: 'Is there a co-deployed dataset that matches this URI?', result: 'Internal link (case 1)' },
38
+ { step: 2, name: 'routing[]', desc: 'Is there a routing entry for this URI in site-config.yml?', result: 'External link (case 2)' },
39
+ { step: 3, name: 'citation.link', desc: 'Does the source have a canonical link?', result: 'Flat bib record (case 3)' },
40
+ { step: 4, name: 'unresolved', desc: 'No match anywhere', result: 'Plain text' },
41
+ ];
42
+ </script>
43
+
44
+ <template>
45
+ <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
46
+ <h1 class="font-title text-3xl text-ink-800 dark:text-ink-50 mb-2">
47
+ {{ t('learn.inlineContent.title') || 'Inline Content Syntax' }}
48
+ </h1>
49
+ <p class="text-ink-500 dark:text-ink-400">
50
+ {{ introText }}
51
+ </p>
52
+
53
+ <LearnCard title="Mention Kinds">
54
+ <template #body>
55
+ <div class="overflow-x-auto">
56
+ <table class="w-full text-sm">
57
+ <thead>
58
+ <tr class="border-b border-ink-100 dark:border-ink-700 text-left">
59
+ <th class="py-2 pr-4">Kind</th>
60
+ <th class="py-2 pr-4">Syntax</th>
61
+ <th class="py-2 pr-4">Example</th>
62
+ <th class="py-2">Description</th>
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ <tr v-for="row in mentionKinds" :key="row.syntax" class="border-b border-ink-50 dark:border-ink-800">
67
+ <td class="py-2 pr-4 font-mono text-xs text-ink-600 dark:text-ink-300">{{ row.kind }}</td>
68
+ <td class="py-2 pr-4 font-mono text-xs text-ink-700 dark:text-ink-200">{{ row.syntax }}</td>
69
+ <td class="py-2 pr-4 font-mono text-xs text-emerald-700 dark:text-emerald-300">{{ row.example }}</td>
70
+ <td class="py-2 text-xs text-ink-500 dark:text-ink-400">{{ row.desc }}</td>
71
+ </tr>
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+ </template>
76
+ </LearnCard>
77
+
78
+ <LearnCard title="Data / Deployment Boundary">
79
+ <template #body>
80
+ <div class="space-y-3 text-sm text-ink-600 dark:text-ink-300">
81
+ <p>
82
+ <strong>Dataset authors</strong> write citations in a deployment-agnostic way.
83
+ They don't know where their dataset will be deployed or what other datasets will be co-deployed.
84
+ </p>
85
+ <p>
86
+ <strong>Deployers</strong> write <code class="text-xs bg-ink-50 dark:bg-ink-800 px-1 rounded">site-config.yml</code>
87
+ to register datasets and declare URI patterns. They never edit dataset content.
88
+ </p>
89
+ <p>
90
+ <strong>Concept-browser</strong> resolves every citation at runtime. The same YAML works in any deployment.
91
+ </p>
92
+ </div>
93
+ </template>
94
+ </LearnCard>
95
+
96
+ <LearnCard title="Resolution Cascade">
97
+ <template #body>
98
+ <div class="space-y-2">
99
+ <div v-for="step in cascade" :key="step.step" class="flex items-start gap-3 py-2">
100
+ <span class="flex-shrink-0 w-6 h-6 rounded-full bg-ink-100 dark:bg-ink-700 text-ink-600 dark:text-ink-300 text-xs font-mono flex items-center justify-center">{{ step.step }}</span>
101
+ <div class="flex-1">
102
+ <div class="font-mono text-xs text-ink-700 dark:text-ink-200">{{ step.name }}</div>
103
+ <div class="text-xs text-ink-500 dark:text-ink-400">{{ step.desc }}</div>
104
+ <div class="text-xs text-emerald-700 dark:text-emerald-300 mt-0.5">{{ step.result }}</div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </template>
109
+ </LearnCard>
110
+
111
+ <LearnCard title="Deprecated: <<ref,title>>">
112
+ <template #body>
113
+ <div class="text-sm text-ink-600 dark:text-ink-300 space-y-2">
114
+ <p>
115
+ The AsciiDoc xref syntax <code class="text-xs bg-amber-50 dark:bg-amber-900/20 px-1 rounded">&lt;&lt;ref,title&gt;&gt;</code>
116
+ is deprecated. It emits a console warning and renders as plain text.
117
+ </p>
118
+ <p>Migrate to the unified syntax:</p>
119
+ <ul class="list-disc list-inside text-xs space-y-1">
120
+ <li><code>&lt;&lt;fig_3,Figure 3&gt;&gt;</code> → <code class="text-emerald-700 dark:text-emerald-300" v-text="'{{fig:fig_3, Figure 3}}'"></code></li>
121
+ <li><code>&lt;&lt;845-01-01, 845-01-01&gt;&gt;</code> → <code class="text-emerald-700 dark:text-emerald-300" v-text="'{{cite:iev_845-01-01}}'"></code></li>
122
+ <li><code>&lt;&lt;ref_1, ISO 704&gt;&gt;</code> → <code class="text-emerald-700 dark:text-emerald-300" v-text="'{{bib:ref_1, ISO 704}}'"></code></li>
123
+ </ul>
124
+ </div>
125
+ </template>
126
+ </LearnCard>
127
+ </div>
128
+ </template>