@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.
- package/cli/index.mjs +32 -0
- package/env.d.ts +15 -0
- package/package.json +12 -2
- package/scripts/__tests__/doctor.test.mjs +147 -0
- package/scripts/doctor.mjs +327 -0
- package/scripts/generate-data.mjs +136 -0
- package/scripts/generate-ontology-data.mjs +3 -3
- package/scripts/generate-ontology-schema.mjs +3 -3
- package/scripts/lib/agents-turtle.mjs +64 -0
- package/scripts/lib/bibliography-turtle.mjs +54 -0
- package/scripts/lib/build-activity-turtle.mjs +92 -0
- package/scripts/lib/build-cache.mjs +70 -0
- package/scripts/lib/dataset-turtle.mjs +79 -0
- package/scripts/lib/turtle-escape.mjs +0 -0
- package/scripts/lib/version-turtle.mjs +56 -0
- package/scripts/lib/vocab-turtle.mjs +64 -0
- package/scripts/normalize-yaml.mjs +99 -0
- package/scripts/sync-concept-model.mjs +86 -0
- package/scripts/validate-shacl.mjs +194 -0
- package/src/__fixtures__/concept-shape.ttl +20 -0
- package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
- package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
- package/src/__fixtures__/shacl/good/concept.ttl +8 -0
- package/src/__tests__/__fixtures__/concepts.ts +221 -0
- package/src/__tests__/adapters/concept-identity.test.ts +76 -0
- package/src/__tests__/components/error-boundary.test.ts +109 -0
- package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
- package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
- package/src/__tests__/concept-rdf/differential.test.ts +96 -0
- package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
- package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
- package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
- package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
- package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
- package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
- package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
- package/src/__tests__/concept-rdf-view.test.ts +136 -0
- package/src/__tests__/dataset-style.test.ts +12 -7
- package/src/__tests__/errors/errors.test.ts +142 -0
- package/src/__tests__/format-downloads.test.ts +47 -65
- package/src/__tests__/markdown-lite.test.ts +19 -0
- package/src/__tests__/perf/bundle-layout.test.ts +50 -0
- package/src/__tests__/perf/serialization-perf.test.ts +121 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
- package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
- package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
- package/src/__tests__/scripts/build-cache.test.ts +78 -0
- package/src/__tests__/scripts/build-integration.test.ts +134 -0
- package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
- package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
- package/src/__tests__/scripts/stryker-config.test.ts +33 -0
- package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
- package/src/__tests__/scripts/version-turtle.test.ts +72 -0
- package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
- package/src/__tests__/use-format-registry.test.ts +125 -0
- package/src/__tests__/utils/bcp47.test.ts +166 -0
- package/src/__tests__/utils/color-theme.test.ts +143 -0
- package/src/__tests__/utils/url-safety.test.ts +65 -0
- package/src/__tests__/validate-shacl.test.ts +100 -0
- package/src/adapters/DatasetAdapter.ts +11 -5
- package/src/adapters/GraphDataSource.ts +2 -1
- package/src/adapters/UriRouter.ts +2 -1
- package/src/adapters/concept-identity.ts +69 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -1
- package/src/adapters/non-verbal/figure-bridge.ts +22 -23
- package/src/adapters/non-verbal/formula-bridge.ts +11 -9
- package/src/adapters/non-verbal/glossarist-augment.d.ts +133 -0
- package/src/adapters/non-verbal/index.ts +12 -9
- package/src/adapters/non-verbal/kind.ts +2 -1
- package/src/adapters/non-verbal/table-bridge.ts +12 -10
- package/src/adapters/non-verbal/types.ts +36 -54
- package/src/adapters/non-verbal-resolver.ts +6 -3
- package/src/components/AppSidebar.vue +189 -93
- package/src/components/ConceptDetail.vue +8 -0
- package/src/components/ConceptEditionRail.vue +222 -0
- package/src/components/ConceptRdfView.vue +37 -377
- package/src/components/DatasetSeriesCard.vue +270 -0
- package/src/components/ErrorBoundary.vue +95 -0
- package/src/components/FormatDownloads.vue +17 -13
- package/src/components/HomeSeriesSection.vue +277 -0
- package/src/components/NonVerbalRepDisplay.vue +2 -2
- package/src/components/RelationSphere.vue +1672 -0
- package/src/components/SidebarSeriesSection.vue +239 -0
- package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
- package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
- package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
- package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
- package/src/components/concept-rdf/agents-emitter.ts +82 -0
- package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
- package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
- package/src/components/concept-rdf/concept-emitter.ts +443 -0
- package/src/components/concept-rdf/dataset-emitter.ts +95 -0
- package/src/components/concept-rdf/group-emitter.ts +69 -0
- package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
- package/src/components/concept-rdf/jsonld-writer.ts +82 -0
- package/src/components/concept-rdf/predicates.ts +261 -0
- package/src/components/concept-rdf/provenance.ts +80 -0
- package/src/components/concept-rdf/rdf-graph.ts +211 -0
- package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
- package/src/components/concept-rdf/sections-builder.ts +62 -0
- package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
- package/src/components/concept-rdf/turtle-writer.ts +116 -0
- package/src/components/concept-rdf/use-rdf-document.ts +72 -0
- package/src/components/concept-rdf/version-emitter.ts +65 -0
- package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
- package/src/components/figure/FigureDisplay.vue +16 -15
- package/src/components/figure/FigureImages.vue +38 -16
- package/src/components/figure/figure-image-pick.ts +1 -1
- package/src/components/figure/figure-layout.ts +1 -1
- package/src/components/formula/FormulaDisplay.vue +11 -9
- package/src/components/formula/FormulaExpression.vue +4 -4
- package/src/components/non-verbal/NonVerbalCaption.vue +5 -5
- package/src/components/non-verbal/NonVerbalSources.vue +3 -11
- package/src/components/table/TableDisplay.vue +6 -4
- package/src/components/table/TableMarkup.vue +1 -1
- package/src/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- package/src/composables/use-non-verbal-entity.ts +2 -1
- package/src/composables/useDatasetSeries.ts +258 -0
- package/src/composables/useSphereProjection.ts +125 -0
- package/src/config/group-types.ts +92 -0
- package/src/config/types.ts +81 -2
- package/src/config/use-site-config.ts +2 -1
- package/src/errors.ts +136 -0
- package/src/i18n/locales/eng.yml +24 -0
- package/src/i18n/locales/fra.yml +24 -0
- package/src/stores/vocabulary.ts +3 -1
- package/src/style.css +17 -2
- package/src/types/agents-version-turtle.d.ts +27 -0
- package/src/types/bibliography-turtle.d.ts +12 -0
- package/src/types/build-activity-turtle.d.ts +16 -0
- package/src/types/build-cache.d.ts +20 -0
- package/src/types/dataset-turtle.d.ts +32 -0
- package/src/types/normalize-yaml.d.ts +16 -0
- package/src/types/turtle-escape.d.ts +6 -0
- package/src/types/vocab-turtle.d.ts +13 -0
- package/src/utils/asciidoc-lite.ts +11 -6
- package/src/utils/bcp47.ts +141 -0
- package/src/utils/color-theme-integration.ts +11 -0
- package/src/utils/color-theme.ts +129 -0
- package/src/utils/dataset-style.ts +31 -6
- package/src/utils/locale.ts +6 -14
- package/src/utils/markdown-lite.ts +6 -1
- package/src/utils/relation-sphere-styling.ts +63 -0
- package/src/utils/relationship-categories.ts +30 -0
- package/src/utils/url-safety.ts +30 -0
- package/src/views/ConceptView.vue +183 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare module '*/scripts/lib/dataset-turtle.mjs' {
|
|
2
|
+
export interface DatasetDistribution {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly mediaType: string;
|
|
6
|
+
readonly downloadUrl: string;
|
|
7
|
+
readonly byteSize?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DatasetSection {
|
|
11
|
+
readonly collectionIri: string;
|
|
12
|
+
readonly title: string;
|
|
13
|
+
readonly memberUris: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DatasetTurtleInput {
|
|
17
|
+
readonly datasetIri: string;
|
|
18
|
+
readonly registerId: string;
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly description?: string;
|
|
21
|
+
readonly modified: string;
|
|
22
|
+
readonly languages: readonly string[];
|
|
23
|
+
readonly distributions: readonly DatasetDistribution[];
|
|
24
|
+
readonly topConceptUris: readonly string[];
|
|
25
|
+
readonly sections: readonly DatasetSection[];
|
|
26
|
+
readonly sourceRepoUrl?: string;
|
|
27
|
+
readonly publisherIri?: string;
|
|
28
|
+
readonly contactIri?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildDatasetTurtle(input: DatasetTurtleInput): string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module '*/scripts/normalize-yaml.mjs' {
|
|
2
|
+
export interface NormalizeResult {
|
|
3
|
+
checked: number;
|
|
4
|
+
nonNfc: number;
|
|
5
|
+
fixed: string[];
|
|
6
|
+
check: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface NormalizeOptions {
|
|
10
|
+
root?: string;
|
|
11
|
+
check?: boolean;
|
|
12
|
+
paths?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeYaml(options?: NormalizeOptions): NormalizeResult;
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare module '*/scripts/lib/turtle-escape.mjs' {
|
|
2
|
+
export function ttlLit(s: unknown): string;
|
|
3
|
+
export function ttlPrefixed(qname: string): string;
|
|
4
|
+
export function ttlIri(iri: string): string;
|
|
5
|
+
export function assertValidIri(iri: unknown, context?: string): string;
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare module '*/scripts/lib/vocab-turtle.mjs' {
|
|
2
|
+
export interface VocabTerm {
|
|
3
|
+
readonly iri: string;
|
|
4
|
+
readonly label: string;
|
|
5
|
+
}
|
|
6
|
+
export interface VocabScheme {
|
|
7
|
+
readonly schemeIri: string;
|
|
8
|
+
readonly label: string;
|
|
9
|
+
readonly terms: readonly VocabTerm[];
|
|
10
|
+
}
|
|
11
|
+
export function buildVocabularyTurtle(): string;
|
|
12
|
+
export function listVocabSchemes(): readonly VocabScheme[];
|
|
13
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { escapeHtml, escapeAttr } from './escape';
|
|
7
|
+
import { sanitizeUrl } from './url-safety';
|
|
7
8
|
|
|
8
9
|
export function renderAsciiDocLite(text: string): string {
|
|
9
10
|
if (!text) return '';
|
|
@@ -102,14 +103,18 @@ export function renderAsciiDocLite(text: string): string {
|
|
|
102
103
|
|
|
103
104
|
function inlineFormat(text: string): string {
|
|
104
105
|
// AsciiDoc link: https://example.com[text]
|
|
105
|
-
text = text.replace(/(https?:\/\/[^\s\[]+)\[([^\]]*)\]/g, (_, url, label) =>
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
text = text.replace(/(https?:\/\/[^\s\[]+)\[([^\]]*)\]/g, (_, url, label) => {
|
|
107
|
+
const href = sanitizeUrl(url);
|
|
108
|
+
if (!href) return escapeHtml(label || url);
|
|
109
|
+
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener">${escapeHtml(label || url)}</a>`;
|
|
110
|
+
});
|
|
108
111
|
|
|
109
112
|
// Bare URLs
|
|
110
|
-
text = text.replace(/(?<!href="|">)(https?:\/\/[^\s<]+)/g, url =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
text = text.replace(/(?<!href="|">)(https?:\/\/[^\s<]+)/g, url => {
|
|
114
|
+
const href = sanitizeUrl(url);
|
|
115
|
+
if (!href) return escapeHtml(url);
|
|
116
|
+
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener">${escapeHtml(url)}</a>`;
|
|
117
|
+
});
|
|
113
118
|
|
|
114
119
|
// Monospace: `text`
|
|
115
120
|
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { InvalidLangTagError } from '../errors';
|
|
2
|
+
|
|
3
|
+
export interface LangTag {
|
|
4
|
+
readonly primary: string;
|
|
5
|
+
readonly script?: string;
|
|
6
|
+
readonly region?: string;
|
|
7
|
+
readonly variants?: string[];
|
|
8
|
+
readonly privateUse?: string[];
|
|
9
|
+
readonly raw: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ISO_639_3_TO_1: Readonly<Record<string, string>> = {
|
|
13
|
+
eng: 'en', fra: 'fr', deu: 'de', zho: 'zh', ara: 'ar', jpn: 'ja', rus: 'ru',
|
|
14
|
+
kor: 'ko', spa: 'es', ita: 'it', por: 'pt', nld: 'nl', swe: 'sv', fin: 'fi',
|
|
15
|
+
dan: 'da', nob: 'nb', nno: 'nn', nor: 'no', pol: 'pl', tur: 'tr', ces: 'cs', ell: 'el',
|
|
16
|
+
heb: 'he', hin: 'hi', ind: 'id', fas: 'fa', ukr: 'uk', hun: 'hu', ron: 'ro',
|
|
17
|
+
slk: 'sk', slv: 'sl', hrv: 'hr', srp: 'sr', bul: 'bg', msa: 'ms', tha: 'th',
|
|
18
|
+
vie: 'vi', urd: 'ur', ben: 'bn', tam: 'ta', tel: 'te', mar: 'mr', guj: 'gu',
|
|
19
|
+
pan: 'pa', mal: 'ml', kan: 'kn', ori: 'or', asm: 'as', sin: 'si', nep: 'ne',
|
|
20
|
+
lit: 'lt', lav: 'lv', est: 'et', gle: 'ga', cym: 'cy', eus: 'eu', cat: 'ca',
|
|
21
|
+
glg: 'gl', afr: 'af', sqi: 'sq', mkd: 'mk', bel: 'be', kaz: 'kk', uzb: 'uz',
|
|
22
|
+
aze: 'az', hye: 'hy', kat: 'ka', mon: 'mn', tuk: 'tk', uig: 'ug', tgl: 'tl',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ISO_639_1_TO_3: Readonly<Record<string, string>> = (() => {
|
|
26
|
+
const out: Record<string, string> = {};
|
|
27
|
+
for (const [k, v] of Object.entries(ISO_639_3_TO_1)) out[v] = k;
|
|
28
|
+
return Object.freeze(out);
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
const DEFAULT_SCRIPTS: Readonly<Record<string, string>> = {
|
|
32
|
+
zh: 'Hans',
|
|
33
|
+
sr: 'Cyrl',
|
|
34
|
+
uz: 'Latn',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SCRIPT_RE = /^[A-Z][a-z]{3}$/;
|
|
38
|
+
const ISO_ALPHA2_REGION_RE = /^[A-Z]{2}$/;
|
|
39
|
+
const UN_M49_REGION_RE = /^\d{3}$/;
|
|
40
|
+
|
|
41
|
+
export function mapIso6393To6391(code: string): string | null {
|
|
42
|
+
return ISO_639_3_TO_1[code] ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function mapIso6391To6393(code: string): string | null {
|
|
46
|
+
return ISO_639_1_TO_3[code] ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseLangTag(input: string): LangTag {
|
|
50
|
+
if (!input || typeof input !== 'string') {
|
|
51
|
+
throw new InvalidLangTagError(`Invalid language tag: ${String(input)}`, { input });
|
|
52
|
+
}
|
|
53
|
+
const trimmed = input.trim();
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
throw new InvalidLangTagError(`Empty language tag`, { input });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const segments = trimmed.split('-');
|
|
59
|
+
const first = segments[0]!;
|
|
60
|
+
if (!/^[a-zA-Z]{2,3}$/.test(first)) {
|
|
61
|
+
throw new InvalidLangTagError(
|
|
62
|
+
`Invalid primary subtag: ${first}`,
|
|
63
|
+
{ input, subtag: first },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const primary = (ISO_639_3_TO_1[first.toLowerCase()] ?? first.toLowerCase());
|
|
68
|
+
const tag: LangTag = { primary, raw: input };
|
|
69
|
+
|
|
70
|
+
let i = 1;
|
|
71
|
+
let privateUse: string[] | undefined;
|
|
72
|
+
if (segments[i] === 'x') {
|
|
73
|
+
privateUse = segments.slice(i + 1);
|
|
74
|
+
i = segments.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let script: string | undefined;
|
|
78
|
+
let region: string | undefined;
|
|
79
|
+
let variants: string[] | undefined;
|
|
80
|
+
|
|
81
|
+
for (; i < segments.length; i++) {
|
|
82
|
+
const seg = segments[i]!;
|
|
83
|
+
if (seg === 'x') {
|
|
84
|
+
privateUse = segments.slice(i + 1);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (SCRIPT_RE.test(seg) && !script) {
|
|
88
|
+
script = seg;
|
|
89
|
+
} else if ((ISO_ALPHA2_REGION_RE.test(seg) || UN_M49_REGION_RE.test(seg)) && !region) {
|
|
90
|
+
region = seg;
|
|
91
|
+
} else if (/^[a-z0-9]{4,8}$/i.test(seg)) {
|
|
92
|
+
variants = variants ?? [];
|
|
93
|
+
variants.push(seg);
|
|
94
|
+
} else {
|
|
95
|
+
throw new InvalidLangTagError(
|
|
96
|
+
`Unrecognized language subtag: ${seg}`,
|
|
97
|
+
{ input, subtag: seg },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const finalTag: LangTag = {
|
|
103
|
+
primary,
|
|
104
|
+
script: script ?? DEFAULT_SCRIPTS[primary],
|
|
105
|
+
region,
|
|
106
|
+
variants,
|
|
107
|
+
privateUse,
|
|
108
|
+
raw: input,
|
|
109
|
+
};
|
|
110
|
+
return finalTag;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatLangTag(tag: LangTag): string {
|
|
114
|
+
const parts: string[] = [tag.primary];
|
|
115
|
+
if (tag.script) parts.push(tag.script);
|
|
116
|
+
if (tag.region) parts.push(tag.region);
|
|
117
|
+
if (tag.variants?.length) parts.push(...tag.variants);
|
|
118
|
+
if (tag.privateUse?.length) parts.push('x', ...tag.privateUse);
|
|
119
|
+
return parts.join('-');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function canonicalLangTag(input: string): string {
|
|
123
|
+
return formatLangTag(parseLangTag(input));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isValidLangTag(input: string): boolean {
|
|
127
|
+
try {
|
|
128
|
+
parseLangTag(input);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isNfc(text: string): boolean {
|
|
136
|
+
return text.normalize('NFC') === text;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function toNfc(text: string): string {
|
|
140
|
+
return text.normalize('NFC');
|
|
141
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color-theme integration bridge for the relationship-categories module.
|
|
3
|
+
* Re-exports the color pair accessors with a stable interface so consumers
|
|
4
|
+
* don't need to import from two places.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
colorPairForType,
|
|
8
|
+
colorPairForCategory,
|
|
9
|
+
colorThemeForOverrides,
|
|
10
|
+
} from './relationship-categories';
|
|
11
|
+
export type { ColorPair } from './color-theme';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color theme SSOT. Loads `data/colors.json` defaults and merges per-
|
|
3
|
+
* deployment overrides from `site-config.json` `colors` block.
|
|
4
|
+
*
|
|
5
|
+
* Pure data + pure accessors — no Vue reactivity. Reactive consumption
|
|
6
|
+
* is via the `useColorTheme()` composable.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import type { DatasetColorSpec, SiteColors } from '../config/types';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const COLORS_PATH = join(__dirname, '..', '..', 'data', 'colors.json');
|
|
15
|
+
|
|
16
|
+
export interface ColorPair {
|
|
17
|
+
readonly light: string;
|
|
18
|
+
readonly dark: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ColorDefaults {
|
|
22
|
+
readonly relationshipCategory: Record<string, ColorPair>;
|
|
23
|
+
readonly relationshipType: Record<string, ColorPair>;
|
|
24
|
+
readonly conceptStatus: Record<string, ColorPair>;
|
|
25
|
+
readonly groupKind: Record<string, ColorPair>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cachedDefaults: ColorDefaults | undefined;
|
|
29
|
+
|
|
30
|
+
function loadDefaults(): ColorDefaults {
|
|
31
|
+
if (cachedDefaults) return cachedDefaults;
|
|
32
|
+
const raw = JSON.parse(readFileSync(COLORS_PATH, 'utf8'));
|
|
33
|
+
cachedDefaults = {
|
|
34
|
+
relationshipCategory: raw.relationshipCategory,
|
|
35
|
+
relationshipType: raw.relationshipType,
|
|
36
|
+
conceptStatus: raw.conceptStatus,
|
|
37
|
+
groupKind: raw.groupKind,
|
|
38
|
+
};
|
|
39
|
+
return cachedDefaults!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalize(spec: DatasetColorSpec | undefined): ColorPair | undefined {
|
|
43
|
+
if (spec == null) return undefined;
|
|
44
|
+
if (typeof spec === 'string') return { light: spec, dark: spec };
|
|
45
|
+
return { light: spec.light, dark: spec.dark };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolvePair(
|
|
49
|
+
key: string | undefined,
|
|
50
|
+
overrides: Record<string, DatasetColorSpec> | undefined,
|
|
51
|
+
defaults: Record<string, ColorPair>,
|
|
52
|
+
fallback: ColorPair,
|
|
53
|
+
): ColorPair {
|
|
54
|
+
if (key && overrides) {
|
|
55
|
+
const ov = normalize(overrides[key]);
|
|
56
|
+
if (ov) return ov;
|
|
57
|
+
}
|
|
58
|
+
if (key && defaults[key]) return defaults[key];
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createColorTheme(siteColors?: SiteColors) {
|
|
63
|
+
const defaults = loadDefaults();
|
|
64
|
+
|
|
65
|
+
function relationshipCategoryColor(categoryId: string): ColorPair {
|
|
66
|
+
return resolvePair(
|
|
67
|
+
categoryId,
|
|
68
|
+
siteColors?.relationshipCategory,
|
|
69
|
+
defaults.relationshipCategory,
|
|
70
|
+
defaults.relationshipCategory.associative,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function relationshipTypeColor(typeId: string, categoryId?: string): ColorPair {
|
|
75
|
+
const ov = normalize(siteColors?.relationshipType?.[typeId]);
|
|
76
|
+
if (ov) return ov;
|
|
77
|
+
if (defaults.relationshipType[typeId]) return defaults.relationshipType[typeId];
|
|
78
|
+
if (categoryId) return relationshipCategoryColor(categoryId);
|
|
79
|
+
return defaults.relationshipCategory.associative;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function conceptStatusColor(statusId: string): ColorPair {
|
|
83
|
+
return resolvePair(
|
|
84
|
+
statusId,
|
|
85
|
+
siteColors?.conceptStatus,
|
|
86
|
+
defaults.conceptStatus,
|
|
87
|
+
{ light: '#6B6E7D', dark: '#9CA3AF' },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function groupKindColor(kind: string): ColorPair {
|
|
92
|
+
return resolvePair(
|
|
93
|
+
kind,
|
|
94
|
+
siteColors?.groupKind,
|
|
95
|
+
defaults.groupKind,
|
|
96
|
+
defaults.groupKind.default,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function datasetColor(datasetId: string, declared?: DatasetColorSpec): ColorPair {
|
|
101
|
+
const ov = normalize(siteColors?.dataset?.[datasetId]);
|
|
102
|
+
if (ov) return ov;
|
|
103
|
+
const declaredPair = normalize(declared);
|
|
104
|
+
if (declaredPair) return declaredPair;
|
|
105
|
+
return { light: '#3366ff', dark: '#60A5FA' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function groupColor(groupId: string, declared?: DatasetColorSpec): ColorPair {
|
|
109
|
+
const ov = normalize(siteColors?.group?.[groupId]);
|
|
110
|
+
if (ov) return ov;
|
|
111
|
+
const declaredPair = normalize(declared);
|
|
112
|
+
if (declaredPair) return declaredPair;
|
|
113
|
+
return groupKindColor('default');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
relationshipCategoryColor,
|
|
118
|
+
relationshipTypeColor,
|
|
119
|
+
conceptStatusColor,
|
|
120
|
+
groupKindColor,
|
|
121
|
+
datasetColor,
|
|
122
|
+
groupColor,
|
|
123
|
+
defaults,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type ColorTheme = ReturnType<typeof createColorTheme>;
|
|
128
|
+
|
|
129
|
+
export const FALLBACK_COLOR_PAIR: ColorPair = { light: '#6B6E7D', dark: '#9CA3AF' };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
2
|
+
import type { DatasetColorSpec } from '../config/types';
|
|
3
|
+
import { createColorTheme } from './color-theme';
|
|
2
4
|
|
|
3
5
|
const PALETTE = [
|
|
4
6
|
'#3366ff', '#0d9488', '#d97706', '#8b5cf6',
|
|
@@ -7,9 +9,16 @@ const PALETTE = [
|
|
|
7
9
|
];
|
|
8
10
|
|
|
9
11
|
export interface DsStyle {
|
|
12
|
+
/** Single-hex backward-compat color (light mode). */
|
|
10
13
|
color: string;
|
|
14
|
+
/** Explicit light-mode color. */
|
|
11
15
|
light: string;
|
|
16
|
+
/** Explicit dark-mode color. */
|
|
12
17
|
dark: string;
|
|
18
|
+
/** Light-mode rgba with custom alpha. */
|
|
19
|
+
lightAlpha: (a: number) => string;
|
|
20
|
+
/** Dark-mode rgba with custom alpha. */
|
|
21
|
+
darkAlpha: (a: number) => string;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
function hexToRgba(hex: string, alpha: number): string {
|
|
@@ -19,14 +28,29 @@ function hexToRgba(hex: string, alpha: number): string {
|
|
|
19
28
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
|
-
export function makeDsStyle(
|
|
31
|
+
export function makeDsStyle(spec?: DatasetColorSpec, fallbackLight = '#3366ff'): DsStyle {
|
|
32
|
+
const pair = normalizeSpec(spec, fallbackLight);
|
|
23
33
|
return {
|
|
24
|
-
color,
|
|
25
|
-
light:
|
|
26
|
-
dark:
|
|
34
|
+
color: pair.light,
|
|
35
|
+
light: pair.light,
|
|
36
|
+
dark: pair.dark,
|
|
37
|
+
lightAlpha: (a: number) => hexToRgba(pair.light, a),
|
|
38
|
+
darkAlpha: (a: number) => hexToRgba(pair.dark, a),
|
|
27
39
|
};
|
|
28
40
|
}
|
|
29
41
|
|
|
42
|
+
function normalizeSpec(spec: DatasetColorSpec | undefined, fallback: string): { light: string; dark: string } {
|
|
43
|
+
if (spec == null) {
|
|
44
|
+
const dark = hexToRgba(fallback, 0.85);
|
|
45
|
+
return { light: fallback, dark };
|
|
46
|
+
}
|
|
47
|
+
if (typeof spec === 'string') {
|
|
48
|
+
const dark = hexToRgba(spec, 0.85);
|
|
49
|
+
return { light: spec, dark };
|
|
50
|
+
}
|
|
51
|
+
return { light: spec.light, dark: spec.dark };
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
export function paletteColor(index: number): string {
|
|
31
55
|
return PALETTE[index % PALETTE.length];
|
|
32
56
|
}
|
|
@@ -40,8 +64,9 @@ export function useDsStyle() {
|
|
|
40
64
|
|
|
41
65
|
const store = useVocabularyStore();
|
|
42
66
|
const ds = store.datasetList.find(d => d.id === registerId);
|
|
43
|
-
const
|
|
44
|
-
const
|
|
67
|
+
const declared = ds?.manifest?.color as DatasetColorSpec | undefined;
|
|
68
|
+
const fallback = paletteColor(store.datasetList.findIndex(d => d.id === registerId));
|
|
69
|
+
const style = makeDsStyle(declared, fallback);
|
|
45
70
|
cache.set(registerId, style);
|
|
46
71
|
return style;
|
|
47
72
|
}
|
package/src/utils/locale.ts
CHANGED
|
@@ -5,27 +5,19 @@
|
|
|
5
5
|
* Both the non-verbal entity resolver and any other localized content
|
|
6
6
|
* resolution should call `pickLocaleText` / `pickLocaleMap` rather than
|
|
7
7
|
* implement their own fallback chain.
|
|
8
|
+
*
|
|
9
|
+
* Language-code mapping (ISO 639-3 ↔ ISO 639-1) and BCP-47 parsing live in
|
|
10
|
+
* `./bcp47`; this module re-exports the mapping for backwards
|
|
11
|
+
* compatibility.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
import { fetchLocalizedString } from 'glossarist';
|
|
15
|
+
import { mapIso6393To6391 } from './bcp47';
|
|
11
16
|
|
|
12
17
|
const DEFAULT_FALLBACK_CHAIN: readonly string[] = ['eng'] as const;
|
|
13
18
|
|
|
14
19
|
const RTL_LOCALES: ReadonlySet<string> = new Set(['ara', 'heb', 'fas', 'urd', 'arb']);
|
|
15
20
|
|
|
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
21
|
export type LocalizedText = Record<string, string>;
|
|
30
22
|
|
|
31
23
|
export interface ResolvedLocaleText {
|
|
@@ -77,7 +69,7 @@ export function isRtl(locale: string): boolean {
|
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
export function localeToBcp47(locale: string): string {
|
|
80
|
-
return
|
|
72
|
+
return mapIso6393To6391(locale) ?? locale;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
export function resolveFallbackChain(datasetLocales?: readonly string[]): readonly string[] {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { escapeHtml } from './escape';
|
|
2
|
+
import { sanitizeUrl } from './url-safety';
|
|
2
3
|
|
|
3
4
|
const INLINE_PATTERNS: [RegExp, (m: RegExpMatchArray) => string][] = [
|
|
4
5
|
[/\*\*(.+?)\*\*/g, m => `<strong>${m[1]}</strong>`],
|
|
5
6
|
[/(?<!\*)\*([^*]+?)\*(?!\*)/g, m => `<em>${m[1]}</em>`],
|
|
6
7
|
[/`([^`]+?)`/g, m => `<code>${m[1]}</code>`],
|
|
7
|
-
[/\[([^\]]+)\]\(([^)]+)\)/g, m =>
|
|
8
|
+
[/\[([^\]]+)\]\(([^)]+)\)/g, m => {
|
|
9
|
+
const href = sanitizeUrl(m[2]);
|
|
10
|
+
if (!href) return escapeHtml(m[1]);
|
|
11
|
+
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener">${m[1]}</a>`;
|
|
12
|
+
}],
|
|
8
13
|
];
|
|
9
14
|
|
|
10
15
|
function renderInline(text: string): string {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relation sphere styling — bridge between the project's semantic SSOTs
|
|
3
|
+
* (taxonomy + color-theme) and the visual encoding the sphere needs.
|
|
4
|
+
*
|
|
5
|
+
* Single source: taxonomy drives category/type identity.
|
|
6
|
+
* Color-theme drives light/dark color pairs.
|
|
7
|
+
* This module adds only the sphere-specific concern: dasharray patterns.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
RELATIONSHIP_CATEGORIES,
|
|
11
|
+
categorizeRelationship,
|
|
12
|
+
relationshipLabel,
|
|
13
|
+
} from './relationship-categories';
|
|
14
|
+
import { colorPairForType, colorPairForCategory, type ColorPair } from './color-theme-integration';
|
|
15
|
+
|
|
16
|
+
export interface SphereRelationCategory {
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
readonly color: string;
|
|
20
|
+
readonly dasharray: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Dasharray per category — sphere-specific visual encoding. Sourced
|
|
24
|
+
* from colors.json `relationshipCategoryDash` block (added in #14). */
|
|
25
|
+
const DASHARRAY: Record<string, string> = {
|
|
26
|
+
lifecycle: 'none',
|
|
27
|
+
mapping: '1 2',
|
|
28
|
+
hierarchical: '6 3 1 3',
|
|
29
|
+
associative: 'none',
|
|
30
|
+
comparative: '2 2',
|
|
31
|
+
definitional: '8 4',
|
|
32
|
+
spatiotemporal: '4 2 1 2',
|
|
33
|
+
lexical: '3 1',
|
|
34
|
+
designation: '1 3',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Build the sphere-category list from the taxonomy SSOT. */
|
|
38
|
+
export const SPHERE_RELATION_CATEGORIES: readonly SphereRelationCategory[] =
|
|
39
|
+
RELATIONSHIP_CATEGORIES.map(cat => ({
|
|
40
|
+
key: cat.id,
|
|
41
|
+
label: cat.label,
|
|
42
|
+
color: colorPairForCategory(cat.id).light,
|
|
43
|
+
dasharray: DASHARRAY[cat.id] ?? 'none',
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
export function categorizeRelationForSphere(type: string): string {
|
|
47
|
+
return categorizeRelationship(type).id;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function sphereCategoryForType(type: string): SphereRelationCategory {
|
|
51
|
+
const cat = categorizeRelationship(type);
|
|
52
|
+
return SPHERE_RELATION_CATEGORIES.find(c => c.key === cat.id)
|
|
53
|
+
?? SPHERE_RELATION_CATEGORIES[3];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function colorForTypeInMode(type: string, isDark: boolean): string {
|
|
57
|
+
const pair = colorPairForType(type);
|
|
58
|
+
return isDark ? pair.dark : pair.light;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function relationLabel(type: string): string {
|
|
62
|
+
return relationshipLabel(type);
|
|
63
|
+
}
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* RELATIONSHIP_CATEGORIES, INVERSE_RELATIONSHIPS, and the category lookup
|
|
5
5
|
* map are all computed from `taxonomies.json` at module load time.
|
|
6
6
|
* Adding a new relationship type requires only a taxonomy edit — no code changes.
|
|
7
|
+
*
|
|
8
|
+
* Color pairs (light + dark) come from `data/colors.json` via the color-theme
|
|
9
|
+
* module, merged with per-deployment overrides from `site-config.json`.
|
|
7
10
|
*/
|
|
8
11
|
import { ontology } from '../adapters/ontology-registry';
|
|
9
12
|
import type { TaxonomyCategory } from '../adapters/ontology-registry';
|
|
13
|
+
import { createColorTheme, type ColorPair, type ColorTheme } from './color-theme';
|
|
10
14
|
|
|
11
15
|
export interface RelationshipCategory {
|
|
12
16
|
id: string;
|
|
@@ -95,3 +99,29 @@ export function relationshipLabel(type: string): string {
|
|
|
95
99
|
export function relationshipDefinition(type: string): string | null {
|
|
96
100
|
return ontology.getDefinition('relationshipType', type);
|
|
97
101
|
}
|
|
102
|
+
|
|
103
|
+
// ── Color pairs (light + dark) ────────────────────────────────────────────
|
|
104
|
+
//
|
|
105
|
+
// `createColorTheme(undefined)` returns the default theme (no per-deployment
|
|
106
|
+
// overrides). Components that need overrides should construct their own theme
|
|
107
|
+
// via `useSiteConfig()` + `createColorTheme(config.colors)`.
|
|
108
|
+
|
|
109
|
+
const defaultTheme: ColorTheme = createColorTheme(undefined);
|
|
110
|
+
|
|
111
|
+
/** Returns the color pair for a relationship type, optionally given a known
|
|
112
|
+
* category id (skips a taxonomy lookup when the caller already knows it). */
|
|
113
|
+
export function colorPairForType(type: string, categoryId?: string): ColorPair {
|
|
114
|
+
const concept = ontology.getConcept('relationshipType', type);
|
|
115
|
+
const cat = categoryId ?? concept?.category;
|
|
116
|
+
return defaultTheme.relationshipTypeColor(type, cat);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Returns the color pair for a category id (e.g. "lifecycle"). */
|
|
120
|
+
export function colorPairForCategory(categoryId: string): ColorPair {
|
|
121
|
+
return defaultTheme.relationshipCategoryColor(categoryId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Construct a per-deployment theme that overrides defaults. */
|
|
125
|
+
export function colorThemeForOverrides(siteColors: Parameters<typeof createColorTheme>[0]): ColorTheme {
|
|
126
|
+
return createColorTheme(siteColors);
|
|
127
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const SAFE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
2
|
+
|
|
3
|
+
const DANGEROUS_TOKEN_RE = /[\s<>"]/;
|
|
4
|
+
|
|
5
|
+
export function isSafeUrl(url: string): boolean {
|
|
6
|
+
if (!url || typeof url !== 'string') return false;
|
|
7
|
+
if (DANGEROUS_TOKEN_RE.test(url)) return false;
|
|
8
|
+
|
|
9
|
+
const trimmed = url.trim();
|
|
10
|
+
if (!trimmed) return false;
|
|
11
|
+
|
|
12
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
|
|
13
|
+
try {
|
|
14
|
+
const u = new URL(trimmed);
|
|
15
|
+
return SAFE_URL_PROTOCOLS.has(u.protocol);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sanitizeUrl(url: string): string {
|
|
29
|
+
return isSafeUrl(url) ? url : '';
|
|
30
|
+
}
|