@glossarist/concept-browser 0.7.50 → 0.7.52

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.
Files changed (170) hide show
  1. package/cli/index.mjs +32 -0
  2. package/env.d.ts +15 -0
  3. package/package.json +12 -2
  4. package/scripts/__tests__/doctor.test.mjs +147 -0
  5. package/scripts/doctor.mjs +327 -0
  6. package/scripts/generate-data.mjs +136 -0
  7. package/scripts/generate-ontology-data.mjs +3 -3
  8. package/scripts/generate-ontology-schema.mjs +3 -3
  9. package/scripts/lib/agents-turtle.mjs +64 -0
  10. package/scripts/lib/bibliography-turtle.mjs +54 -0
  11. package/scripts/lib/build-activity-turtle.mjs +92 -0
  12. package/scripts/lib/build-cache.mjs +70 -0
  13. package/scripts/lib/dataset-turtle.mjs +79 -0
  14. package/scripts/lib/turtle-escape.mjs +0 -0
  15. package/scripts/lib/version-turtle.mjs +56 -0
  16. package/scripts/lib/vocab-turtle.mjs +64 -0
  17. package/scripts/normalize-yaml.mjs +99 -0
  18. package/scripts/sync-concept-model.mjs +86 -0
  19. package/scripts/validate-shacl.mjs +194 -0
  20. package/src/__fixtures__/concept-shape.ttl +20 -0
  21. package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
  22. package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
  23. package/src/__fixtures__/shacl/good/concept.ttl +8 -0
  24. package/src/__tests__/__fixtures__/concepts.ts +221 -0
  25. package/src/__tests__/adapters/concept-identity.test.ts +76 -0
  26. package/src/__tests__/components/error-boundary.test.ts +109 -0
  27. package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
  28. package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
  29. package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
  30. package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
  31. package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
  32. package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
  33. package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
  34. package/src/__tests__/concept-rdf/differential.test.ts +96 -0
  35. package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
  36. package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
  37. package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
  38. package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
  39. package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
  40. package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
  41. package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
  42. package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
  43. package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
  44. package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
  45. package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
  46. package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
  47. package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
  48. package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
  49. package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
  50. package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
  51. package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
  52. package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
  53. package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
  54. package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
  55. package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
  56. package/src/__tests__/concept-rdf-view.test.ts +136 -0
  57. package/src/__tests__/dataset-style.test.ts +12 -7
  58. package/src/__tests__/errors/errors.test.ts +142 -0
  59. package/src/__tests__/format-downloads.test.ts +47 -65
  60. package/src/__tests__/markdown-lite.test.ts +19 -0
  61. package/src/__tests__/perf/bundle-layout.test.ts +50 -0
  62. package/src/__tests__/perf/serialization-perf.test.ts +121 -0
  63. package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
  64. package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
  65. package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
  66. package/src/__tests__/scripts/build-cache.test.ts +78 -0
  67. package/src/__tests__/scripts/build-integration.test.ts +134 -0
  68. package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
  69. package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
  70. package/src/__tests__/scripts/stryker-config.test.ts +33 -0
  71. package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
  72. package/src/__tests__/scripts/version-turtle.test.ts +72 -0
  73. package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
  74. package/src/__tests__/use-format-registry.test.ts +125 -0
  75. package/src/__tests__/utils/bcp47.test.ts +166 -0
  76. package/src/__tests__/utils/color-theme.test.ts +143 -0
  77. package/src/__tests__/utils/url-safety.test.ts +65 -0
  78. package/src/__tests__/validate-shacl.test.ts +100 -0
  79. package/src/adapters/DatasetAdapter.ts +11 -5
  80. package/src/adapters/GraphDataSource.ts +2 -1
  81. package/src/adapters/UriRouter.ts +2 -1
  82. package/src/adapters/concept-identity.ts +69 -0
  83. package/src/adapters/factory.ts +3 -2
  84. package/src/adapters/model-bridge.ts +2 -1
  85. package/src/adapters/non-verbal/figure-bridge.ts +22 -23
  86. package/src/adapters/non-verbal/formula-bridge.ts +11 -9
  87. package/src/adapters/non-verbal/glossarist-augment.d.ts +133 -0
  88. package/src/adapters/non-verbal/index.ts +12 -9
  89. package/src/adapters/non-verbal/kind.ts +2 -1
  90. package/src/adapters/non-verbal/table-bridge.ts +12 -10
  91. package/src/adapters/non-verbal/types.ts +36 -54
  92. package/src/adapters/non-verbal-resolver.ts +6 -3
  93. package/src/components/AppSidebar.vue +189 -93
  94. package/src/components/ConceptDetail.vue +8 -0
  95. package/src/components/ConceptEditionRail.vue +222 -0
  96. package/src/components/ConceptRdfView.vue +37 -377
  97. package/src/components/DatasetSeriesCard.vue +270 -0
  98. package/src/components/ErrorBoundary.vue +95 -0
  99. package/src/components/FormatDownloads.vue +17 -13
  100. package/src/components/HomeSeriesSection.vue +277 -0
  101. package/src/components/NonVerbalRepDisplay.vue +2 -2
  102. package/src/components/RelationSphere.vue +1672 -0
  103. package/src/components/SidebarSeriesSection.vue +239 -0
  104. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  105. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  106. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  107. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  108. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  109. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  110. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  111. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  112. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  113. package/src/components/concept-rdf/group-emitter.ts +69 -0
  114. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  115. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  116. package/src/components/concept-rdf/predicates.ts +261 -0
  117. package/src/components/concept-rdf/provenance.ts +80 -0
  118. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  119. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  120. package/src/components/concept-rdf/sections-builder.ts +62 -0
  121. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  122. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  123. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  124. package/src/components/concept-rdf/version-emitter.ts +65 -0
  125. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  126. package/src/components/figure/FigureDisplay.vue +16 -15
  127. package/src/components/figure/FigureImages.vue +38 -16
  128. package/src/components/figure/figure-image-pick.ts +1 -1
  129. package/src/components/figure/figure-layout.ts +1 -1
  130. package/src/components/formula/FormulaDisplay.vue +11 -9
  131. package/src/components/formula/FormulaExpression.vue +4 -4
  132. package/src/components/non-verbal/NonVerbalCaption.vue +5 -5
  133. package/src/components/non-verbal/NonVerbalSources.vue +3 -11
  134. package/src/components/table/TableDisplay.vue +6 -4
  135. package/src/components/table/TableMarkup.vue +1 -1
  136. package/src/composables/use-color-theme.ts +82 -0
  137. package/src/composables/use-format-registry.ts +42 -0
  138. package/src/composables/use-non-verbal-entity.ts +2 -1
  139. package/src/composables/useDatasetSeries.ts +258 -0
  140. package/src/composables/useSphereProjection.ts +125 -0
  141. package/src/config/group-types.ts +92 -0
  142. package/src/config/types.ts +81 -2
  143. package/src/config/use-site-config.ts +2 -1
  144. package/src/errors.ts +136 -0
  145. package/src/i18n/locales/eng.yml +24 -0
  146. package/src/i18n/locales/fra.yml +24 -0
  147. package/src/stores/vocabulary.ts +3 -1
  148. package/src/style.css +17 -2
  149. package/src/types/agents-version-turtle.d.ts +27 -0
  150. package/src/types/bibliography-turtle.d.ts +12 -0
  151. package/src/types/build-activity-turtle.d.ts +16 -0
  152. package/src/types/build-cache.d.ts +20 -0
  153. package/src/types/dataset-turtle.d.ts +32 -0
  154. package/src/types/normalize-yaml.d.ts +16 -0
  155. package/src/types/turtle-escape.d.ts +6 -0
  156. package/src/types/vocab-turtle.d.ts +13 -0
  157. package/src/utils/asciidoc-lite.ts +11 -6
  158. package/src/utils/bcp47.ts +141 -0
  159. package/src/utils/color-theme-integration.ts +11 -0
  160. package/src/utils/color-theme.ts +129 -0
  161. package/src/utils/dataset-style.ts +31 -6
  162. package/src/utils/locale.ts +6 -14
  163. package/src/utils/markdown-lite.ts +6 -1
  164. package/src/utils/relation-sphere-styling.ts +63 -0
  165. package/src/utils/relationship-categories.ts +30 -0
  166. package/src/utils/url-safety.ts +30 -0
  167. package/src/views/ConceptView.vue +183 -9
  168. package/src/views/DatasetView.vue +6 -0
  169. package/src/views/HomeView.vue +5 -0
  170. package/vite.config.ts +7 -0
