@glossarist/concept-browser 0.7.112 → 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 +42 -0
- package/package.json +1 -1
- package/src/__tests__/composables/cite-resolver.test.ts +119 -0
- package/src/__tests__/utils/inline-content-kinds.test.ts +139 -0
- package/src/components/ConceptDetail.vue +11 -4
- package/src/composables/use-render-options.ts +88 -10
- package/src/pages/[...path].astro +1 -0
- package/src/router/index.ts +5 -0
- package/src/utils/content-renderer.ts +100 -24
- package/src/views/learn/LearnInlineContentView.vue +128 -0
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.
|
|
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,119 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import { useConceptEdges } from '../../composables/use-concept-edges';
|
|
4
|
+
import { useVocabularyStore } from '../../stores/vocabulary';
|
|
5
|
+
import type { Manifest, GraphEdge } from '../../adapters/types';
|
|
6
|
+
import { makeManifest, setupPinia } from '../test-helpers';
|
|
7
|
+
import { getFactory, resetFactory } from '../../adapters/factory';
|
|
8
|
+
import { useRenderOptions } from '../../composables/use-render-options';
|
|
9
|
+
import type { Router } from 'vue-router';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* citeResolver contract — pins the {{cite:sourceId}} resolution cascade.
|
|
13
|
+
*
|
|
14
|
+
* The citeResolver must:
|
|
15
|
+
* 1. Look up the concept's own sources[] for a matching id
|
|
16
|
+
* 2. Delegate to ReferenceResolver.resolveCite() which walks the
|
|
17
|
+
* deployment's cascade
|
|
18
|
+
* 3. Render based on classification (internal → navigable link,
|
|
19
|
+
* self-contained → external link with origin.link, unresolved → span)
|
|
20
|
+
*
|
|
21
|
+
* Bug history: citeResolver was dead code (never wired). Every
|
|
22
|
+
* {{cite:foo}} mention fell through to a plain <span> with no resolution.
|
|
23
|
+
* This spec pins the wiring.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
describe('citeResolver — {{cite:sourceId}} resolution', () => {
|
|
27
|
+
const noopRouter = { push() {} } as unknown as Router;
|
|
28
|
+
let store: ReturnType<typeof useVocabularyStore>;
|
|
29
|
+
const manifest: Manifest = makeManifest({
|
|
30
|
+
id: 'test',
|
|
31
|
+
datasetUri: 'urn:test:dataset',
|
|
32
|
+
uriBase: 'https://example.org',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
setupPinia();
|
|
37
|
+
resetFactory();
|
|
38
|
+
store = useVocabularyStore();
|
|
39
|
+
store.manifests.set(manifest.id, manifest);
|
|
40
|
+
|
|
41
|
+
const factory = getFactory();
|
|
42
|
+
factory.resolveRelatedRef = vi.fn((ref: { source: string | null; id: string | null } | null) =>
|
|
43
|
+
ref?.source === 'urn:test:dataset' && ref.id
|
|
44
|
+
? { registerId: manifest.id, conceptId: ref.id }
|
|
45
|
+
: null,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns cite-ref span when no sources are provided', () => {
|
|
50
|
+
const { citeResolver } = useRenderOptions(() => 'test', () => undefined);
|
|
51
|
+
const html = citeResolver('foo', null);
|
|
52
|
+
expect(html).toContain('cite-ref');
|
|
53
|
+
expect(html).toContain('foo');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns cite-unresolved when source id not found in sources', () => {
|
|
57
|
+
const sources = [{ id: 'bar', origin: { ref: { source: 'Test', id: '42' } } }];
|
|
58
|
+
const { citeResolver } = useRenderOptions(() => 'test', () => sources);
|
|
59
|
+
const html = citeResolver('foo', null);
|
|
60
|
+
expect(html).toContain('cite-unresolved');
|
|
61
|
+
expect(html).toContain('foo');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders the label when provided', () => {
|
|
65
|
+
const sources = [{ id: 'foo', origin: { ref: { source: 'Test', id: '42' } } }];
|
|
66
|
+
const { citeResolver } = useRenderOptions(() => 'test', () => sources);
|
|
67
|
+
const html = citeResolver('foo', 'custom label');
|
|
68
|
+
// The resolver runs the cascade. Without a registered sourceRef, it
|
|
69
|
+
// falls through to unresolved or self-contained. Either way, the label
|
|
70
|
+
// or the source info should be in the output.
|
|
71
|
+
expect(html).toMatch(/custom label|Test|42/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders origin.link as external link for self-contained citation', () => {
|
|
75
|
+
const link = 'https://example.org/external/page';
|
|
76
|
+
const sources = [{
|
|
77
|
+
id: 'foo',
|
|
78
|
+
origin: { ref: { source: 'External', id: 'X1' }, link },
|
|
79
|
+
}];
|
|
80
|
+
const { citeResolver } = useRenderOptions(() => 'test', () => sources);
|
|
81
|
+
const html = citeResolver('foo', null);
|
|
82
|
+
// Without a registered sourceRef matching "External", the cascade
|
|
83
|
+
// falls through to self-contained or external. The link should appear.
|
|
84
|
+
expect(html).toContain('href');
|
|
85
|
+
expect(html).toContain(link);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('does NOT crash when source.origin is null', () => {
|
|
89
|
+
const sources = [{ id: 'foo', origin: null }];
|
|
90
|
+
const { citeResolver } = useRenderOptions(() => 'test', () => sources);
|
|
91
|
+
expect(() => citeResolver('foo', null)).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('does NOT crash when sources is empty array', () => {
|
|
95
|
+
const { citeResolver } = useRenderOptions(() => 'test', () => []);
|
|
96
|
+
expect(() => citeResolver('foo', null)).not.toThrow();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('does NOT crash when registerId returns empty string', () => {
|
|
100
|
+
const sources = [{ id: 'foo', origin: { ref: { source: 'X', id: '1' } } }];
|
|
101
|
+
const { citeResolver } = useRenderOptions(() => '', () => sources);
|
|
102
|
+
expect(() => citeResolver('foo', null)).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('RenderOptions — urnRefResolver removed', () => {
|
|
107
|
+
it('content-renderer no longer exports UrnRefResolver type', async () => {
|
|
108
|
+
// RenderOptions is a TypeScript interface (no runtime export).
|
|
109
|
+
// Verify the UrnRefResolver type is gone by checking the source.
|
|
110
|
+
const fs = await import('fs');
|
|
111
|
+
const path = await import('path');
|
|
112
|
+
const source = fs.readFileSync(
|
|
113
|
+
path.resolve(process.cwd(), 'src/utils/content-renderer.ts'),
|
|
114
|
+
'utf-8',
|
|
115
|
+
);
|
|
116
|
+
expect(source).not.toMatch(/UrnRefResolver/);
|
|
117
|
+
expect(source).not.toMatch(/urnRefResolver/);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -117,9 +117,14 @@ const conceptSources = computed(() => props.concept?.sources ?? []);
|
|
|
117
117
|
const conceptTags = computed(() => props.concept?.tags ?? []);
|
|
118
118
|
|
|
119
119
|
const factory = getFactory();
|
|
120
|
-
const { ensureBibLoaded, bibResolver, nonVerbalRefResolver } = useRenderOptions(
|
|
120
|
+
const { ready: bibReady, ensureBibLoaded, bibResolver, citeResolver, nonVerbalRefResolver } = useRenderOptions(
|
|
121
|
+
() => props.registerId,
|
|
122
|
+
() => props.concept?.sources,
|
|
123
|
+
);
|
|
121
124
|
|
|
122
|
-
const renderOpts = computed<RenderOptions>(() =>
|
|
125
|
+
const renderOpts = computed<RenderOptions>(() => {
|
|
126
|
+
bibReady.value;
|
|
127
|
+
return {
|
|
123
128
|
xrefResolver: (uri, term) => {
|
|
124
129
|
const resolution = factory.resolve(uri, props.registerId);
|
|
125
130
|
if (resolution.type === 'internal') {
|
|
@@ -139,8 +144,10 @@ const renderOpts = computed<RenderOptions>(() => ({
|
|
|
139
144
|
return `<a href="#" class="xref-link" data-register="${escapeAttr(props.registerId)}" data-concept="${escapeAttr(resolvedId)}">${escapeAttr(term)}</a>`;
|
|
140
145
|
},
|
|
141
146
|
bibResolver,
|
|
147
|
+
citeResolver,
|
|
142
148
|
nonVerbalRefResolver,
|
|
143
|
-
}
|
|
149
|
+
};
|
|
150
|
+
});
|
|
144
151
|
|
|
145
152
|
watch(() => props.registerId, () => { ensureBibLoaded(); }, { immediate: true });
|
|
146
153
|
|
|
@@ -152,7 +159,7 @@ const structuralEntityRefs = useConceptEntities(
|
|
|
152
159
|
useNonVerbalCrossRef();
|
|
153
160
|
|
|
154
161
|
function handleContentClick(e: MouseEvent) {
|
|
155
|
-
const target = (e.target as HTMLElement).closest('.xref-link') as HTMLElement | null;
|
|
162
|
+
const target = (e.target as HTMLElement).closest('.xref-link, .cite-link') as HTMLElement | null;
|
|
156
163
|
if (!target) return;
|
|
157
164
|
e.preventDefault();
|
|
158
165
|
const registerId = target.dataset.register;
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Render-options composable — builds resolvers for the content renderer.
|
|
3
|
+
*
|
|
4
|
+
* The citeResolver is the load-bearing addition: it connects inline
|
|
5
|
+
* {{cite:sourceId}} mentions to the existing ReferenceResolver cascade
|
|
6
|
+
* (factory.resolver.resolveCite). This closes the data/deployment
|
|
7
|
+
* boundary: the data author writes {{cite:iev_702-02-07}}, the resolver
|
|
8
|
+
* walks the deployment's sourceRefs → routing → link cascade, and the
|
|
9
|
+
* renderer emits the correct HTML based on classification.
|
|
10
|
+
*/
|
|
11
|
+
import { ref, type Ref } from 'vue';
|
|
12
|
+
import type { RenderOptions, BibResolver, CiteResolver, NonVerbalRefResolver } from '../utils/content-renderer';
|
|
3
13
|
import type { NonVerbalKind } from '../adapters/non-verbal/types';
|
|
4
14
|
import { getFactory } from '../adapters/factory';
|
|
5
15
|
import { anchorId } from '../utils/non-verbal-anchor';
|
|
6
|
-
import { escapeAttr } from '../utils/escape';
|
|
16
|
+
import { escapeAttr, escapeHtml } from '../utils/escape';
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
/** Minimal shape of a ConceptSource that citeResolver needs. */
|
|
19
|
+
interface ConceptSourceLike {
|
|
20
|
+
id?: string | null;
|
|
21
|
+
origin?: {
|
|
22
|
+
ref?: { source?: string | null; id?: string | null; version?: string | null } | null;
|
|
23
|
+
locality?: { type?: string | null; reference_from?: string | null; referenceTo?: string | null; reference_to?: string | null; referenceFrom?: string | null } | null;
|
|
24
|
+
link?: string | null;
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useRenderOptions(
|
|
29
|
+
registerId: () => string,
|
|
30
|
+
getSources?: () => readonly ConceptSourceLike[] | undefined,
|
|
31
|
+
) {
|
|
9
32
|
const ready = ref(false);
|
|
10
33
|
|
|
11
34
|
async function ensureBibLoaded() {
|
|
@@ -19,28 +42,80 @@ export function useRenderOptions(registerId: () => string) {
|
|
|
19
42
|
const id = registerId();
|
|
20
43
|
const entry = id ? getFactory().bibliography(id).findById(refId) : null;
|
|
21
44
|
if (!entry) {
|
|
22
|
-
return `<span class="bib-ref">${
|
|
45
|
+
return `<span class="bib-ref">${escapeHtml(title)}</span>`;
|
|
23
46
|
}
|
|
24
47
|
const display = title || entry.reference || refId;
|
|
25
48
|
if (entry.link) {
|
|
26
|
-
return `<a href="${escapeAttr(entry.link)}" target="_blank" rel="noopener" class="bib-link" title="${escapeAttr(entry.
|
|
49
|
+
return `<a href="${escapeAttr(entry.link)}" target="_blank" rel="noopener" class="bib-link" title="${escapeAttr(entry.reference ?? '')}">${escapeHtml(display)}</a>`;
|
|
50
|
+
}
|
|
51
|
+
return `<span class="bib-ref" title="${escapeAttr(entry.reference ?? '')}">${escapeHtml(display)}</span>`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* citeResolver — the canonical path for {{cite:sourceId}} mentions.
|
|
56
|
+
*
|
|
57
|
+
* Looks up the concept's own sources[] for a matching id, then
|
|
58
|
+
* delegates to ReferenceResolver.resolveCite() which walks the
|
|
59
|
+
* deployment's cascade (sourceRefs → routing → link → unresolved).
|
|
60
|
+
*/
|
|
61
|
+
const citeResolver: CiteResolver = (key, label) => {
|
|
62
|
+
const rid = registerId();
|
|
63
|
+
const sources = getSources?.();
|
|
64
|
+
if (!sources || !rid) {
|
|
65
|
+
return `<span class="cite-ref">${escapeHtml(label ?? key)}</span>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const source = sources.find(s => s.id === key);
|
|
69
|
+
if (!source?.origin) {
|
|
70
|
+
return `<span class="cite-unresolved">${escapeHtml(label ?? key)}</span>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const factory = getFactory();
|
|
74
|
+
const resolution = factory.resolver.resolveCite(source.origin, rid);
|
|
75
|
+
const refId = source.origin.ref?.id ?? key;
|
|
76
|
+
const refSource = source.origin.ref?.source ?? '';
|
|
77
|
+
const display = label ?? refId;
|
|
78
|
+
|
|
79
|
+
switch (resolution.classification) {
|
|
80
|
+
case 'internal-citation': {
|
|
81
|
+
if (!resolution.resolved) break;
|
|
82
|
+
const { registerId: targetReg, conceptId } = resolution.resolved;
|
|
83
|
+
const isCross = targetReg !== rid;
|
|
84
|
+
const crossBadge = isCross ? '<span class="text-[9px] opacity-60">↗</span>' : '';
|
|
85
|
+
return `<a href="#" class="cite-link" data-register="${escapeAttr(targetReg)}" data-concept="${escapeAttr(conceptId)}">${escapeHtml(display)} ${crossBadge}</a>`;
|
|
86
|
+
}
|
|
87
|
+
case 'self-contained-citation': {
|
|
88
|
+
const link = source.origin.link;
|
|
89
|
+
if (link) {
|
|
90
|
+
return `<a href="${escapeAttr(link)}" target="_blank" rel="noopener" class="cite-link cite-external">${escapeHtml(refSource || display)} <span class="text-[9px] opacity-60">↗</span></a>`;
|
|
91
|
+
}
|
|
92
|
+
return `<span class="cite-ref">${escapeHtml(refSource || display)}</span>`;
|
|
93
|
+
}
|
|
94
|
+
case 'external-citation': {
|
|
95
|
+
const link = source.origin.link;
|
|
96
|
+
if (link) {
|
|
97
|
+
return `<a href="${escapeAttr(link)}" target="_blank" rel="noopener" class="cite-link cite-external">${escapeHtml(refSource || display)} <span class="text-[9px] opacity-60">↗</span></a>`;
|
|
98
|
+
}
|
|
99
|
+
return `<span class="cite-ref">${escapeHtml(refSource || display)}</span>`;
|
|
100
|
+
}
|
|
27
101
|
}
|
|
28
|
-
|
|
102
|
+
|
|
103
|
+
return `<span class="cite-unresolved">${escapeHtml(display)}</span>`;
|
|
29
104
|
};
|
|
30
105
|
|
|
31
106
|
const nonVerbalRefResolver: NonVerbalRefResolver = (kind: NonVerbalKind, entityId, display) => {
|
|
32
107
|
const id = registerId();
|
|
33
108
|
if (!id) {
|
|
34
109
|
const label = display ?? entityId;
|
|
35
|
-
return `<span class="nv-ref nv-ref--${kind}">${
|
|
110
|
+
return `<span class="nv-ref nv-ref--${kind}">${escapeHtml(label)}</span>`;
|
|
36
111
|
}
|
|
37
112
|
const href = `#${anchorId(kind, id, entityId)}`;
|
|
38
113
|
const label = display ?? defaultLabelForKind(kind, entityId);
|
|
39
114
|
const cls = `nv-ref nv-ref--${kind}`;
|
|
40
|
-
return `<a href="${href}" class="${cls}" data-nv-kind="${escapeAttr(kind)}" data-nv-dataset="${escapeAttr(id)}" data-nv-entity="${escapeAttr(entityId)}">${
|
|
115
|
+
return `<a href="${href}" class="${cls}" data-nv-kind="${escapeAttr(kind)}" data-nv-dataset="${escapeAttr(id)}" data-nv-entity="${escapeAttr(entityId)}">${escapeHtml(label)}</a>`;
|
|
41
116
|
};
|
|
42
117
|
|
|
43
|
-
return { ready, ensureBibLoaded, bibResolver, nonVerbalRefResolver };
|
|
118
|
+
return { ready, ensureBibLoaded, bibResolver, citeResolver, nonVerbalRefResolver };
|
|
44
119
|
}
|
|
45
120
|
|
|
46
121
|
function defaultLabelForKind(kind: NonVerbalKind, entityId: string): string {
|
|
@@ -50,3 +125,6 @@ function defaultLabelForKind(kind: NonVerbalKind, entityId: string): string {
|
|
|
50
125
|
case 'formula': return `Formula ${entityId}`;
|
|
51
126
|
}
|
|
52
127
|
}
|
|
128
|
+
|
|
129
|
+
/** Ensure `ready` is tracked as a Ref for consumers that read `.value`. */
|
|
130
|
+
export type RenderOptionsReady = Ref<boolean>;
|
|
@@ -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` } });
|
package/src/router/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - {{
|
|
12
|
-
* - {{
|
|
13
|
-
* - {{
|
|
14
|
-
* - {{
|
|
15
|
-
* - {{
|
|
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.
|
|
@@ -28,16 +36,18 @@ export type XrefResolver = (uri: string, term: string) => string;
|
|
|
28
36
|
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
|
-
export type UrnRefResolver = (uri: string, term: string) => string;
|
|
32
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;
|
|
33
42
|
|
|
34
43
|
export interface RenderOptions {
|
|
35
44
|
xrefResolver?: XrefResolver;
|
|
36
45
|
bibResolver?: BibResolver;
|
|
37
46
|
conceptRefResolver?: ConceptRefResolver;
|
|
38
47
|
citeResolver?: CiteResolver;
|
|
39
|
-
urnRefResolver?: UrnRefResolver;
|
|
40
48
|
nonVerbalRefResolver?: NonVerbalRefResolver;
|
|
49
|
+
linkResolver?: LinkResolver;
|
|
50
|
+
imageResolver?: ImageResolver;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
// ── Math placeholders ────────────────────────────────────────────────────
|
|
@@ -146,12 +156,30 @@ function convertLists(text: string): string {
|
|
|
146
156
|
|
|
147
157
|
// ── Inline reference resolution ──────────────────────────────────────────
|
|
148
158
|
|
|
149
|
-
|
|
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 {
|
|
150
172
|
return text.replace(/<<([^,>]+),([^>]+)>>/g, (_, refId, title) => {
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
);
|
|
153
181
|
}
|
|
154
|
-
return `<span class="
|
|
182
|
+
return `<span class="legacy-xref" title="Deprecated: use {{kind:target}} syntax">${escapeHtml(lbl)}</span>`;
|
|
155
183
|
});
|
|
156
184
|
}
|
|
157
185
|
|
|
@@ -182,6 +210,45 @@ function resolveUrnRefs(text: string, opts: RenderOptions): string {
|
|
|
182
210
|
|
|
183
211
|
function resolveMentions(text: string, opts: RenderOptions): string {
|
|
184
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 ──
|
|
185
252
|
const parsed = parseMention(body);
|
|
186
253
|
const p = parsed as unknown as Record<string, unknown>;
|
|
187
254
|
|
|
@@ -209,7 +276,6 @@ function resolveMentions(text: string, opts: RenderOptions): string {
|
|
|
209
276
|
case 'urn-ref': {
|
|
210
277
|
const uri = p.uri as string;
|
|
211
278
|
const label = (p.label as string) ?? uri;
|
|
212
|
-
if (opts.urnRefResolver) return opts.urnRefResolver(uri, label);
|
|
213
279
|
if (opts.xrefResolver) return opts.xrefResolver(uri, label);
|
|
214
280
|
return escapeHtml(label);
|
|
215
281
|
}
|
|
@@ -256,10 +322,10 @@ function resolveMentions(text: string, opts: RenderOptions): string {
|
|
|
256
322
|
* 2. AsciiDoc tables
|
|
257
323
|
* 3. Bullet and numbered lists
|
|
258
324
|
* 4. Text formatting (bold, italic, subscript)
|
|
259
|
-
* 5.
|
|
260
|
-
* 6.
|
|
261
|
-
* 7. Mention dispatcher —
|
|
262
|
-
* (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)
|
|
263
329
|
*/
|
|
264
330
|
export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
|
|
265
331
|
if (!text) return '';
|
|
@@ -283,7 +349,7 @@ export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver |
|
|
|
283
349
|
result = result.replace(/~([^~]+)~/g, '<sub>$1</sub>');
|
|
284
350
|
|
|
285
351
|
// Stage 4: Reference resolution
|
|
286
|
-
result =
|
|
352
|
+
result = resolveLegacyXref(result);
|
|
287
353
|
result = resolveUrnRefs(result, opts);
|
|
288
354
|
|
|
289
355
|
// Stage 5: Mention dispatcher (non-verbal first, then parseMention SSOT)
|
|
@@ -299,18 +365,28 @@ export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver |
|
|
|
299
365
|
export function cleanContent(text: string): string {
|
|
300
366
|
if (!text) return '';
|
|
301
367
|
let result = text
|
|
368
|
+
// Legacy xrefs first (before HTML tag stripping eats <<)
|
|
369
|
+
.replace(/<<([^,>]+),([^>]+)>>/g, '$2')
|
|
302
370
|
.replace(/<[^>]+>/g, '')
|
|
303
371
|
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
304
372
|
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '$1')
|
|
305
373
|
.replace(/~([^~]+)~/g, '_$1')
|
|
306
374
|
.replace(/\n[ \t]*\* /g, '; ')
|
|
307
|
-
|
|
308
|
-
// Non-verbal mentions: {{fig:id, display}} → display; {{fig:id}} → id
|
|
375
|
+
// Entity mentions: {{fig:id, display}} → display; {{fig:id}} → id
|
|
309
376
|
.replace(/\{\{(?:fig|figure|table|tbl|formula|eq):([^,}]+),\s*([^}]+)\}\}/g, '$2')
|
|
310
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')
|
|
311
387
|
// URN refs — show render term (second part for two-arg, third part for three-arg)
|
|
312
|
-
.replace(/\{\{urn:[^,}]+,([^,}]+),([^}]+)\}\}/g, '$1')
|
|
313
|
-
.replace(/\{\{urn:[^,}]+(?:,([^}]+))?\}\}/g, (_, label) => label ? label.trim() : '')
|
|
388
|
+
.replace(/\{\{urn:[^,}]+,([^,}]+),([^}]+)\}\}/g, '$1')
|
|
389
|
+
.replace(/\{\{urn:[^,}]+(?:,([^}]+))?\}\}/g, (_, label) => label ? label.trim() : '')
|
|
314
390
|
.replace(/\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}/g, '$1')
|
|
315
391
|
// Cite refs — show render term (or empty if bare)
|
|
316
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"><<ref,title>></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><<fig_3,Figure 3>></code> → <code class="text-emerald-700 dark:text-emerald-300" v-text="'{{fig:fig_3, Figure 3}}'"></code></li>
|
|
121
|
+
<li><code><<845-01-01, 845-01-01>></code> → <code class="text-emerald-700 dark:text-emerald-300" v-text="'{{cite:iev_845-01-01}}'"></code></li>
|
|
122
|
+
<li><code><<ref_1, ISO 704>></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>
|