@glossarist/concept-browser 0.7.114 → 0.7.115
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 +45 -2
- package/cli/index.mjs +15 -15
- package/cli/index.ts +9 -11
- package/data/concept-model/shapes/glossarist.concept.yaml.schema.json +255 -0
- package/package.json +14 -12
- package/scripts/__tests__/{bibliography.test.mjs → bibliography.test.ts} +1 -1
- package/scripts/__tests__/{cli-script-contract.test.mjs → cli-script-contract.test.ts} +3 -3
- package/scripts/__tests__/{concept-groups.test.mjs → concept-groups.test.ts} +1 -1
- package/scripts/__tests__/{doctor.test.mjs → doctor.test.ts} +1 -1
- package/scripts/__tests__/{fetch-datasets.test.mjs → fetch-datasets.test.ts} +1 -1
- package/scripts/__tests__/{first-non-empty.test.mjs → first-non-empty.test.ts} +1 -1
- package/scripts/__tests__/{process-about-pages.test.mjs → process-about-pages.test.ts} +1 -1
- package/scripts/{bridge-to-astro.mjs → bridge-to-astro.ts} +2 -4
- package/scripts/{build-edges.js → build-edges.ts} +11 -13
- package/scripts/{check-syntax.mjs → check-syntax.ts} +20 -13
- package/scripts/{fetch-datasets.mjs → fetch-datasets.ts} +2 -2
- package/scripts/{generate-data.mjs → generate-data.ts} +73 -460
- package/scripts/{generate-ontology-schema.mjs → generate-ontology-schema.ts} +2 -2
- package/scripts/lib/{build-cache.mjs → build-cache.ts} +3 -2
- package/scripts/lib/concept-formats.ts +206 -0
- package/scripts/lib/concept-uri.ts +14 -0
- package/scripts/lib/{dataset-turtle.mjs → dataset-turtle.ts} +4 -3
- package/scripts/lib/harmonize-concept.ts +86 -0
- package/scripts/lib/jsonld-emitter.ts +169 -0
- package/scripts/lib/{local-path-safety.mjs → local-path-safety.ts} +6 -2
- package/scripts/lib/yaml-types.ts +262 -0
- package/scripts/{process-about-pages.mjs → process-about-pages.ts} +1 -1
- package/scripts/{smoke.mjs → smoke.ts} +4 -3
- package/scripts/{validate-shacl.mjs → validate-shacl.ts} +5 -5
- package/scripts/validate-yaml.ts +144 -0
- package/src/__tests__/extract-source-refs.test.ts +2 -3
- package/src/__tests__/perf/serialization-perf.test.ts +5 -5
- package/src/__tests__/scripts/agents-turtle.test.ts +1 -1
- package/src/__tests__/scripts/bibliography-turtle.test.ts +1 -1
- package/src/__tests__/scripts/build-activity-turtle.test.ts +1 -1
- package/src/__tests__/scripts/build-cache.test.ts +1 -1
- package/src/__tests__/scripts/build-integration.test.ts +19 -19
- package/src/__tests__/scripts/dataset-turtle.test.ts +1 -1
- package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +1 -1
- package/src/__tests__/scripts/normalize-yaml.test.ts +1 -1
- package/src/__tests__/scripts/turtle-escape.test.ts +1 -1
- package/src/__tests__/scripts/validate-yaml.test.ts +76 -0
- package/src/__tests__/scripts/version-turtle.test.ts +1 -1
- package/src/__tests__/scripts/vocab-turtle.test.ts +1 -1
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/__tests__/use-rdf-document.test.ts +2 -3
- package/src/__tests__/utils/content-renderer-xss.test.ts +57 -0
- package/src/__tests__/validate-shacl.test.ts +3 -3
- package/src/adapters/wire/index.ts +69 -0
- package/src/utils/content-renderer.ts +7 -3
- package/src/utils/url-safety.ts +1 -5
- package/tsconfig.json +2 -2
- package/vite.config.ts +1 -1
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +0 -167
- package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +0 -166
- package/src/types/agents-version-turtle.d.ts +0 -27
- package/src/types/bibliography-turtle.d.ts +0 -12
- package/src/types/build-activity-turtle.d.ts +0 -16
- package/src/types/build-cache.d.ts +0 -20
- package/src/types/dataset-turtle.d.ts +0 -32
- package/src/types/normalize-yaml.d.ts +0 -16
- package/src/types/turtle-escape.d.ts +0 -6
- package/src/types/vocab-turtle.d.ts +0 -13
- /package/scripts/{doctor.mjs → doctor.ts} +0 -0
- /package/scripts/{extract-source-refs.js → extract-source-refs.ts} +0 -0
- /package/scripts/{generate-404.js → generate-404.ts} +0 -0
- /package/scripts/{generate-ontology-data.mjs → generate-ontology-data.ts} +0 -0
- /package/scripts/lib/{agents-turtle.mjs → agents-turtle.ts} +0 -0
- /package/scripts/lib/{bibliography-turtle.mjs → bibliography-turtle.ts} +0 -0
- /package/scripts/lib/{bibliography.mjs → bibliography.ts} +0 -0
- /package/scripts/lib/build/{image-assets.mjs → image-assets.ts} +0 -0
- /package/scripts/lib/build/{non-verbal-consumer.mjs → non-verbal-consumer.ts} +0 -0
- /package/scripts/lib/{build-activity-turtle.mjs → build-activity-turtle.ts} +0 -0
- /package/scripts/lib/{concept-groups.mjs → concept-groups.ts} +0 -0
- /package/scripts/lib/{first-non-empty.mjs → first-non-empty.ts} +0 -0
- /package/scripts/lib/{turtle-escape.mjs → turtle-escape.ts} +0 -0
- /package/scripts/lib/{version-turtle.mjs → version-turtle.ts} +0 -0
- /package/scripts/lib/{vocab-turtle.mjs → vocab-turtle.ts} +0 -0
- /package/scripts/{load-site-config.mjs → load-site-config.ts} +0 -0
- /package/scripts/{migrate-v1-to-v3.mjs → migrate-v1-to-v3.ts} +0 -0
- /package/scripts/{normalize-yaml.mjs → normalize-yaml.ts} +0 -0
- /package/scripts/{sync-concept-model.mjs → sync-concept-model.ts} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Parser, Store } from 'n3';
|
|
3
|
-
import { buildDatasetTurtle } from '../../../scripts/lib/dataset-turtle
|
|
3
|
+
import { buildDatasetTurtle } from '../../../scripts/lib/dataset-turtle';
|
|
4
4
|
|
|
5
5
|
const BASE = 'https://glossarist.org/dataset';
|
|
6
6
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
migrateRelation,
|
|
6
6
|
migrateConceptData,
|
|
7
7
|
isAlreadyMigrated,
|
|
8
|
-
} from '../../../scripts/migrate-v1-to-v3
|
|
8
|
+
} from '../../../scripts/migrate-v1-to-v3';
|
|
9
9
|
|
|
10
10
|
describe('migrate-v1-to-v3 pure conversion', () => {
|
|
11
11
|
describe('isV1PartitiveRelation', () => {
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { normalizeYaml } from '../../../scripts/normalize-yaml
|
|
5
|
+
import { normalizeYaml } from '../../../scripts/normalize-yaml';
|
|
6
6
|
|
|
7
7
|
const NFC_STRING = 'café';
|
|
8
8
|
const NON_NFC_STRING = 'café';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ttlLit, ttlPrefixed, ttlIri, assertValidIri } from '../../../scripts/lib/turtle-escape
|
|
2
|
+
import { ttlLit, ttlPrefixed, ttlIri, assertValidIri } from '../../../scripts/lib/turtle-escape';
|
|
3
3
|
|
|
4
4
|
describe('D1/D2 — Turtle escaping hardening', () => {
|
|
5
5
|
describe('ttlLit', () => {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { validateYaml } from '../../../scripts/validate-yaml';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const SCHEMA_PATH = resolve(__dirname, '..', '..', '..', 'data', 'concept-model', 'shapes', 'glossarist.concept.yaml.schema.json');
|
|
9
|
+
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
10
|
+
|
|
11
|
+
describe('validate-yaml', () => {
|
|
12
|
+
it('accepts a valid concept with termid and localization', () => {
|
|
13
|
+
const yaml = `
|
|
14
|
+
termid: '1'
|
|
15
|
+
eng:
|
|
16
|
+
terms:
|
|
17
|
+
- designation: measurement unit
|
|
18
|
+
normative_status: preferred
|
|
19
|
+
definition:
|
|
20
|
+
- content: A concept used as a basis for comparison.
|
|
21
|
+
`;
|
|
22
|
+
const result = validateYaml(yaml, schema);
|
|
23
|
+
if (!result.valid) console.log('Errors:', result.errors);
|
|
24
|
+
expect(result.valid).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects a concept without termid', () => {
|
|
28
|
+
const yaml = `
|
|
29
|
+
eng:
|
|
30
|
+
terms:
|
|
31
|
+
- designation: missing id
|
|
32
|
+
`;
|
|
33
|
+
const result = validateYaml(yaml, schema);
|
|
34
|
+
expect(result.valid).toBe(false);
|
|
35
|
+
expect(result.errors.some(e => e.message.includes('termid'))).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('accepts managed concept format with underscore fields', () => {
|
|
39
|
+
const yaml = `
|
|
40
|
+
termid: '5'
|
|
41
|
+
_domains:
|
|
42
|
+
- physics
|
|
43
|
+
_status: valid
|
|
44
|
+
`;
|
|
45
|
+
const result = validateYaml(yaml, schema);
|
|
46
|
+
if (!result.valid) console.log('Errors:', result.errors);
|
|
47
|
+
expect(result.valid).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('accepts partitive relations with MECE axes', () => {
|
|
51
|
+
const yaml = `
|
|
52
|
+
termid: '3'
|
|
53
|
+
_partitiveRelations:
|
|
54
|
+
- type: hasPart
|
|
55
|
+
members:
|
|
56
|
+
- ref: part-a
|
|
57
|
+
presence: required
|
|
58
|
+
count: exactly_one
|
|
59
|
+
`;
|
|
60
|
+
const result = validateYaml(yaml, schema);
|
|
61
|
+
if (!result.valid) console.log('Errors:', result.errors);
|
|
62
|
+
expect(result.valid).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('rejects non-string termid', () => {
|
|
66
|
+
const yaml = `
|
|
67
|
+
termid: 12345
|
|
68
|
+
eng:
|
|
69
|
+
terms:
|
|
70
|
+
- designation: test
|
|
71
|
+
`;
|
|
72
|
+
const result = validateYaml(yaml, schema);
|
|
73
|
+
expect(result.valid).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Parser, Store } from 'n3';
|
|
3
|
-
import { buildVersionTurtle, buildVersionHistoryTurtle } from '../../../scripts/lib/version-turtle
|
|
3
|
+
import { buildVersionTurtle, buildVersionHistoryTurtle } from '../../../scripts/lib/version-turtle';
|
|
4
4
|
|
|
5
5
|
function parse(turtle: string): Store {
|
|
6
6
|
const parser = new Parser({ format: 'Turtle' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Parser, Store } from 'n3';
|
|
3
|
-
import { buildVocabularyTurtle, listVocabSchemes } from '../../../scripts/lib/vocab-turtle
|
|
3
|
+
import { buildVocabularyTurtle, listVocabSchemes } from '../../../scripts/lib/vocab-turtle';
|
|
4
4
|
|
|
5
5
|
function parse(turtle: string): Store {
|
|
6
6
|
const parser = new Parser({ format: 'Turtle' });
|
|
@@ -9,10 +9,10 @@ const repoRoot = path.resolve(
|
|
|
9
9
|
);
|
|
10
10
|
|
|
11
11
|
describe('scripts syntax gate', () => {
|
|
12
|
-
it('`npm run check:scripts` exits 0 (every .mjs/.js/.cjs in scripts/ and cli/ parses)', () => {
|
|
12
|
+
it('`npm run check:scripts` exits 0 (every .ts/.mjs/.js/.cjs in scripts/ and cli/ parses)', () => {
|
|
13
13
|
const result = spawnSync(
|
|
14
14
|
process.execPath,
|
|
15
|
-
[path.join(repoRoot, 'scripts', 'check-syntax.
|
|
15
|
+
['--import', 'tsx', path.join(repoRoot, 'scripts', 'check-syntax.ts')],
|
|
16
16
|
{ encoding: 'utf8' },
|
|
17
17
|
);
|
|
18
18
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// @ts-nocheck — TODO.typescript/12: remove after glossarist TS migration
|
|
2
1
|
import { describe, it, expect } from 'vitest';
|
|
3
2
|
import { ref } from 'vue';
|
|
4
3
|
import { Concept } from 'glossarist';
|
|
5
4
|
import { useRdfDocument } from '../components/use-rdf-document';
|
|
6
5
|
|
|
7
|
-
function makeConcept():
|
|
6
|
+
function makeConcept(): any {
|
|
8
7
|
return Concept.fromJSON({
|
|
9
8
|
id: '3.1.1',
|
|
10
9
|
uri: 'https://glossarist.org/test/concept/3.1.1',
|
|
@@ -24,7 +23,7 @@ function makeConcept(): Concept {
|
|
|
24
23
|
});
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
function makeConceptWithNonVerbal():
|
|
26
|
+
function makeConceptWithNonVerbal(): any {
|
|
28
27
|
return Concept.fromJSON({
|
|
29
28
|
id: '3.1.2',
|
|
30
29
|
uri: 'https://glossarist.org/test/concept/3.1.2',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderContent } from '../../utils/content-renderer';
|
|
3
|
+
|
|
4
|
+
describe('content-renderer XSS protection', () => {
|
|
5
|
+
it('blocks javascript: URLs in {{link:}} (no anchor tag)', () => {
|
|
6
|
+
const html = renderContent('{{link:javascript:alert(1)}}', {});
|
|
7
|
+
expect(html).not.toContain('<a');
|
|
8
|
+
expect(html).not.toContain('href=');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('blocks javascript: URLs in {{link:}} with label (renders as text)', () => {
|
|
12
|
+
const html = renderContent('{{link:javascript:alert(1), click me}}', {});
|
|
13
|
+
expect(html).not.toContain('<a');
|
|
14
|
+
expect(html).not.toContain('href=');
|
|
15
|
+
expect(html).toContain('click me');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('blocks data: URLs in {{link:}} (no anchor tag)', () => {
|
|
19
|
+
const html = renderContent('{{link:data:text/html,<script>alert(1)</script>}}', {});
|
|
20
|
+
expect(html).not.toContain('<a');
|
|
21
|
+
expect(html).not.toContain('href=');
|
|
22
|
+
expect(html).not.toContain('<script>');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('allows https: URLs in {{link:}}', () => {
|
|
26
|
+
const html = renderContent('{{link:https://example.com}}', {});
|
|
27
|
+
expect(html).toContain('href="https://example.com"');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('blocks javascript: URLs in {{image:}} (no img tag)', () => {
|
|
31
|
+
const html = renderContent('{{image:javascript:alert(1)}}', {});
|
|
32
|
+
expect(html).not.toContain('<img');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('blocks data: URLs in {{image:}} (no img tag with executable src)', () => {
|
|
36
|
+
const html = renderContent('{{image:data:text/html,<script>}}', {});
|
|
37
|
+
expect(html).not.toContain('<img');
|
|
38
|
+
expect(html).not.toContain('data:text/html');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('allows relative paths in {{image:}}', () => {
|
|
42
|
+
const html = renderContent('{{image:diagram.png}}', {});
|
|
43
|
+
expect(html).toContain('src="diagram.png"');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('escapes HTML in unordered list items', () => {
|
|
47
|
+
const html = renderContent('* <script>alert(1)</script>\n* safe item', {});
|
|
48
|
+
expect(html).not.toContain('<script>');
|
|
49
|
+
expect(html).toContain('<script>');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('escapes HTML in ordered list items', () => {
|
|
53
|
+
const html = renderContent('1. <img onerror=alert(1)>\n2. safe', {});
|
|
54
|
+
expect(html).not.toContain('<img onerror');
|
|
55
|
+
expect(html).toContain('<img');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -4,7 +4,7 @@ import { join, dirname } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const SCRIPT = join(__dirname, '..', '..', 'scripts', 'validate-shacl.
|
|
7
|
+
const SCRIPT = join(__dirname, '..', '..', 'scripts', 'validate-shacl.ts');
|
|
8
8
|
const FIXTURES = join(__dirname, '..', '__fixtures__', 'shacl');
|
|
9
9
|
const SHAPES = join(__dirname, '..', '__fixtures__', 'concept-shape.ttl');
|
|
10
10
|
|
|
@@ -31,13 +31,13 @@ function execCaught(cmd: string, args: string[], opts: Parameters<typeof execFil
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function runValidate(dataDir: string, { env, shapes }: RunOptions = {}): RunResult {
|
|
34
|
-
return execCaught('node', [SCRIPT, '--shapes', shapes ?? SHAPES, dataDir], {
|
|
34
|
+
return execCaught('node', ['--import', 'tsx', SCRIPT, '--shapes', shapes ?? SHAPES, dataDir], {
|
|
35
35
|
env: { ...process.env, ...env },
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function runValidateRaw(args: string[], { env }: RunOptions = {}): RunResult {
|
|
40
|
-
return execCaught('node', [SCRIPT, ...args], {
|
|
40
|
+
return execCaught('node', ['--import', 'tsx', SCRIPT, ...args], {
|
|
41
41
|
env: { ...process.env, ...env },
|
|
42
42
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
43
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-format types barrel — single import path for all wire types.
|
|
3
|
+
*
|
|
4
|
+
* Three MECE layers:
|
|
5
|
+
* - YAML author format (INPUT to build pipeline)
|
|
6
|
+
* - JSON-LD browser format (OUTPUT of build pipeline)
|
|
7
|
+
* - Manifest / site-config metadata
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import type { HarmonizedConcept, JsonLdConcept, Manifest } from '@/adapters/wire'
|
|
11
|
+
*
|
|
12
|
+
* Or layer-specific:
|
|
13
|
+
* import type { HarmonizedConcept } from '@/adapters/wire/yaml'
|
|
14
|
+
* import type { JsonLdConcept } from '@/adapters/wire/jsonld'
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// YAML author format — what dataset authors write
|
|
18
|
+
export type {
|
|
19
|
+
YamlContent,
|
|
20
|
+
YamlTerm,
|
|
21
|
+
YamlOrigin,
|
|
22
|
+
YamlGrammarInfo,
|
|
23
|
+
YamlSource,
|
|
24
|
+
YamlDate,
|
|
25
|
+
YamlRelation,
|
|
26
|
+
YamlPartitiveMember,
|
|
27
|
+
YamlPartitiveRelation,
|
|
28
|
+
YamlGenericRelation,
|
|
29
|
+
YamlLocalization,
|
|
30
|
+
HarmonizedConcept,
|
|
31
|
+
YamlManagedConceptDoc,
|
|
32
|
+
YamlManifestSection,
|
|
33
|
+
YamlManifest,
|
|
34
|
+
YamlBibliographyEntry,
|
|
35
|
+
YamlNewsFrontmatter,
|
|
36
|
+
YamlContentPage,
|
|
37
|
+
ShaclConstraint,
|
|
38
|
+
PrefixMap,
|
|
39
|
+
} from '../../../scripts/lib/yaml-types';
|
|
40
|
+
|
|
41
|
+
// JSON-LD browser format — what the SPA reads
|
|
42
|
+
export type {
|
|
43
|
+
JsonLdContent,
|
|
44
|
+
JsonLdDate,
|
|
45
|
+
JsonLdPronunciation,
|
|
46
|
+
JsonLdGrammarInfo,
|
|
47
|
+
JsonLdRef,
|
|
48
|
+
JsonLdLocality,
|
|
49
|
+
JsonLdOrigin,
|
|
50
|
+
JsonLdSource,
|
|
51
|
+
JsonLdRelated,
|
|
52
|
+
JsonLdDesignation,
|
|
53
|
+
JsonLdLocalizedConcept,
|
|
54
|
+
JsonLdPartitiveMember,
|
|
55
|
+
JsonLdPartitiveRelation,
|
|
56
|
+
JsonLdConcept,
|
|
57
|
+
} from '../jsonld-types';
|
|
58
|
+
|
|
59
|
+
// Manifest + summary types
|
|
60
|
+
export type {
|
|
61
|
+
Manifest,
|
|
62
|
+
ManifestSection,
|
|
63
|
+
ConceptSummary,
|
|
64
|
+
CitationClassification,
|
|
65
|
+
CiteResolution,
|
|
66
|
+
} from '../types';
|
|
67
|
+
|
|
68
|
+
// Wire-key constants (runtime value, not just type)
|
|
69
|
+
export { GL } from '../wire-keys';
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* The v-math directive upgrades the placeholders to Plurimath at runtime.
|
|
27
27
|
*/
|
|
28
28
|
import { escapeHtml, escapeAttr } from './escape';
|
|
29
|
+
import { sanitizeUrl, isSafeUrl } from './url-safety';
|
|
29
30
|
import { parseMention } from 'glossarist';
|
|
30
31
|
import type { NonVerbalKind } from '../adapters/non-verbal/types';
|
|
31
32
|
import { entityKindFromMentionKind } from '../adapters/non-verbal/kind';
|
|
@@ -135,7 +136,7 @@ function convertLists(text: string): string {
|
|
|
135
136
|
items.push(m[1].trim());
|
|
136
137
|
}
|
|
137
138
|
if (!items.length) return _;
|
|
138
|
-
const lis = items.map(item => `<li>${item}</li>`).join('');
|
|
139
|
+
const lis = items.map(item => `<li>${escapeHtml(item)}</li>`).join('');
|
|
139
140
|
return `\n<ul class="concept-list">${lis}</ul>`;
|
|
140
141
|
});
|
|
141
142
|
|
|
@@ -147,7 +148,7 @@ function convertLists(text: string): string {
|
|
|
147
148
|
items.push(m[1].trim());
|
|
148
149
|
}
|
|
149
150
|
if (!items.length) return _;
|
|
150
|
-
const lis = items.map(item => `<li>${item}</li>`).join('');
|
|
151
|
+
const lis = items.map(item => `<li>${escapeHtml(item)}</li>`).join('');
|
|
151
152
|
return `\n<ol class="concept-list concept-list-ordered">${lis}</ol>`;
|
|
152
153
|
});
|
|
153
154
|
|
|
@@ -225,7 +226,9 @@ function resolveMentions(text: string, opts: RenderOptions): string {
|
|
|
225
226
|
const url = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
|
|
226
227
|
const label = commaIdx > 0 ? rest.slice(commaIdx + 1).trim() : url;
|
|
227
228
|
if (opts.linkResolver) return opts.linkResolver(url, label);
|
|
228
|
-
|
|
229
|
+
const safeUrl = sanitizeUrl(url);
|
|
230
|
+
if (!safeUrl) return escapeHtml(label);
|
|
231
|
+
return `<a href="${escapeAttr(safeUrl)}" target="_blank" rel="noopener" class="ext-link">${escapeHtml(label)}</a>`;
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
const imageMatch = body.match(/^image:(.+)$/i);
|
|
@@ -235,6 +238,7 @@ function resolveMentions(text: string, opts: RenderOptions): string {
|
|
|
235
238
|
const src = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
|
|
236
239
|
const alt = commaIdx > 0 ? rest.slice(commaIdx + 1).trim() : '';
|
|
237
240
|
if (opts.imageResolver) return opts.imageResolver(src, alt);
|
|
241
|
+
if (!isSafeUrl(src)) return escapeHtml(alt || src);
|
|
238
242
|
return `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}" class="inline-image" />`;
|
|
239
243
|
}
|
|
240
244
|
|
package/src/utils/url-safety.ts
CHANGED
|
@@ -18,11 +18,7 @@ export function isSafeUrl(url: string): boolean {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return false;
|
|
21
|
+
return true;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
export function sanitizeUrl(url: string): string {
|
package/tsconfig.json
CHANGED
|
@@ -20,6 +20,6 @@
|
|
|
20
20
|
},
|
|
21
21
|
"baseUrl": "."
|
|
22
22
|
},
|
|
23
|
-
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "
|
|
24
|
-
"exclude": ["src/content.config.ts", "src/pages/**", "src/layouts/**", "src/astro-islands/**", "src/lib/site-config.ts"]
|
|
23
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "env.d.ts", ".astro/types.d.ts"],
|
|
24
|
+
"exclude": ["src/content.config.ts", "src/pages/**", "src/layouts/**", "src/astro-islands/**", "src/lib/site-config.ts", "src/__tests__/**", "scripts/**", "cli/**"]
|
|
25
25
|
}
|
package/vite.config.ts
CHANGED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck — TODO.typescript/12: remove after glossarist TS migration
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
// glossarist 0.4.33 renamed PartitiveRelation → PartitiveHyperedge.
|
|
4
|
-
// Import from glossarist/models (extended by our local augmentation).
|
|
5
|
-
import type {
|
|
6
|
-
PartitiveHyperedge,
|
|
7
|
-
PartitiveMember,
|
|
8
|
-
ConceptRef,
|
|
9
|
-
} from 'glossarist/models';
|
|
10
|
-
import {
|
|
11
|
-
PartitiveHyperedge as PartitiveHyperedgeModel,
|
|
12
|
-
PartitiveMember as PartitiveMemberModel,
|
|
13
|
-
ConceptRef as ConceptRefModel,
|
|
14
|
-
} from 'glossarist/models';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Verifies glossarist-js 0.4.26's MECE-native PartitiveRelation model
|
|
18
|
-
* (ISO 704:2022 presence × count + is_delimiting).
|
|
19
|
-
*
|
|
20
|
-
* The concept-browser consumes these fields directly (no migration
|
|
21
|
-
* bridge). These specs pin the runtime contract so a regression in
|
|
22
|
-
* upstream is caught here.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
function makeRef(source: string, id: string): ConceptRef {
|
|
26
|
-
return ConceptRefModel.fromJSON({ source, id }) as ConceptRef;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makePartitive(
|
|
30
|
-
source: string,
|
|
31
|
-
id: string,
|
|
32
|
-
fields: { presence?: 'required' | 'optional'; count?: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting?: boolean } = {},
|
|
33
|
-
): PartitiveMember {
|
|
34
|
-
const data: {
|
|
35
|
-
ref: ConceptRef;
|
|
36
|
-
presence?: 'required' | 'optional';
|
|
37
|
-
count?: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
38
|
-
is_delimiting?: boolean;
|
|
39
|
-
} = { ref: makeRef(source, id) };
|
|
40
|
-
if (fields.presence) data.presence = fields.presence;
|
|
41
|
-
if (fields.count) data.count = fields.count;
|
|
42
|
-
if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
|
|
43
|
-
return new (PartitiveMemberModel as unknown as new (data: unknown) => PartitiveMember)(data) as PartitiveMember;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makeRelation(opts: {
|
|
47
|
-
comprehensive: ConceptRef;
|
|
48
|
-
partitives: PartitiveMember[];
|
|
49
|
-
completeness?: 'complete' | 'partial';
|
|
50
|
-
criterion?: Record<string, string>;
|
|
51
|
-
}): PartitiveHyperedge {
|
|
52
|
-
return new (PartitiveHyperedgeModel as unknown as new (data: unknown) => PartitiveHyperedge)(opts) as PartitiveHyperedge;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
|
|
56
|
-
describe('PartitiveMember (ISO 704:2022 presence × count + delimiting)', () => {
|
|
57
|
-
it('presence defaults to required, count defaults to exactly_one', () => {
|
|
58
|
-
const m = makePartitive('test', '1.1');
|
|
59
|
-
expect(m.presence).toBe('required');
|
|
60
|
-
expect(m.presence === "required").toBe(true);
|
|
61
|
-
expect(m.presence === "optional").toBe(false);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('count defaults to exactly_one', () => {
|
|
65
|
-
const m = makePartitive('test', '1.1');
|
|
66
|
-
expect(m.count).toBe('exactly_one');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('presence=optional is preserved', () => {
|
|
70
|
-
const m = makePartitive('test', '1.1', { presence: 'optional' });
|
|
71
|
-
expect(m.presence).toBe('optional');
|
|
72
|
-
expect(m.presence === "optional").toBe(true);
|
|
73
|
-
expect(m.presence === "required").toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('count=multiple is preserved', () => {
|
|
77
|
-
const m = makePartitive('test', '1.1', { count: 'multiple' });
|
|
78
|
-
expect(m.count).toBe('multiple');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('count=at_least_one is preserved', () => {
|
|
82
|
-
const m = makePartitive('test', '1.1', { count: 'at_least_one' });
|
|
83
|
-
expect(m.count).toBe('at_least_one');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('is_delimiting defaults to false', () => {
|
|
87
|
-
const m = makePartitive('test', '1.1');
|
|
88
|
-
expect((m as any).is_delimiting).toBe(false);
|
|
89
|
-
expect((m as any).is_delimiting).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('is_delimiting is preserved when set to true', () => {
|
|
93
|
-
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
94
|
-
expect((m as any).is_delimiting).toBe(true);
|
|
95
|
-
expect((m as any).is_delimiting).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
99
|
-
expect(() =>
|
|
100
|
-
makePartitive('test', '1.1', { presence: 'optional', count: 'at_least_one' }),
|
|
101
|
-
).toThrow(/collapses to optional \+ multiple/);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('PartitiveRelation', () => {
|
|
106
|
-
it('accepts 2 members with default axes', () => {
|
|
107
|
-
const r = makeRelation({
|
|
108
|
-
comprehensive: makeRef('test', '1.3'),
|
|
109
|
-
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
110
|
-
});
|
|
111
|
-
expect((r as any).members).toHaveLength(2);
|
|
112
|
-
expect((r as any).completeness).toBe('complete');
|
|
113
|
-
expect((r as any).isComplete).toBe(true);
|
|
114
|
-
expect((r as any).isPartial).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('completeness=partial is preserved', () => {
|
|
118
|
-
const r = makeRelation({
|
|
119
|
-
comprehensive: makeRef('test', '1.3'),
|
|
120
|
-
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
121
|
-
completeness: 'partial',
|
|
122
|
-
});
|
|
123
|
-
expect((r as any).completeness).toBe('partial');
|
|
124
|
-
expect((r as any).isPartial).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('isCoordinate true when ≥2 partitives', () => {
|
|
128
|
-
const r = makeRelation({
|
|
129
|
-
comprehensive: makeRef('test', '1.3'),
|
|
130
|
-
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
131
|
-
});
|
|
132
|
-
expect((r as any).isCoordinate).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('rejects < 2 partitives (ISO 704 requirement)', () => {
|
|
136
|
-
expect(() => makeRelation({
|
|
137
|
-
comprehensive: makeRef('test', '1.3'),
|
|
138
|
-
partitives: [makePartitive('test', '1.4')],
|
|
139
|
-
})).toThrow(/≥2 partitives/);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('round-trips through toJSON with MECE fields', () => {
|
|
143
|
-
const r = makeRelation({
|
|
144
|
-
comprehensive: makeRef('test', '1.3'),
|
|
145
|
-
partitives: [
|
|
146
|
-
makePartitive('test', '1.4', { presence: 'required', count: 'multiple' }),
|
|
147
|
-
makePartitive('test', '1.5', { presence: 'optional', isDelimiting: true }),
|
|
148
|
-
],
|
|
149
|
-
completeness: 'partial',
|
|
150
|
-
criterion: { eng: 'physical structure' },
|
|
151
|
-
});
|
|
152
|
-
const json = r.toJSON() as {
|
|
153
|
-
comprehensive: unknown;
|
|
154
|
-
partitives: Array<{ presence?: string; count?: string; is_delimiting?: boolean; ref: unknown }>;
|
|
155
|
-
completeness: string;
|
|
156
|
-
criterion?: Record<string, string>;
|
|
157
|
-
};
|
|
158
|
-
expect(json.comprehensive).toBeDefined();
|
|
159
|
-
expect(json.partitives).toHaveLength(2);
|
|
160
|
-
expect(json.partitives[0].count).toBe('multiple');
|
|
161
|
-
expect(json.partitives[1].presence).toBe('optional');
|
|
162
|
-
expect(json.partitives[1].is_delimiting).toBe(true);
|
|
163
|
-
expect(json.completeness).toBe('partial');
|
|
164
|
-
expect(json.criterion).toEqual({ eng: 'physical structure' });
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
});
|