@@ -7,7 +7,8 @@
7
7
  * `<figure>` receives the anchor ID for `{{formula:id}}` mentions.
8
8
  */
9
9
  import { computed } from 'vue';
10
- import type { Formula } from '../../adapters/non-verbal/types';
10
+ import type { Formula } from 'glossarist';
11
+ import type { FormulaNotation } from '../../adapters/non-verbal/types';
11
12
  import { useNonVerbalEntity } from '../../composables/use-non-verbal-entity';
12
13
  import { resolveFallbackChain } from '../../utils/locale';
13
14
  import { anchorId } from '../../utils/non-verbal-anchor';
@@ -27,37 +28,38 @@ const k = () => 'formula' as const;
27
28
  const { entity, state, error } = useNonVerbalEntity(k, () => props.datasetId, () => props.entityId);
28
29
 
29
30
  const fallbackChain = computed(() => resolveFallbackChain(props.datasetLocales));
31
+ const form = computed(() => entity.value as Formula | null);
30
32
  const anchor = computed(() => anchorId('formula', props.datasetId, props.entityId));
31
33
  const descriptionId = computed(() => `${anchor.value}-desc`);
32
34
  </script>
33
35
 
34
36
  <template>
35
37
  <figure
36
- v-if="entity && state === 'loaded'"
38
+ v-if="form && state === 'loaded'"
37
39
  :id="anchor"
