@glossarist/concept-browser 0.7.43 → 0.7.45
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/cli/index.mjs +12 -13
- package/package.json +3 -2
- package/scripts/__tests__/fetch-datasets.test.mjs +105 -0
- package/scripts/fetch-datasets.mjs +53 -51
- package/scripts/generate-data.mjs +41 -19
- package/scripts/lib/build/image-assets.mjs +190 -0
- package/scripts/lib/build/non-verbal-consumer.mjs +221 -0
- package/scripts/lib/local-path-safety.mjs +68 -0
- package/src/__tests__/bibliography-adapter.test.ts +79 -0
- package/src/__tests__/content-renderer-nvr-mentions.test.ts +57 -0
- package/src/__tests__/locale.test.ts +46 -0
- package/src/__tests__/model-bridge-entity-refs.test.ts +114 -0
- package/src/__tests__/non-verbal-anchor.test.ts +33 -0
- package/src/__tests__/non-verbal-cross-ref.test.ts +146 -0
- package/src/__tests__/non-verbal-highlight.test.ts +56 -0
- package/src/__tests__/non-verbal-kind.test.ts +77 -0
- package/src/__tests__/non-verbal-list.test.ts +67 -0
- package/src/__tests__/non-verbal-rep-display.test.ts +85 -0
- package/src/__tests__/non-verbal-scroll-guard.test.ts +116 -0
- package/src/__tests__/use-concept-entities.test.ts +76 -0
- package/src/adapters/bibliography-adapter.ts +49 -0
- package/src/adapters/factory.ts +14 -0
- package/src/adapters/model-bridge.ts +51 -0
- package/src/adapters/non-verbal/figure-bridge.ts +101 -0
- package/src/adapters/non-verbal/formula-bridge.ts +48 -0
- package/src/adapters/non-verbal/index.ts +55 -0
- package/src/adapters/non-verbal/kind.ts +46 -0
- package/src/adapters/non-verbal/prefix.ts +67 -0
- package/src/adapters/non-verbal/source-bridge.ts +81 -0
- package/src/adapters/non-verbal/table-bridge.ts +98 -0
- package/src/adapters/non-verbal/types.ts +133 -0
- package/src/adapters/non-verbal-resolver.ts +101 -0
- package/src/components/ConceptDetail.vue +17 -4
- package/src/components/LanguageDetail.vue +0 -3
- package/src/components/NonVerbalRepDisplay.vue +82 -24
- package/src/components/figure/FigureDisplay.vue +132 -0
- package/src/components/figure/FigureImages.vue +111 -0
- package/src/components/figure/figure-image-pick.ts +56 -0
- package/src/components/figure/figure-layout.ts +26 -0
- package/src/components/formula/FormulaDisplay.vue +90 -0
- package/src/components/formula/FormulaExpression.vue +70 -0
- package/src/components/non-verbal/NonVerbalCaption.vue +104 -0
- package/src/components/non-verbal/NonVerbalFallback.vue +69 -0
- package/src/components/non-verbal/NonVerbalList.vue +118 -0
- package/src/components/non-verbal/NonVerbalSources.vue +61 -0
- package/src/components/table/TableDisplay.vue +99 -0
- package/src/components/table/TableMarkup.vue +63 -0
- package/src/components/table/TableStructured.vue +66 -0
- package/src/composables/use-concept-entities.ts +70 -0
- package/src/composables/use-non-verbal-cross-ref.ts +79 -0
- package/src/composables/use-non-verbal-entity.ts +58 -0
- package/src/composables/use-reduced-motion.ts +26 -0
- package/src/composables/use-render-options.ts +30 -33
- package/src/router/index.ts +3 -0
- package/src/router/non-verbal-scroll-guard.ts +56 -0
- package/src/style.css +17 -0
- package/src/utils/content-renderer.ts +76 -64
- package/src/utils/locale.ts +92 -0
- package/src/utils/non-verbal-anchor.ts +51 -0
- package/src/utils/non-verbal-highlight.ts +27 -0
|
@@ -5,28 +5,39 @@
|
|
|
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]}} → urnRefResolver / xrefResolver
|
|
14
|
+
* - {{concept_id[, render term]}} → conceptRefResolver
|
|
15
|
+
* - {{designation[, render term]}} → conceptRefResolver
|
|
16
|
+
*
|
|
8
17
|
* Math-specific helpers (replaceBracketed, mathPlaceholder) are internal.
|
|
9
|
-
* The v-math directive upgrades the placeholders to
|
|
18
|
+
* The v-math directive upgrades the placeholders to Plurimath at runtime.
|
|
10
19
|
*/
|
|
11
20
|
import { escapeHtml, escapeAttr } from './escape';
|
|
12
21
|
import { parseMention } from 'glossarist';
|
|
22
|
+
import type { NonVerbalKind } from '../adapters/non-verbal/types';
|
|
23
|
+
import { entityKindFromMentionKind } from '../adapters/non-verbal/kind';
|
|
13
24
|
|
|
14
25
|
// ── Resolver types ────────────────────────────────────────────────────────
|
|
15
26
|
|
|
16
27
|
export type XrefResolver = (uri: string, term: string) => string;
|
|
17
28
|
export type BibResolver = (refId: string, title: string) => string;
|
|
18
|
-
export type FigResolver = (figId: string) => string;
|
|
19
29
|
export type CiteResolver = (key: string, label: string | null) => string;
|
|
20
30
|
export type ConceptRefResolver = (conceptId: string, term: string) => string;
|
|
21
31
|
export type UrnRefResolver = (uri: string, term: string) => string;
|
|
32
|
+
export type NonVerbalRefResolver = (kind: NonVerbalKind, entityId: string, display?: string) => string;
|
|
22
33
|
|
|
23
34
|
export interface RenderOptions {
|
|
24
35
|
xrefResolver?: XrefResolver;
|
|
25
36
|
bibResolver?: BibResolver;
|
|
26
|
-
figResolver?: FigResolver;
|
|
27
37
|
conceptRefResolver?: ConceptRefResolver;
|
|
28
38
|
citeResolver?: CiteResolver;
|
|
29
39
|
urnRefResolver?: UrnRefResolver;
|
|
40
|
+
nonVerbalRefResolver?: NonVerbalRefResolver;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
// ── Math placeholders ────────────────────────────────────────────────────
|
|
@@ -144,19 +155,11 @@ function resolveBibRefs(text: string, opts: RenderOptions): string {
|
|
|
144
155
|
});
|
|
145
156
|
}
|
|
146
157
|
|
|
147
|
-
function resolveFigRefs(text: string, opts: RenderOptions): string {
|
|
148
|
-
return text.replace(/<<(fig_[^>]+)>>/g, (_, figId) => {
|
|
149
|
-
if (opts.figResolver) {
|
|
150
|
-
return opts.figResolver(figId.trim());
|
|
151
|
-
}
|
|
152
|
-
return `<span class="fig-ref">${escapeHtml(figId.trim())}</span>`;
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
158
|
function resolveUrnRefs(text: string, opts: RenderOptions): string {
|
|
157
159
|
// Double-brace URN refs: {{urn:...,term}} or {{urn:...,term,display}}
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
+
// These bypass parseMention because the three-arg form has different
|
|
161
|
+
// semantics in the renderer (display shown, not term) vs. cleanContent
|
|
162
|
+
// (term shown for search indexing).
|
|
160
163
|
let result = text.replace(/\{\{(urn:[^,}]+),([^,}]+)(?:,([^}]+))?\}\}/g, (_, uri, term, display) => {
|
|
161
164
|
const t = (display || term).trim();
|
|
162
165
|
if (opts.xrefResolver) {
|
|
@@ -178,60 +181,68 @@ function resolveUrnRefs(text: string, opts: RenderOptions): string {
|
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
function resolveMentions(text: string, opts: RenderOptions): string {
|
|
181
|
-
// Single-pass {{...}} mention dispatcher via parseMention (SSOT)
|
|
182
184
|
return text.replace(/\{\{([^{}]+?)\}\}/g, (_orig, body) => {
|
|
183
185
|
const parsed = parseMention(body);
|
|
186
|
+
const p = parsed as Record<string, unknown>;
|
|
187
|
+
|
|
188
|
+
switch (p.kind) {
|
|
189
|
+
case 'fig-ref':
|
|
190
|
+
case 'table-ref':
|
|
191
|
+
case 'formula-ref': {
|
|
192
|
+
const nvKind = entityKindFromMentionKind(p.kind as string) as NonVerbalKind;
|
|
193
|
+
const entityId = p.key as string;
|
|
194
|
+
const display = (p.label as string) ?? undefined;
|
|
195
|
+
if (opts.nonVerbalRefResolver) {
|
|
196
|
+
return opts.nonVerbalRefResolver(nvKind, entityId, display);
|
|
197
|
+
}
|
|
198
|
+
const label = display ?? entityId;
|
|
199
|
+
return `<span class="nv-ref nv-ref--${nvKind}">${escapeHtml(label)}</span>`;
|
|
200
|
+
}
|
|
184
201
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
202
|
+
case 'cite-ref': {
|
|
203
|
+
const key = p.key as string;
|
|
204
|
+
const label = (p.label as string) ?? null;
|
|
205
|
+
if (opts.citeResolver) return opts.citeResolver(key, label);
|
|
206
|
+
return `<span class="bib-ref">${escapeHtml(label ?? key)}</span>`;
|
|
207
|
+
}
|
|
192
208
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return escapeHtml(label);
|
|
201
|
-
}
|
|
209
|
+
case 'urn-ref': {
|
|
210
|
+
const uri = p.uri as string;
|
|
211
|
+
const label = (p.label as string) ?? uri;
|
|
212
|
+
if (opts.urnRefResolver) return opts.urnRefResolver(uri, label);
|
|
213
|
+
if (opts.xrefResolver) return opts.xrefResolver(uri, label);
|
|
214
|
+
return escapeHtml(label);
|
|
215
|
+
}
|
|
202
216
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
case 'numeric': {
|
|
218
|
+
const id = p.id as string;
|
|
219
|
+
const label = p.label as string | null;
|
|
220
|
+
if (label && opts.conceptRefResolver) {
|
|
221
|
+
return opts.conceptRefResolver(id, label);
|
|
222
|
+
}
|
|
223
|
+
return `<span class="gl-mention">${escapeHtml(id)}</span>`;
|
|
209
224
|
}
|
|
210
|
-
return `<span class="gl-mention">${escapeHtml(id)}</span>`;
|
|
211
|
-
}
|
|
212
225
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
case 'designation': {
|
|
227
|
+
const designation = p.id as string;
|
|
228
|
+
const label = (p.label as string) ?? designation;
|
|
229
|
+
if (opts.conceptRefResolver) {
|
|
230
|
+
return opts.conceptRefResolver(designation, label);
|
|
231
|
+
}
|
|
232
|
+
return escapeHtml(label);
|
|
219
233
|
}
|
|
220
|
-
return escapeHtml(label);
|
|
221
|
-
}
|
|
222
234
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
default: {
|
|
236
|
+
const commaIdx = body.indexOf(',');
|
|
237
|
+
if (commaIdx > 0) {
|
|
238
|
+
const id = body.slice(0, commaIdx).trim();
|
|
239
|
+
const display = body.slice(commaIdx + 1).trim();
|
|
240
|
+
if (opts.conceptRefResolver) return opts.conceptRefResolver(id, display);
|
|
241
|
+
return escapeHtml(display);
|
|
242
|
+
}
|
|
243
|
+
return `<span class="gl-mention">${escapeHtml(body.trim())}</span>`;
|
|
244
|
+
}
|
|
232
245
|
}
|
|
233
|
-
|
|
234
|
-
return `<span class="gl-mention">${escapeHtml(body.trim())}</span>`;
|
|
235
246
|
});
|
|
236
247
|
}
|
|
237
248
|
|
|
@@ -246,9 +257,9 @@ function resolveMentions(text: string, opts: RenderOptions): string {
|
|
|
246
257
|
* 3. Bullet and numbered lists
|
|
247
258
|
* 4. Text formatting (bold, italic, subscript)
|
|
248
259
|
* 5. Bibliography cross-references (<<ref,title>>)
|
|
249
|
-
* 6.
|
|
250
|
-
* 7.
|
|
251
|
-
*
|
|
260
|
+
* 6. Single-brace URN inline references ({urn:...})
|
|
261
|
+
* 7. Mention dispatcher — non-verbal (fig/table/formula), then parseMention
|
|
262
|
+
* (cite-ref, urn-ref, numeric, designation)
|
|
252
263
|
*/
|
|
253
264
|
export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
|
|
254
265
|
if (!text) return '';
|
|
@@ -273,10 +284,9 @@ export function renderContent(text: string, xrefResolverOrOpts?: XrefResolver |
|
|
|
273
284
|
|
|
274
285
|
// Stage 4: Reference resolution
|
|
275
286
|
result = resolveBibRefs(result, opts);
|
|
276
|
-
result = resolveFigRefs(result, opts);
|
|
277
287
|
result = resolveUrnRefs(result, opts);
|
|
278
288
|
|
|
279
|
-
// Stage 5: Mention dispatcher (parseMention SSOT)
|
|
289
|
+
// Stage 5: Mention dispatcher (non-verbal first, then parseMention SSOT)
|
|
280
290
|
result = resolveMentions(result, opts);
|
|
281
291
|
|
|
282
292
|
return result;
|
|
@@ -295,7 +305,9 @@ export function cleanContent(text: string): string {
|
|
|
295
305
|
.replace(/~([^~]+)~/g, '_$1')
|
|
296
306
|
.replace(/\n[ \t]*\* /g, '; ')
|
|
297
307
|
.replace(/<<([^,>]+),([^>]+)>>/g, '$2')
|
|
298
|
-
|
|
308
|
+
// Non-verbal mentions: {{fig:id, display}} → display; {{fig:id}} → id
|
|
309
|
+
.replace(/\{\{(?:fig|figure|table|tbl|formula|eq):([^,}]+),\s*([^}]+)\}\}/g, '$2')
|
|
310
|
+
.replace(/\{\{(?:fig|figure|table|tbl|formula|eq):([^}]+)\}\}/g, '$1')
|
|
299
311
|
// URN refs — show render term (second part for two-arg, third part for three-arg)
|
|
300
312
|
.replace(/\{\{urn:[^,}]+,([^,}]+),([^}]+)\}\}/g, '$1') // three-arg: {{urn:...,term,display}} → term
|
|
301
313
|
.replace(/\{\{urn:[^,}]+(?:,([^}]+))?\}\}/g, (_, label) => label ? label.trim() : '') // two-arg or bare
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale fallback SSOT.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for localized text resolution across the runtime.
|
|
5
|
+
* Both the non-verbal entity resolver and any other localized content
|
|
6
|
+
* resolution should call `pickLocaleText` / `pickLocaleMap` rather than
|
|
7
|
+
* implement their own fallback chain.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fetchLocalizedString } from 'glossarist';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_FALLBACK_CHAIN: readonly string[] = ['eng'] as const;
|
|
13
|
+
|
|
14
|
+
const RTL_LOCALES: ReadonlySet<string> = new Set(['ara', 'heb', 'fas', 'urd', 'arb']);
|
|
15
|
+
|
|
16
|
+
const ISO_639_2_TO_BCP47: Record<string, string> = {
|
|
17
|
+
eng: 'en', fra: 'fr', deu: 'de', zho: 'zh', ara: 'ar', jpn: 'ja', rus: 'ru',
|
|
18
|
+
kor: 'ko', spa: 'es', ita: 'it', por: 'pt', nld: 'nl', swe: 'sv', fin: 'fi',
|
|
19
|
+
dan: 'da', nob: 'nb', nno: 'nn', nor: 'no', pol: 'pl', tur: 'tr', ces: 'cs', ell: 'el',
|
|
20
|
+
heb: 'he', hin: 'hi', ind: 'id', fas: 'fa', ukr: 'uk', hun: 'hu', ron: 'ro',
|
|
21
|
+
slk: 'sk', slv: 'sl', hrv: 'hr', srp: 'sr', bul: 'bg', msa: 'ms', tha: 'th',
|
|
22
|
+
vie: 'vi', urd: 'ur', ben: 'bn', tam: 'ta', tel: 'te', mar: 'mr', guj: 'gu',
|
|
23
|
+
pan: 'pa', mal: 'ml', kan: 'kn', ori: 'or', asm: 'as', sin: 'si', nep: 'ne',
|
|
24
|
+
lit: 'lt', lav: 'lv', est: 'et', gle: 'ga', cym: 'cy', eus: 'eu', cat: 'ca',
|
|
25
|
+
glg: 'gl', afr: 'af', sqi: 'sq', mkd: 'mk', bel: 'be', kaz: 'kk', uzb: 'uz',
|
|
26
|
+
aze: 'az', hye: 'hy', kat: 'ka', mon: 'mn', tuk: 'tk', uig: 'ug', tgl: 'tl',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type LocalizedText = Record<string, string>;
|
|
30
|
+
|
|
31
|
+
export interface ResolvedLocaleText {
|
|
32
|
+
locale: string;
|
|
33
|
+
text: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function pickLocaleText(
|
|
37
|
+
map: LocalizedText | undefined,
|
|
38
|
+
locale: string,
|
|
39
|
+
fallbackChain: readonly string[] = DEFAULT_FALLBACK_CHAIN,
|
|
40
|
+
): string {
|
|
41
|
+
const r = pickLocaleMap(map, locale, fallbackChain);
|
|
42
|
+
return r?.text ?? '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function pickLocaleMap(
|
|
46
|
+
map: LocalizedText | undefined,
|
|
47
|
+
locale: string,
|
|
48
|
+
fallbackChain: readonly string[] = DEFAULT_FALLBACK_CHAIN,
|
|
49
|
+
): ResolvedLocaleText | null {
|
|
50
|
+
if (!map) return null;
|
|
51
|
+
|
|
52
|
+
// Disable fetchLocalizedString's built-in 'eng' default by passing null —
|
|
53
|
+
// at runtime `null` defeats the default param and the `!= null` guard
|
|
54
|
+
// inside the lib skips its own fallback. The published .d.ts types the
|
|
55
|
+
// parameter as `string | undefined`, so cast through `unknown`. The chain
|
|
56
|
+
// is owned by THIS module: callers see one predictable resolution order.
|
|
57
|
+
const noFallback = null as unknown as undefined;
|
|
58
|
+
const direct = fetchLocalizedString(map, locale, noFallback);
|
|
59
|
+
if (direct != null) return { locale, text: direct };
|
|
60
|
+
|
|
61
|
+
for (const l of fallbackChain) {
|
|
62
|
+
const fb = fetchLocalizedString(map, l, noFallback);
|
|
63
|
+
if (fb != null) return { locale: l, text: fb };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const entries = Object.entries(map);
|
|
67
|
+
if (entries.length === 0) return null;
|
|
68
|
+
return { locale: entries[0][0], text: entries[0][1] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function hasLocale(map: LocalizedText | undefined, locale: string): boolean {
|
|
72
|
+
return !!map && Object.prototype.hasOwnProperty.call(map, locale);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isRtl(locale: string): boolean {
|
|
76
|
+
return RTL_LOCALES.has(locale);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function localeToBcp47(locale: string): string {
|
|
80
|
+
return ISO_639_2_TO_BCP47[locale] ?? locale;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveFallbackChain(datasetLocales?: readonly string[]): readonly string[] {
|
|
84
|
+
if (!datasetLocales || datasetLocales.length === 0) {
|
|
85
|
+
return DEFAULT_FALLBACK_CHAIN;
|
|
86
|
+
}
|
|
87
|
+
const chain = [...datasetLocales];
|
|
88
|
+
for (const l of DEFAULT_FALLBACK_CHAIN) {
|
|
89
|
+
if (!chain.includes(l)) chain.push(l);
|
|
90
|
+
}
|
|
91
|
+
return chain;
|
|
92
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor scheme SSOT for non-verbal entities.
|
|
3
|
+
*
|
|
4
|
+
* The anchor id format is `{kind}-{datasetId}-{entityId}` (e.g.
|
|
5
|
+
* `figure-iala-2023-mixed-reflection`). Components, cross-ref click
|
|
6
|
+
* handlers, router guards, and prose mentions all use this module to
|
|
7
|
+
* compute or match anchor ids — keeping the scheme in one place means
|
|
8
|
+
* changing it later is a one-file edit.
|
|
9
|
+
*
|
|
10
|
+
* The kind prefix is the kind itself (e.g. `figure`), not a shortened
|
|
11
|
+
* alias. This matches the wire format and the anchor selector prefix
|
|
12
|
+
* used by the cross-ref composable.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { NonVerbalKind } from '../adapters/non-verbal/types';
|
|
16
|
+
|
|
17
|
+
const ANCHOR_KIND_PREFIX: Record<NonVerbalKind, string> = {
|
|
18
|
+
figure: 'figure',
|
|
19
|
+
table: 'table',
|
|
20
|
+
formula: 'formula',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const ANCHOR_KIND_SELECTORS: readonly string[] = Object
|
|
24
|
+
.values(ANCHOR_KIND_PREFIX)
|
|
25
|
+
.map(k => `a[href^="#${k}-"]`);
|
|
26
|
+
|
|
27
|
+
export function anchorId(kind: NonVerbalKind, datasetId: string, entityId: string): string {
|
|
28
|
+
return `${ANCHOR_KIND_PREFIX[kind]}-${datasetId}-${entityId}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function anchorSelector(kind: NonVerbalKind, datasetId: string, entityId: string): string {
|
|
32
|
+
return `#${CSS.escape(anchorId(kind, datasetId, entityId))}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedAnchor {
|
|
36
|
+
kind: NonVerbalKind;
|
|
37
|
+
datasetId: string;
|
|
38
|
+
entityId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PARSER_RE = /^(figure|table|formula)-(.+)-(.+)$/;
|
|
42
|
+
|
|
43
|
+
export function parseAnchorId(id: string): ParsedAnchor | null {
|
|
44
|
+
const m = id.match(PARSER_RE);
|
|
45
|
+
if (!m) return null;
|
|
46
|
+
return { kind: m[1] as NonVerbalKind, datasetId: m[2], entityId: m[3] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function hrefFromAnchor(id: string): string {
|
|
50
|
+
return `#${id}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-reference highlight utility — shared by the click handler and the
|
|
3
|
+
* router scroll guard. Both call `highlightEntity()` after scrolling into
|
|
4
|
+
* view; the user sees a brief, accessible focus ring.
|
|
5
|
+
*
|
|
6
|
+
* Honors prefers-reduced-motion: the highlight class is added either way
|
|
7
|
+
* (it's a state indicator), but the smooth-scroll behavior is gated
|
|
8
|
+
* upstream by `useReducedMotion`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const HIGHLIGHT_CLASS = 'nv-entity--highlighted';
|
|
12
|
+
const HIGHLIGHT_DURATION_MS = 1600;
|
|
13
|
+
|
|
14
|
+
export function highlightEntity(el: HTMLElement | null): void {
|
|
15
|
+
if (!el) return;
|
|
16
|
+
el.classList.add(HIGHLIGHT_CLASS);
|
|
17
|
+
el.setAttribute('tabindex', '-1');
|
|
18
|
+
el.focus({ preventScroll: true });
|
|
19
|
+
window.setTimeout(() => {
|
|
20
|
+
el.classList.remove(HIGHLIGHT_CLASS);
|
|
21
|
+
}, HIGHLIGHT_DURATION_MS);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function scrollToEntity(el: HTMLElement | null, smooth: boolean): void {
|
|
25
|
+
if (!el) return;
|
|
26
|
+
el.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
|
|
27
|
+
}
|