@glossarist/concept-browser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +319 -0
- package/cli/index.mjs +119 -0
- package/env.d.ts +7 -0
- package/index.html +16 -0
- package/package.json +78 -0
- package/postcss.config.js +6 -0
- package/scripts/build-edges.js +112 -0
- package/scripts/fetch-datasets.mjs +195 -0
- package/scripts/generate-404.js +15 -0
- package/scripts/generate-data.mjs +606 -0
- package/scripts/load-site-config.mjs +56 -0
- package/src/App.vue +98 -0
- package/src/__tests__/data-integration.test.ts +135 -0
- package/src/__tests__/data-integrity.test.ts +101 -0
- package/src/__tests__/dataset-adapter.test.ts +336 -0
- package/src/__tests__/dataset-style.test.ts +37 -0
- package/src/__tests__/graph.test.ts +187 -0
- package/src/__tests__/lang.test.ts +29 -0
- package/src/__tests__/math.test.ts +113 -0
- package/src/__tests__/reference-resolver.test.ts +122 -0
- package/src/__tests__/site-config.test.ts +52 -0
- package/src/__tests__/uri-router.test.ts +76 -0
- package/src/adapters/DatasetAdapter.ts +270 -0
- package/src/adapters/ReferenceResolver.ts +95 -0
- package/src/adapters/UriRouter.ts +41 -0
- package/src/adapters/factory.ts +78 -0
- package/src/adapters/types.ts +162 -0
- package/src/components/AppHeader.vue +99 -0
- package/src/components/AppSidebar.vue +133 -0
- package/src/components/ConceptCard.vue +65 -0
- package/src/components/ConceptDetail.vue +540 -0
- package/src/components/ConceptTimeline.vue +410 -0
- package/src/components/FormatDownloads.vue +46 -0
- package/src/components/GraphPanel.vue +499 -0
- package/src/components/LanguageDetail.vue +211 -0
- package/src/components/NavIcon.vue +20 -0
- package/src/components/SearchBar.vue +241 -0
- package/src/composables/use-dataset-loader.ts +27 -0
- package/src/config/types.ts +130 -0
- package/src/config/use-site-config.ts +144 -0
- package/src/graph/GraphEngine.ts +137 -0
- package/src/graph/index.ts +1 -0
- package/src/main.ts +11 -0
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/router/index.ts +43 -0
- package/src/router/page-routes.ts +35 -0
- package/src/stores/ui.ts +59 -0
- package/src/stores/vocabulary.ts +309 -0
- package/src/style.css +314 -0
- package/src/utils/asciidoc-lite.ts +123 -0
- package/src/utils/concept-formats.ts +157 -0
- package/src/utils/dataset-style.ts +54 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/lang.ts +32 -0
- package/src/utils/math.ts +100 -0
- package/src/views/AboutView.vue +122 -0
- package/src/views/ConceptView.vue +119 -0
- package/src/views/ContributorsView.vue +110 -0
- package/src/views/DatasetView.vue +249 -0
- package/src/views/GraphView.vue +65 -0
- package/src/views/HomeView.vue +168 -0
- package/src/views/NewsView.vue +146 -0
- package/src/views/ResolveView.vue +63 -0
- package/src/views/SearchView.vue +33 -0
- package/src/views/StatsView.vue +121 -0
- package/tailwind.config.js +43 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight AsciiDoc-to-HTML converter for news posts.
|
|
3
|
+
* Handles: paragraphs, headings, bold, italic, monospace, links, lists, source blocks.
|
|
4
|
+
*/
|
|
5
|
+
export function renderAsciiDocLite(text: string): string {
|
|
6
|
+
if (!text) return '';
|
|
7
|
+
|
|
8
|
+
const output: string[] = [];
|
|
9
|
+
const lines = text.split('\n');
|
|
10
|
+
let i = 0;
|
|
11
|
+
let inSourceBlock = false;
|
|
12
|
+
let sourceLines: string[] = [];
|
|
13
|
+
|
|
14
|
+
while (i < lines.length) {
|
|
15
|
+
const line = lines[i];
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
|
|
18
|
+
// Source block delimiter
|
|
19
|
+
if (trimmed.match(/^-{4,}\s*$/) || trimmed.match(/^\.{4,}\s*$/)) {
|
|
20
|
+
if (inSourceBlock) {
|
|
21
|
+
output.push(`<pre><code>${sourceLines.map(escapeHtml).join('\n')}</code></pre>`);
|
|
22
|
+
sourceLines = [];
|
|
23
|
+
inSourceBlock = false;
|
|
24
|
+
} else {
|
|
25
|
+
flushParagraph(output);
|
|
26
|
+
inSourceBlock = true;
|
|
27
|
+
}
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (inSourceBlock) {
|
|
33
|
+
sourceLines.push(line);
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Empty line — paragraph break
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
flushParagraph(output);
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Headings
|
|
46
|
+
const headingMatch = trimmed.match(/^(={1,5})\s+(.+)$/);
|
|
47
|
+
if (headingMatch) {
|
|
48
|
+
flushParagraph(output);
|
|
49
|
+
const level = headingMatch[1].length + 1;
|
|
50
|
+
output.push(`<h${level}>${inlineFormat(headingMatch[2])}</h${level}>`);
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Unordered list item
|
|
56
|
+
if (trimmed.match(/^\*\s+/)) {
|
|
57
|
+
flushParagraph(output);
|
|
58
|
+
const items: string[] = [];
|
|
59
|
+
while (i < lines.length && lines[i].trim().match(/^\*\s+/)) {
|
|
60
|
+
items.push(`<li>${inlineFormat(lines[i].trim().replace(/^\*\s+/, ''))}</li>`);
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
output.push(`<ul>${items.join('')}</ul>`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Ordered list item
|
|
68
|
+
if (trimmed.match(/^\.\s+/)) {
|
|
69
|
+
flushParagraph(output);
|
|
70
|
+
const items: string[] = [];
|
|
71
|
+
while (i < lines.length && lines[i].trim().match(/^\.\s+/)) {
|
|
72
|
+
items.push(`<li>${inlineFormat(lines[i].trim().replace(/^\.\s+/, ''))}</li>`);
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
output.push(`<ol>${items.join('')}</ol>`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Regular text — accumulate into paragraph buffer
|
|
80
|
+
paragraphBuf.push(inlineFormat(trimmed));
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
flushParagraph(output);
|
|
85
|
+
|
|
86
|
+
return output.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let paragraphBuf: string[] = [];
|
|
90
|
+
|
|
91
|
+
function flushParagraph(output: string[]) {
|
|
92
|
+
if (paragraphBuf.length > 0) {
|
|
93
|
+
output.push(`<p>${paragraphBuf.join(' ')}</p>`);
|
|
94
|
+
paragraphBuf = [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function inlineFormat(text: string): string {
|
|
99
|
+
// AsciiDoc link: https://example.com[text]
|
|
100
|
+
text = text.replace(/(https?:\/\/[^\s\[]+)\[([^\]]*)\]/g, (_, url, label) =>
|
|
101
|
+
`<a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(label || url)}</a>`
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Bare URLs
|
|
105
|
+
text = text.replace(/(?<!href="|">)(https?:\/\/[^\s<]+)/g, url =>
|
|
106
|
+
`<a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a>`
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Monospace: `text`
|
|
110
|
+
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
111
|
+
|
|
112
|
+
// Bold: *text*
|
|
113
|
+
text = text.replace(/\*([^*]+)\*/g, '<strong>$1</strong>');
|
|
114
|
+
|
|
115
|
+
// Italic: _text_
|
|
116
|
+
text = text.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
117
|
+
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function escapeHtml(s: string): string {
|
|
122
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
123
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format registry and converters for per-concept format downloads.
|
|
3
|
+
*
|
|
4
|
+
* Open/closed: add a new format by adding an entry to FORMAT_REGISTRY
|
|
5
|
+
* and a converter function. No changes to components needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface FormatDescriptor {
|
|
9
|
+
extension: string;
|
|
10
|
+
label: string;
|
|
11
|
+
mediaType: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FORMAT_REGISTRY: Record<string, FormatDescriptor> = {
|
|
15
|
+
ttl: { extension: 'ttl', label: 'Turtle RDF', mediaType: 'text/turtle' },
|
|
16
|
+
jsonld: { extension: 'jsonld', label: 'JSON-LD (SKOS)', mediaType: 'application/ld+json' },
|
|
17
|
+
yaml: { extension: 'yaml', label: 'YAML', mediaType: 'text/yaml' },
|
|
18
|
+
tbx: { extension: 'tbx', label: 'TBX', mediaType: 'application/xml' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ConceptDesignation = {
|
|
22
|
+
'@type'?: string;
|
|
23
|
+
'gl:normativeStatus'?: string;
|
|
24
|
+
'gl:term'?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ConceptDefinition = {
|
|
28
|
+
'gl:content'?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ConceptLocalized = {
|
|
32
|
+
'gl:languageCode'?: string;
|
|
33
|
+
'gl:designation'?: ConceptDesignation[];
|
|
34
|
+
'gl:definition'?: ConceptDefinition[];
|
|
35
|
+
'gl:notes'?: ConceptDefinition[];
|
|
36
|
+
'gl:source'?: any[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ConceptDocument = {
|
|
40
|
+
'@id'?: string;
|
|
41
|
+
'gl:identifier'?: string;
|
|
42
|
+
'gl:localizedConcept'?: Record<string, ConceptLocalized>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function getLocalizedData(concept: ConceptDocument) {
|
|
46
|
+
const result: Record<string, {
|
|
47
|
+
prefLabels: string[];
|
|
48
|
+
altLabels: string[];
|
|
49
|
+
definitions: string[];
|
|
50
|
+
notes: string[];
|
|
51
|
+
}> = {};
|
|
52
|
+
|
|
53
|
+
for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {})) {
|
|
54
|
+
const descs = lc['gl:designation'] || [];
|
|
55
|
+
const prefLabels = descs
|
|
56
|
+
.filter(d => d['gl:normativeStatus'] === 'preferred' && d['gl:term'])
|
|
57
|
+
.map(d => d['gl:term']!);
|
|
58
|
+
const altLabels = descs
|
|
59
|
+
.filter(d => d['gl:normativeStatus'] !== 'preferred' && d['gl:term'])
|
|
60
|
+
.map(d => d['gl:term']!);
|
|
61
|
+
const definitions = (lc['gl:definition'] || [])
|
|
62
|
+
.map(d => d['gl:content'] || '')
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
const notes = (lc['gl:notes'] || [])
|
|
65
|
+
.map(d => d['gl:content'] || '')
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
|
|
68
|
+
if (prefLabels.length || definitions.length) {
|
|
69
|
+
result[lang] = { prefLabels, altLabels, definitions, notes };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function escapeTurtle(s: string): string {
|
|
77
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert a concept document to SKOS Turtle.
|
|
82
|
+
*/
|
|
83
|
+
export function conceptToTurtle(concept: ConceptDocument): string {
|
|
84
|
+
const uri = concept['@id'] || '';
|
|
85
|
+
const id = concept['gl:identifier'] || '';
|
|
86
|
+
const data = getLocalizedData(concept);
|
|
87
|
+
|
|
88
|
+
const lines: string[] = [
|
|
89
|
+
'@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
|
|
90
|
+
'@prefix dcterms: <http://purl.org/dc/terms/> .',
|
|
91
|
+
'@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .',
|
|
92
|
+
'',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const props: string[] = ['a skos:Concept'];
|
|
96
|
+
props.push(` skos:notation "${escapeTurtle(id)}"`);
|
|
97
|
+
|
|
98
|
+
for (const [lang, d] of Object.entries(data)) {
|
|
99
|
+
for (const label of d.prefLabels) {
|
|
100
|
+
props.push(` skos:prefLabel "${escapeTurtle(label)}"@${lang}`);
|
|
101
|
+
}
|
|
102
|
+
for (const label of d.altLabels) {
|
|
103
|
+
props.push(` skos:altLabel "${escapeTurtle(label)}"@${lang}`);
|
|
104
|
+
}
|
|
105
|
+
for (const def of d.definitions) {
|
|
106
|
+
props.push(` skos:definition "${escapeTurtle(def)}"@${lang}`);
|
|
107
|
+
}
|
|
108
|
+
for (const note of d.notes) {
|
|
109
|
+
props.push(` skos:scopeNote "${escapeTurtle(note)}"@${lang}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines.push(`<${uri}>`);
|
|
114
|
+
lines.push(props.join(' ;\n'));
|
|
115
|
+
lines.push(' .');
|
|
116
|
+
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convert a concept document to SKOS JSON-LD.
|
|
122
|
+
*/
|
|
123
|
+
export function conceptToSkosJsonLd(concept: ConceptDocument): string {
|
|
124
|
+
const uri = concept['@id'] || '';
|
|
125
|
+
const id = concept['gl:identifier'] || '';
|
|
126
|
+
const data = getLocalizedData(concept);
|
|
127
|
+
|
|
128
|
+
const doc: Record<string, any> = {
|
|
129
|
+
'@context': {
|
|
130
|
+
skos: 'http://www.w3.org/2004/02/skos/core#',
|
|
131
|
+
dcterms: 'http://purl.org/dc/terms/',
|
|
132
|
+
'@language': { '@container': '@language' },
|
|
133
|
+
},
|
|
134
|
+
'@id': uri,
|
|
135
|
+
'@type': 'skos:Concept',
|
|
136
|
+
'skos:notation': id,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const prefLabels: Record<string, string> = {};
|
|
140
|
+
const altLabels: Record<string, string> = {};
|
|
141
|
+
const definitions: Record<string, string> = {};
|
|
142
|
+
const scopeNotes: Record<string, string> = {};
|
|
143
|
+
|
|
144
|
+
for (const [lang, d] of Object.entries(data)) {
|
|
145
|
+
if (d.prefLabels[0]) prefLabels[lang] = d.prefLabels[0];
|
|
146
|
+
if (d.altLabels[0]) altLabels[lang] = d.altLabels[0];
|
|
147
|
+
if (d.definitions[0]) definitions[lang] = d.definitions[0];
|
|
148
|
+
if (d.notes[0]) scopeNotes[lang] = d.notes[0];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (Object.keys(prefLabels).length) doc['skos:prefLabel'] = prefLabels;
|
|
152
|
+
if (Object.keys(altLabels).length) doc['skos:altLabel'] = altLabels;
|
|
153
|
+
if (Object.keys(definitions).length) doc['skos:definition'] = definitions;
|
|
154
|
+
if (Object.keys(scopeNotes).length) doc['skos:scopeNote'] = scopeNotes;
|
|
155
|
+
|
|
156
|
+
return JSON.stringify(doc, null, 2);
|
|
157
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
2
|
+
|
|
3
|
+
const PALETTE = [
|
|
4
|
+
'#3366ff', '#0d9488', '#d97706', '#8b5cf6',
|
|
5
|
+
'#ec4899', '#059669', '#dc2626', '#6366f1',
|
|
6
|
+
'#0891b2', '#65a30d', '#be185d', '#7c3aed',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export interface DsStyle {
|
|
10
|
+
color: string;
|
|
11
|
+
light: string;
|
|
12
|
+
dark: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hexToRgba(hex: string, alpha: number): string {
|
|
16
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
17
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
18
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
19
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function makeDsStyle(color: string): DsStyle {
|
|
23
|
+
return {
|
|
24
|
+
color,
|
|
25
|
+
light: hexToRgba(color, 0.1),
|
|
26
|
+
dark: hexToRgba(color, 0.85),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function paletteColor(index: number): string {
|
|
31
|
+
return PALETTE[index % PALETTE.length];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useDsStyle() {
|
|
35
|
+
const cache = new Map<string, DsStyle>();
|
|
36
|
+
|
|
37
|
+
function getStyle(registerId: string): DsStyle {
|
|
38
|
+
const cached = cache.get(registerId);
|
|
39
|
+
if (cached) return cached;
|
|
40
|
+
|
|
41
|
+
const store = useVocabularyStore();
|
|
42
|
+
const ds = store.datasetList.find(d => d.id === registerId);
|
|
43
|
+
const color = ds?.manifest.color || paletteColor(store.datasetList.findIndex(d => d.id === registerId));
|
|
44
|
+
const style = makeDsStyle(color);
|
|
45
|
+
cache.set(registerId, style);
|
|
46
|
+
return style;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getColor(registerId: string): string {
|
|
50
|
+
return getStyle(registerId).color;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { getStyle, getColor, paletteColor, makeDsStyle };
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { langName, langLabel, DEFAULT_LANG } from './lang';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const LANG_NAMES: Record<string, string> = {
|
|
2
|
+
eng: 'English',
|
|
3
|
+
ara: 'Arabic',
|
|
4
|
+
deu: 'German',
|
|
5
|
+
fra: 'French',
|
|
6
|
+
spa: 'Spanish',
|
|
7
|
+
ita: 'Italian',
|
|
8
|
+
jpn: 'Japanese',
|
|
9
|
+
kor: 'Korean',
|
|
10
|
+
pol: 'Polish',
|
|
11
|
+
por: 'Portuguese',
|
|
12
|
+
srp: 'Serbian',
|
|
13
|
+
swe: 'Swedish',
|
|
14
|
+
zho: 'Chinese',
|
|
15
|
+
rus: 'Russian',
|
|
16
|
+
fin: 'Finnish',
|
|
17
|
+
dan: 'Danish',
|
|
18
|
+
nld: 'Dutch',
|
|
19
|
+
msa: 'Malay',
|
|
20
|
+
nob: 'Norwegian Bokmål',
|
|
21
|
+
nno: 'Norwegian Nynorsk',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function langName(code: string): string {
|
|
25
|
+
return LANG_NAMES[code] ?? code;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function langLabel(code: string): string {
|
|
29
|
+
return code.toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_LANG = 'eng';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import katex from 'katex';
|
|
2
|
+
|
|
3
|
+
export type XrefResolver = (uri: string, term: string) => string;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert `* item` lines into <ul><li> blocks.
|
|
7
|
+
*/
|
|
8
|
+
function convertLists(text: string): string {
|
|
9
|
+
return text.replace(/(?:^|\n\n)((?:[ \t]*\* [^\n]+)(?:\n\n[ \t]*\* [^\n]+)*)/g, (_, block) => {
|
|
10
|
+
if (/^\*stem:\[/.test(block.trimStart())) return _;
|
|
11
|
+
const items: string[] = [];
|
|
12
|
+
const re = /[ \t]*\* ([^\n]+)/g;
|
|
13
|
+
let m;
|
|
14
|
+
while ((m = re.exec(block)) !== null) {
|
|
15
|
+
items.push(m[1].trim());
|
|
16
|
+
}
|
|
17
|
+
if (!items.length) return _;
|
|
18
|
+
const lis = items.map(item => `<li>${item}</li>`).join('');
|
|
19
|
+
return `<ul class="concept-list">${lis}</ul>`;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render stem:[...] math notation to KaTeX HTML.
|
|
25
|
+
* Also handles cross-reference inline patterns (URN refs).
|
|
26
|
+
*/
|
|
27
|
+
export function renderMath(text: string, xrefResolver?: XrefResolver): string {
|
|
28
|
+
if (!text) return '';
|
|
29
|
+
let result = text;
|
|
30
|
+
|
|
31
|
+
result = result.replace(/\*stem:\[([^\]]*)\]\*/g, (_, math) => {
|
|
32
|
+
return renderKatexSpan(math, true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
result = result.replace(/stem:\[([^\]]*)\]/g, (_, math) => {
|
|
36
|
+
return renderKatexSpan(math, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
result = convertLists(result);
|
|
40
|
+
|
|
41
|
+
result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
42
|
+
result = result.replace(/~([^~]+)~/g, '<sub>$1</sub>');
|
|
43
|
+
|
|
44
|
+
// Handle URN inline refs: {{urn:...,term}} (double-braced)
|
|
45
|
+
result = result.replace(/\{\{(urn:[^,}]+),([^}]+)\}\}/g, (_, uri, term) => {
|
|
46
|
+
if (xrefResolver) {
|
|
47
|
+
return xrefResolver(uri, term.trim());
|
|
48
|
+
}
|
|
49
|
+
return term.trim();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Handle URN inline refs: {urn:...,term} (single-braced)
|
|
53
|
+
result = result.replace(/\{(urn:[^,}]+),([^}]+)\}/g, (_, uri, term) => {
|
|
54
|
+
if (xrefResolver) {
|
|
55
|
+
return xrefResolver(uri, term.trim());
|
|
56
|
+
}
|
|
57
|
+
return term.trim();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Handle any remaining {{...}} refs (fallback: show term before comma)
|
|
61
|
+
result = result.replace(/\{\{([^,}]+)(?:,\s*[^}]+)?\}\}/g, '$1');
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderKatexSpan(math: string, bold: boolean): string {
|
|
67
|
+
try {
|
|
68
|
+
const html = katex.renderToString(math, {
|
|
69
|
+
throwOnError: false,
|
|
70
|
+
displayMode: false,
|
|
71
|
+
output: 'html',
|
|
72
|
+
});
|
|
73
|
+
return `<span class="math-inline${bold ? ' math-bold' : ''}">${html}</span>`;
|
|
74
|
+
} catch {
|
|
75
|
+
return `<code class="math-fallback">${escapeHtml(math)}</code>`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function escapeHtml(text: string): string {
|
|
80
|
+
return text
|
|
81
|
+
.replace(/&/g, '&')
|
|
82
|
+
.replace(/</g, '<')
|
|
83
|
+
.replace(/>/g, '>');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clean content for plain text display (no math rendering).
|
|
88
|
+
*/
|
|
89
|
+
export function cleanContent(text: string): string {
|
|
90
|
+
if (!text) return '';
|
|
91
|
+
return text
|
|
92
|
+
.replace(/\*stem:\[([^\]]*)\]\*/g, '$1')
|
|
93
|
+
.replace(/stem:\[([^\]]*)\]/g, '$1')
|
|
94
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
95
|
+
.replace(/~([^~]+)~/g, '_$1')
|
|
96
|
+
.replace(/\n[ \t]*\* /g, '; ')
|
|
97
|
+
.replace(/\{\{urn:[^,}]+,([^}]+)\}\}/g, '$1')
|
|
98
|
+
.replace(/\{urn:[^,}]+,([^}]+)\}/g, '$1')
|
|
99
|
+
.replace(/\{\{([^,}]+)(?:,\s*[^}]+)?\}\}/g, '$1');
|
|
100
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
4
|
+
import { useDsStyle } from '../utils/dataset-style';
|
|
5
|
+
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
|
+
import { langName, langLabel } from '../utils/lang';
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ registerId: string }>();
|
|
9
|
+
|
|
10
|
+
const store = useVocabularyStore();
|
|
11
|
+
const { getColor } = useDsStyle();
|
|
12
|
+
const { loading, localError, ensureLoaded } = useDatasetLoader(() => props.registerId);
|
|
13
|
+
|
|
14
|
+
const manifest = computed(() => store.manifests.get(props.registerId));
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
19
|
+
<!-- Breadcrumb -->
|
|
20
|
+
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-ink-400 mb-6">
|
|
21
|
+
<router-link :to="{ name: 'home' }" class="hover:text-ink-700 transition-colors">Home</router-link>
|
|
22
|
+
<span class="text-ink-200">/</span>
|
|
23
|
+
<router-link :to="{ name: 'dataset', params: { registerId } }" class="hover:text-ink-700 transition-colors">{{ manifest?.title || registerId }}</router-link>
|
|
24
|
+
<span class="text-ink-200">/</span>
|
|
25
|
+
<span class="text-ink-700">About</span>
|
|
26
|
+
</nav>
|
|
27
|
+
|
|
28
|
+
<template v-if="loading">
|
|
29
|
+
<div class="animate-pulse space-y-6">
|
|
30
|
+
<div class="h-8 bg-ink-100 rounded w-32"></div>
|
|
31
|
+
<div class="card p-6"><div class="h-24 bg-ink-50 rounded"></div></div>
|
|
32
|
+
<div class="card p-6"><div class="h-48 bg-ink-50 rounded"></div></div>
|
|
33
|
+
<div class="card p-6"><div class="h-16 bg-ink-50 rounded"></div></div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
<template v-else-if="localError">
|
|
37
|
+
<div class="card p-8 border-red-200 bg-red-50/50 text-center">
|
|
38
|
+
<p class="text-red-700 font-medium mb-1">Failed to load dataset info</p>
|
|
39
|
+
<p class="text-sm text-red-600/80 mb-4">{{ localError }}</p>
|
|
40
|
+
<button @click="ensureLoaded" class="btn-primary">Retry</button>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
<template v-else-if="manifest">
|
|
44
|
+
<h1 class="font-serif text-3xl text-ink-800 mb-6">About</h1>
|
|
45
|
+
|
|
46
|
+
<!-- Description -->
|
|
47
|
+
<div class="card p-6 mb-6">
|
|
48
|
+
<h2 class="section-label">Description</h2>
|
|
49
|
+
<p class="text-ink-700 leading-relaxed mt-3">{{ manifest.description }}</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Key info -->
|
|
53
|
+
<div class="card p-6 mb-6">
|
|
54
|
+
<h2 class="section-label">Key Information</h2>
|
|
55
|
+
<dl class="space-y-4 mt-3">
|
|
56
|
+
<div class="flex items-start gap-4">
|
|
57
|
+
<dt class="text-ink-400 text-sm w-32 flex-shrink-0 pt-0.5">Owner</dt>
|
|
58
|
+
<dd class="text-ink-800 font-medium">{{ manifest.owner }}</dd>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="flex items-start gap-4">
|
|
61
|
+
<dt class="text-ink-400 text-sm w-32 flex-shrink-0 pt-0.5">Concepts</dt>
|
|
62
|
+
<dd class="text-ink-800 font-mono">{{ manifest.conceptCount.toLocaleString() }}</dd>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="flex items-start gap-4">
|
|
65
|
+
<dt class="text-ink-400 text-sm w-32 flex-shrink-0 pt-0.5">Languages</dt>
|
|
66
|
+
<dd class="text-ink-800">{{ manifest.languages.length }}</dd>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="flex items-start gap-4">
|
|
69
|
+
<dt class="text-ink-400 text-sm w-32 flex-shrink-0 pt-0.5">Last Updated</dt>
|
|
70
|
+
<dd class="text-ink-800">{{ manifest.lastUpdated }}</dd>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="flex items-start gap-4">
|
|
73
|
+
<dt class="text-ink-400 text-sm w-32 flex-shrink-0 pt-0.5">Schema Version</dt>
|
|
74
|
+
<dd class="text-ink-800 font-mono text-sm">{{ manifest.schemaVersion }}</dd>
|
|
75
|
+
</div>
|
|
76
|
+
<div v-if="manifest.sourceRepo" class="flex items-start gap-4">
|
|
77
|
+
<dt class="text-ink-400 text-sm w-32 flex-shrink-0 pt-0.5">Source</dt>
|
|
78
|
+
<dd>
|
|
79
|
+
<a :href="manifest.sourceRepo" target="_blank" class="concept-link text-sm break-all">
|
|
80
|
+
{{ manifest.sourceRepo.replace('https://github.com/', '') }}
|
|
81
|
+
</a>
|
|
82
|
+
</dd>
|
|
83
|
+
</div>
|
|
84
|
+
</dl>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Languages -->
|
|
88
|
+
<div class="card p-6 mb-6">
|
|
89
|
+
<h2 class="section-label">Languages</h2>
|
|
90
|
+
<div class="flex flex-wrap gap-2 mt-3">
|
|
91
|
+
<div
|
|
92
|
+
v-for="lang in manifest.languages"
|
|
93
|
+
:key="lang"
|
|
94
|
+
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-ink-50"
|
|
95
|
+
>
|
|
96
|
+
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langLabel(lang) }}</span>
|
|
97
|
+
<span class="text-sm font-medium text-ink-700">{{ langName(lang) }}</span>
|
|
98
|
+
<span class="text-xs text-ink-300">({{ lang }})</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Tags -->
|
|
104
|
+
<div v-if="manifest.tags?.length" class="card p-6">
|
|
105
|
+
<h2 class="section-label">Tags</h2>
|
|
106
|
+
<div class="flex flex-wrap gap-2 mt-3">
|
|
107
|
+
<span
|
|
108
|
+
v-for="tag in manifest.tags"
|
|
109
|
+
:key="tag"
|
|
110
|
+
class="badge"
|
|
111
|
+
:style="{
|
|
112
|
+
backgroundColor: getColor(registerId) + '15',
|
|
113
|
+
color: getColor(registerId),
|
|
114
|
+
}"
|
|
115
|
+
>
|
|
116
|
+
{{ tag }}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|