38
40
  class="formula-entity"
39
41
  >
40
42
  <div class="formula__expr-line">
41
43
  <FormulaExpression
42
- :expression="(entity as Formula).expression"
43
- :notation="(entity as Formula).notation"
44
+ :expression="form.expression"
45
+ :notation="(form.notation as FormulaNotation | null)"
44
46
  :locale="locale"
45
47
  :fallback-chain="fallbackChain"
46
48
  />
47
49
  </div>
48
50
 
49
51
  <NonVerbalCaption
50
- :identifier="(entity as Formula).identifier"
51
- :caption="(entity as Formula).caption"
52
- :description="(entity as Formula).description"
52
+ :identifier="form.identifier"
53
+ :caption="form.caption"
54
+ :description="form.description"
53
55
  :locale="locale"
54
56
  :fallback-chain="fallbackChain"
55
57
  :description-id="descriptionId"
56
58
  />
57
59
 
58
60
  <NonVerbalSources
59
- v-if="(entity as Formula).sources?.length"
60
- :sources="(entity as Formula).sources!"
61
+ v-if="form.sources?.length"
62
+ :sources="form.sources"
61
63
  />
62
64
  </figure>
63
65
 
@@ -16,13 +16,13 @@ import { pickLocaleMap, localeToBcp47 } from '../../utils/locale';
16
16
  import { loadPlurimath } from '../../utils/plurimath';
17
17
 
18
18
  const props = defineProps<{
19
- expression: LocalizedString;
20
- notation: FormulaNotation;
19
+ expression: LocalizedString | null;
20
+ notation: FormulaNotation | null;
21
21
  locale: string;
22
22
  fallbackChain?: readonly string[];
23
23
  }>();
24
24
 
25
- const resolved = computed(() => pickLocaleMap(props.expression, props.locale, props.fallbackChain));
25
+ const resolved = computed(() => pickLocaleMap(props.expression ?? undefined, props.locale, props.fallbackChain));
26
26
  const html = ref<string>('');
27
27
  const lang = computed(() => resolved.value ? localeToBcp47(resolved.value.locale) : undefined);
28
28
 
@@ -34,7 +34,7 @@ const PLURIMATH_FORMAT: Record<FormulaNotation, string> = {
34
34
 
35
35
  async function render() {
36
36
  const r = resolved.value;
37
- if (!r) { html.value = ''; return; }
37
+ if (!r || !props.notation) { html.value = ''; return; }
38
38
  try {
39
39
  const Plurimath = await loadPlurimath();
40
40
  const p = new Plurimath(r.text, PLURIMATH_FORMAT[props.notation]);
@@ -22,20 +22,20 @@ import type { LocalizedString } from '../../adapters/non-verbal/types';
22
22
  import { pickLocaleMap, localeToBcp47 } from '../../utils/locale';
23
23
 
24
24
  const props = defineProps<{
25
- identifier?: string;
26
- caption?: LocalizedString;
27
- description?: LocalizedString;
25
+ identifier?: string | null;
26
+ caption?: LocalizedString | null;
27
+ description?: LocalizedString | null;
28
28
  locale: string;
29
29
  fallbackChain?: readonly string[];
30
30
  descriptionId?: string;
31
31
  }>();
32
32
 
33
33
  const captionResolved = computed(() =>
34
- pickLocaleMap(props.caption, props.locale, props.fallbackChain),
34
+ pickLocaleMap(props.caption ?? undefined, props.locale, props.fallbackChain),
35
35
  );
36
36
 
37
37
  const descriptionResolved = computed(() =>
38
- pickLocaleMap(props.description, props.locale, props.fallbackChain),
38
+ pickLocaleMap(props.description ?? undefined, props.locale, props.fallbackChain),
39
39
  );
40
40
 
41
41
  const captionLang = computed(() =>
@@ -6,20 +6,12 @@
6
6
  * so the rendering matches the rest of the app. Each source may carry a
7
7
  * modification note (e.g. "Adapted.") which is rendered alongside.
8
8
  */
9
- import type { Citation } from 'glossarist';
10
- import type { NonVerbalSource } from '../../adapters/non-verbal/types';
9
+ import type { ConceptSource } from 'glossarist';
11
10
  import CitationDisplay from '../CitationDisplay.vue';
12
11
 
13
12
  defineProps<{
14
- sources: NonVerbalSource[];
13
+ sources: ConceptSource[];
15
14
  }>();
16
-
17
- // CitationDisplay expects glossarist's Citation class. NonVerbalSource.origin
18
- // is wire-compatible at runtime but typed differently on the consumer side;
19
- // cast once at this boundary.
20
- function asCitation(origin: NonVerbalSource['origin']): Citation | null {
21
- return (origin as unknown as Citation) ?? null;
22
- }
23
15
  </script>
24
16
 
25
17
  <template>
@@ -27,7 +19,7 @@ function asCitation(origin: NonVerbalSource['origin']): Citation | null {
27
19
  <div class="nv-sources__label">Sources</div>
28
20
  <ol class="nv-sources__list">
29
21
  <li v-for="(src, i) in sources" :key="i" class="nv-source">
30
- <CitationDisplay v-if="asCitation(src.origin)" :citation="asCitation(src.origin)!" />
22
+ <CitationDisplay v-if="src.origin" :citation="src.origin" />
31
23
  <span v-if="src.modification" class="nv-source__modification">
32
24
  — {{ src.modification }}
33
25
  </span>
@@ -6,7 +6,8 @@
6
6
  * TableMarkup (HTML / Markdown / AsciiDoc) based on `content.kind`.
7
7
  */
8
8
  import { computed } from 'vue';
9
- import type { Table } from '../../adapters/non-verbal/types';
9
+ import type { Table } from 'glossarist';
10
+ import type { TableContent, TableFormat } from '../../adapters/non-verbal/types';
10
11
  import { useNonVerbalEntity } from '../../composables/use-non-verbal-entity';
11
12
  import { resolveFallbackChain } from '../../utils/locale';
12
13
  import { anchorId } from '../../utils/non-verbal-anchor';
@@ -31,12 +32,13 @@ const anchor = computed(() => anchorId('table', props.datasetId, props.entityId)
31
32
  const descriptionId = computed(() => `${anchor.value}-desc`);
32
33
 
33
34
  const table = computed<Table | null>(() => entity.value as Table | null);
35
+ const content = computed<TableContent | null>(() => (table.value?.content ?? null) as TableContent | null);
34
36
  const structuredContent = computed(() => {
35
- const c = table.value?.content;
37
+ const c = content.value;
36
38
  return c && c.kind === 'structured' ? c : null;
37
39
  });
38
40
  const markup = computed(() => {
39
- const c = table.value?.content;
41
+ const c = content.value;
40
42
  return c && c.kind === 'markup' ? c.markup : null;
41
43
  });
42
44
  </script>
@@ -56,7 +58,7 @@ const markup = computed(() => {
56
58
  <TableMarkup
57
59
  v-else-if="markup"
58
60
  :content="markup"
59
- :format="table.format"
61
+ :format="(table.format as TableFormat | null)"
60
62
  :locale="locale"
61
63
  :fallback-chain="fallbackChain"
62
64
  />
@@ -18,7 +18,7 @@ import { renderAsciiDocLite } from '../../utils/asciidoc-lite';
18
18
 
19
19
  const props = defineProps<{
20
20
  content: LocalizedString;
21
- format?: TableFormat;
21
+ format?: TableFormat | null;
22
22
  locale: string;
23
23
  fallbackChain?: readonly string[];
24
24
  }>();
@@ -0,0 +1,82 @@
1
+ /**
2
+ * useColorTheme — emits CSS custom properties for every colorable
3
+ * semantic category, scoped per dataset/group via [data-ds] / [data-group]
4
+ * attribute selectors.
5
+ *
6
+ * Components consume via `var(--rel-lifecycle-light)` etc. Theme
7
+ * switching is a single class swap on `<html>` — no JS recompute.
8
+ */
9
+ import { watchEffect, onScopeDispose } from 'vue';
10
+ import { useSiteConfig } from '../config/use-site-config';
11
+ import { useVocabularyStore } from '../stores/vocabulary';
12
+ import { createColorTheme, type ColorPair } from '../utils/color-theme';
13
+ import type { DatasetColorSpec } from '../config/types';
14
+
15
+ function kebab(s: string): string {
16
+ return s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase().replace(/[^a-z0-9-]/g, '-');
17
+ }
18
+
19
+ function setVar(root: HTMLElement, name: string, value: string): void {
20
+ root.style.setProperty(name, value);
21
+ }
22
+
23
+ function emitPair(root: HTMLElement, prefix: string, key: string, pair: ColorPair): void {
24
+ const safeKey = kebab(key);
25
+ setVar(root, `--${prefix}-${safeKey}-light`, pair.light);
26
+ setVar(root, `--${prefix}-${safeKey}-dark`, pair.dark);
27
+ }
28
+
29
+ export function useColorTheme(): void {
30
+ const { config } = useSiteConfig();
31
+ const store = useVocabularyStore();
32
+
33
+ const disposers: Array<() => void> = [];
34
+
35
+ watchEffect(() => {
36
+ const siteColors = config.value?.colors;
37
+ const theme = createColorTheme(siteColors);
38
+ const root = document.documentElement;
39
+
40
+ /* Global category + type colors — emit on :root */
41
+ for (const cat of Object.keys(theme.defaults.relationshipCategory)) {
42
+ emitPair(root, 'rel-cat', cat, theme.relationshipCategoryColor(cat));
43
+ }
44
+ for (const type of Object.keys(theme.defaults.relationshipType)) {
45
+ emitPair(root, 'rel-type', type, theme.relationshipTypeColor(type));
46
+ }
47
+ for (const status of Object.keys(theme.defaults.conceptStatus)) {
48
+ emitPair(root, 'concept-status', status, theme.conceptStatusColor(status));
49
+ }
50
+ for (const kind of Object.keys(theme.defaults.groupKind)) {
51
+ emitPair(root, 'group-kind', kind, theme.groupKindColor(kind));
52
+ }
53
+ });
54
+
55
+ /* Per-dataset colors — emit on [data-ds="<id>"] scoped wrappers.
56
+ Uses a stable wrapper element so the variables don't leak globally. */
57
+ watchEffect(() => {
58
+ const siteColors = config.value?.colors;
59
+ const theme = createColorTheme(siteColors);
60
+
61
+ for (const ds of store.datasetList) {
62
+ const declared = ds.manifest?.color as DatasetColorSpec | undefined;
63
+ const pair = theme.datasetColor(ds.id, declared);
64
+ const scopeId = `ds-color-scope-${ds.id.replace(/[^a-zA-Z0-9]/g, '_')}`;
65
+ let scope = document.getElementById(scopeId);
66
+ if (!scope) {
67
+ scope = document.createElement('div');
68
+ scope.id = scopeId;
69
+ scope.setAttribute('data-ds', ds.id);
70
+ scope.style.display = 'none';
71
+ document.head.appendChild(scope);
72
+ disposers.push(() => scope?.remove());
73
+ }
74
+ setVar(scope, '--ds-light', pair.light);
75
+ setVar(scope, '--ds-dark', pair.dark);
76
+ }
77
+ });
78
+
79
+ onScopeDispose(() => {
80
+ for (const dispose of disposers) dispose();
81
+ });
82
+ }
@@ -0,0 +1,42 @@
1
+ export type FormatAvailability = 'per-concept' | 'aggregate' | 'both';
2
+ export type SerializeMode = 'build' | 'runtime';
3
+
4
+ export interface FormatDescriptor {
5
+ id: string;
6
+ extension: string;
7
+ mediaType: string;
8
+ label: string;
9
+ available: FormatAvailability;
10
+ serialize: SerializeMode;
11
+ }
12
+
13
+ const REGISTRY = new Map<string, FormatDescriptor>();
14
+
15
+ export function registerFormat(desc: FormatDescriptor): void {
16
+ REGISTRY.set(desc.id, desc);
17
+ }
18
+
19
+ export function unregisterFormat(id: string): void {
20
+ REGISTRY.delete(id);
21
+ }
22
+
23
+ export function getFormat(id: string): FormatDescriptor | undefined {
24
+ return REGISTRY.get(id);
25
+ }
26
+
27
+ export function listFormats(opts: { availability?: FormatAvailability } = {}): FormatDescriptor[] {
28
+ const filter = opts.availability;
29
+ return [...REGISTRY.values()]
30
+ .filter(f => !filter || f.available === filter || f.available === 'both')
31
+ .sort((a, b) => a.label.localeCompare(b.label));
32
+ }
33
+
34
+ export function clearFormats(): void {
35
+ REGISTRY.clear();
36
+ }
37
+
38
+ registerFormat({ id: 'ttl', extension: 'ttl', mediaType: 'text/turtle', label: 'Turtle', available: 'both', serialize: 'build' });
39
+ registerFormat({ id: 'jsonld', extension: 'jsonld', mediaType: 'application/ld+json', label: 'JSON-LD', available: 'both', serialize: 'build' });
40
+ registerFormat({ id: 'yaml', extension: 'yaml', mediaType: 'application/yaml', label: 'YAML', available: 'per-concept', serialize: 'build' });
41
+ registerFormat({ id: 'tbx', extension: 'tbx.xml', mediaType: 'application/x-tbx', label: 'TBX', available: 'aggregate', serialize: 'build' });
42
+ registerFormat({ id: 'jsonl', extension: 'jsonl', mediaType: 'application/jsonl+json', label: 'JSON-Lines', available: 'aggregate', serialize: 'build' });
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { ref, watch, shallowRef } from 'vue';
10
10
  import { getFactory } from '../adapters/factory';
11
- import type { NonVerbalEntity, NonVerbalKind } from '../adapters/non-verbal/types';
11
+ import type { NonVerbalKind } from '../adapters/non-verbal/types';
12
+ import type { NonVerbalEntity } from 'glossarist';
12
13
 
13
14
  export type LoadState = 'loading' | 'loaded' | 'not-found' | 'error';
14
15
 
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Dataset series — groups related editions of the same vocabulary.
3
+ *
4
+ * A "series" is a family of datasets that share a base name but differ in
5
+ * edition year/status. e.g. `viml-2022`, `viml-2013`, `viml-2000`, `viml-1968`
6
+ * all belong to the `viml` series.
7
+ *
8
+ * Resolution order (most authoritative first):
9
+ * 1. Explicit `datasetGroups` from site-config with `series: true` —
10
+ * config-driven series have stable ids, labels, and orderings.
11
+ * 2. Auto-derivation by naming convention — `name-YYYY` → `name`. Used
12
+ * only when NO configured groups exist (backward compat).
13
+ *
14
+ * Within a series, editions are sorted by year ascending so the timeline
15
+ * reads naturally left-to-right / top-to-bottom.
16
+ */
17
+
18
+ import { computed } from 'vue';
19
+ import { useVocabularyStore } from '../stores/vocabulary';
20
+ import { useSiteConfig } from '../config/use-site-config';
21
+ import { resolveGroupKind } from '../config/group-types';
22
+ import type { Manifest } from '../adapters/types';
23
+
24
+ export interface DatasetSeriesMember {
25
+ id: string;
26
+ ref: string;
27
+ year?: number;
28
+ status: string;
29
+ isCurrent: boolean;
30
+ isActive: boolean;
31
+ conceptCount?: number;
32
+ registerId: string;
33
+ }
34
+
35
+ export interface DatasetSeries {
36
+ /** Stable series key, e.g. `viml`. */
37
+ key: string;
38
+ /** Display title for the series, e.g. `VIML` or `International Vocabulary of Legal Metrology`. */
39
+ title: string;
40
+ /** Optional description from config. */
41
+ description?: string;
42
+ /** Optional accent color from config. Accepts single hex or { light, dark }. */
43
+ color?: string | { light: string; dark: string };
44
+ /** All known editions, oldest first. */
45
+ members: DatasetSeriesMember[];
46
+ /** The current (newest valid) member, if any. */
47
+ current?: DatasetSeriesMember;
48
+ /** Total concept count across the series (sum of members). */
49
+ totalConcepts: number;
50
+ /** Whether this series was explicitly configured (vs auto-derived). */
51
+ configured: boolean;
52
+ }
53
+
54
+ const YEAR_SUFFIX = /[-_:](\d{4})([a-z]?)$/i;
55
+
56
+ /** Strip trailing year from a dataset id to get the series key. */
57
+ export function deriveSeriesKey(id: string): string {
58
+ return id.replace(YEAR_SUFFIX, '').replace(/[-_:]+$/, '');
59
+ }
60
+
61
+ /** Extract a year from a string. Prefers the ISO convention
62
+ * (`:YYYY`, `:YYYYa`) used in ISO/IEC standard references, then
63
+ * falls back to a year-as-suffix match (`-YYYY`, `_YYYY`, ` YYYY`),
64
+ * then a bare 4-digit year. Returns undefined for out-of-range or
65
+ * 4-digit runs that are clearly standard numbers (e.g. `ISO 10241`
66
+ * → 1024 is rejected because it's not preceded by `:`). */
67
+ export function extractYear(source: string): number | undefined {
68
+ if (!source) return undefined;
69
+ const isoMatch = source.match(/:(\d{4})([a-z]?)$/i);
70
+ if (isoMatch) {
71
+ const year = parseInt(isoMatch[1], 10);
72
+ if (year >= 1900 && year <= 2100) return year;
73
+ }
74
+ const suffixMatch = source.match(/[-_\s](\d{4})([a-z]?)$/i);
75
+ if (suffixMatch) {
76
+ const year = parseInt(suffixMatch[1], 10);
77
+ if (year >= 1900 && year <= 2100) return year;
78
+ }
79
+ if (/^\d{4}$/.test(source.trim())) {
80
+ const year = parseInt(source.trim(), 10);
81
+ if (year >= 1900 && year <= 2100) return year;
82
+ }
83
+ return undefined;
84
+ }
85
+
86
+ /** Build the series title from a member — `OIML V 1:2022` → `OIML V 1`.
87
+ * Returns the dataset id when the manifest is unavailable. */
88
+ function deriveSeriesTitle(m: Manifest | undefined): string {
89
+ if (!m) return '';
90
+ const ref = m.ref ?? m.title;
91
+ return ref.replace(/[:\s-]\d{4}([a-z]?)$/i, '').trim() || m.title;
92
+ }
93
+
94
+ function manifestToMember(m: Manifest, activeDatasetId?: string): DatasetSeriesMember {
95
+ const year = extractYear(m.id) ?? extractYear(m.ref ?? '') ?? extractYear(m.title);
96
+ return {
97
+ id: m.id,
98
+ ref: m.ref ?? m.title,
99
+ year,
100
+ status: m.status ?? m.editionStatus ?? 'unknown',
101
+ isCurrent: false,
102
+ isActive: m.id === activeDatasetId,
103
+ conceptCount: m.conceptCount,
104
+ registerId: m.id,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Group manifests into series. Pure function.
110
+ *
111
+ * Strategy:
112
+ * 1. If `configuredGroups` is provided AND has any group whose `kind` is
113
+ * `lineage` (or the legacy `series: true` flag), use ONLY configured
114
+ * lineage groups — config is the source of truth.
115
+ * 2. Otherwise, fall back to auto-derivation by naming convention.
116
+ */
117
+ export function groupManifestsIntoSeries(
118
+ manifests: Manifest[],
119
+ activeDatasetId?: string,
120
+ configuredGroups?: Array<{
121
+ id: string;
122
+ label?: string;
123
+ description?: string;
124
+ color?: string | { light: string; dark: string };
125
+ datasets: string[];
126
+ series?: boolean;
127
+ kind?: 'lineage' | 'topic' | 'family' | 'collection' | 'default';
128
+ current?: string;
129
+ }>,
130
+ ): DatasetSeries[] {
131
+ const manifestMap = new Map(manifests.map(m => [m.id, m] as const));
132
+
133
+ /* Strategy 1: config-driven lineage series */
134
+ const configSeries = (configuredGroups ?? []).filter(g => resolveGroupKind(g) === 'lineage');
135
+ if (configSeries.length > 0) {
136
+ const series: DatasetSeries[] = [];
137
+ for (const g of configSeries) {
138
+ const members: DatasetSeriesMember[] = [];
139
+ /* Include EVERY dataset in the config, even if its manifest isn't loaded
140
+ yet. Without this, visiting /dataset/viml-2000 (only manifest loaded)
141
+ would shrink the series to one member and mis-mark viml-2000 as current. */
142
+ for (const id of g.datasets) {
143
+ const m = manifestMap.get(id);
144
+ if (m) {
145
+ members.push(manifestToMember(m, activeDatasetId));
146
+ } else {
147
+ /* Stub member from id alone — year derived from `<name>-YYYY` pattern. */
148
+ members.push({
149
+ id,
150
+ ref: id,
151
+ year: extractYear(id),
152
+ status: 'unknown',
153
+ isCurrent: false,
154
+ isActive: id === activeDatasetId,
155
+ conceptCount: undefined,
156
+ registerId: id,
157
+ });
158
+ }
159
+ }
160
+ if (members.length === 0) continue;
161
+ members.sort((a, b) => (a.year ?? 0) - (b.year ?? 0));
162
+
163
+ /* Determine current edition:
164
+ 1. Explicit `current` field from config (most authoritative)
165
+ 2. Newest member with status='valid'
166
+ 3. Last member (newest by year) */
167
+ let current: DatasetSeriesMember | undefined;
168
+ if (g.current) {
169
+ current = members.find(m => m.id === g.current);
170
+ }
171
+ if (!current) {
172
+ const validMembers = members.filter(m => m.status === 'valid');
173
+ current = validMembers[validMembers.length - 1] ?? members[members.length - 1];
174
+ }
175
+ if (current) current.isCurrent = true;
176
+
177
+ const totalConcepts = members.reduce((sum, m) => sum + (m.conceptCount ?? 0), 0);
178
+ series.push({
179
+ key: g.id,
180
+ title: g.label ?? (deriveSeriesTitle(manifestMap.get(members[0].id)) || g.id),
181
+ description: g.description,
182
+ color: g.color,
183
+ members,
184
+ current,
185
+ totalConcepts,
186
+ configured: true,
187
+ });
188
+ }
189
+ return series;
190
+ }
191
+
192
+ /* Strategy 2: auto-derive by naming convention */
193
+ const groups = new Map<string, Manifest[]>();
194
+ for (const m of manifests) {
195
+ const key = deriveSeriesKey(m.id);
196
+ if (!groups.has(key)) groups.set(key, []);
197
+ groups.get(key)!.push(m);
198
+ }
199
+
200
+ const series: DatasetSeries[] = [];
201
+ for (const [key, members] of groups) {
202
+ if (members.length === 0) continue;
203
+ const enriched = members.map(m => manifestToMember(m, activeDatasetId));
204
+ enriched.sort((a, b) => (a.year ?? 0) - (b.year ?? 0));
205
+ const validMembers = enriched.filter(m => m.status === 'valid');
206
+ const current = validMembers[validMembers.length - 1] ?? enriched[enriched.length - 1];
207
+ if (current) current.isCurrent = true;
208
+ const totalConcepts = enriched.reduce((sum, m) => sum + (m.conceptCount ?? 0), 0);
209
+ series.push({
210
+ key,
211
+ title: deriveSeriesTitle(members[0]),
212
+ members: enriched,
213
+ current,
214
+ totalConcepts,
215
+ configured: false,
216
+ });
217
+ }
218
+
219
+ series.sort((a, b) => a.key.localeCompare(b.key));
220
+ return series;
221
+ }
222
+
223
+ /**
224
+ * Composable — exposes series for the currently-loaded datasets.
225
+ * Reactive: re-derives when the store's dataset list changes.
226
+ */
227
+ export function useDatasetSeries(activeDatasetId?: () => string | undefined) {
228
+ const store = useVocabularyStore();
229
+ const { datasetGroups } = useSiteConfig();
230
+
231
+ const series = computed<DatasetSeries[]>(() => {
232
+ const manifests: Manifest[] = [];
233
+ for (const [, adapter] of store.datasets.entries()) {
234
+ const m = adapter.manifest;
235
+ if (m) manifests.push(m);
236
+ }
237
+ const activeId = activeDatasetId?.();
238
+ const configured = datasetGroups.value?.map(g => ({
239
+ id: g.id,
240
+ label: g.label,
241
+ description: g.description,
242
+ color: g.color,
243
+ datasets: g.datasets,
244
+ series: g.series,
245
+ kind: g.kind,
246
+ current: g.current,
247
+ }));
248
+ return groupManifestsIntoSeries(manifests, activeId, configured);
249
+ });
250
+
251
+ const seriesForActive = computed<DatasetSeries | undefined>(() => {
252
+ const activeId = activeDatasetId?.();
253
+ if (!activeId) return undefined;
254
+ return series.value.find(s => s.members.some(m => m.id === activeId));
255
+ });
256
+
257
+ return { series, seriesForActive };
258
+ }