@glossarist/concept-browser 0.7.133 → 0.7.134
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/package.json +1 -1
- package/src/__tests__/components/pipe-bundles.test.ts +114 -0
- package/src/__tests__/wire-keys.test.ts +1 -1
- package/src/adapters/jsonld-types.ts +2 -0
- package/src/adapters/model-bridge.ts +3 -1
- package/src/adapters/types.ts +8 -0
- package/src/adapters/wire-keys.ts +6 -0
- package/src/components/relation-sphere/pipe-bundles.ts +137 -5
- package/src/composables/use-concept-edges.ts +20 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.134",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -167,3 +167,117 @@ describe('drawGenericPipes — pipe-and-thread contract', () => {
|
|
|
167
167
|
expect(svg.querySelectorAll('line.pipe-seg').length).toBe(0);
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
describe('drawGenericPipes — external + ellipsis ghosts (concept-model PR #91)', () => {
|
|
172
|
+
const pos = new Map<string, { x: number; y: number }>([
|
|
173
|
+
['vim-2012/1.9', { x: 0, y: 0 }],
|
|
174
|
+
['vim-2012/1.15', { x: 100, y: -50 }],
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
function makeGhostRelation(): GenericRelationWire {
|
|
178
|
+
return {
|
|
179
|
+
source: 'https://example.org/vim-2012/concept/1.9',
|
|
180
|
+
comprehensive: 'https://example.org/vim-2012/concept/1.9',
|
|
181
|
+
completeness: 'complete',
|
|
182
|
+
criterion: { eng: 'by system membership' },
|
|
183
|
+
register: 'vim-2012',
|
|
184
|
+
members: [
|
|
185
|
+
{ uri: 'https://example.org/vim-2012/concept/1.15', presence: 'required', count: 'at_least_one',
|
|
186
|
+
delimitingCharacteristic: { eng: 'off-system measurement unit' } },
|
|
187
|
+
{ uri: 'urn:gloss:ext:in-system measurement unit', presence: 'optional', count: 'any',
|
|
188
|
+
external: true, text: 'in-system measurement unit',
|
|
189
|
+
delimitingCharacteristic: { eng: 'in-system measurement unit' } },
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function makeEllipsisRelation(): GenericRelationWire {
|
|
195
|
+
return {
|
|
196
|
+
source: 'https://example.org/vim-2012/concept/1.9',
|
|
197
|
+
comprehensive: 'https://example.org/vim-2012/concept/1.9',
|
|
198
|
+
completeness: 'partial',
|
|
199
|
+
criterion: { eng: 'by epistemic status' },
|
|
200
|
+
register: 'vim-2012',
|
|
201
|
+
members: [
|
|
202
|
+
{ uri: 'https://example.org/vim-2012/concept/1.15', presence: 'required', count: 'at_least_one',
|
|
203
|
+
delimitingCharacteristic: { eng: 'x' } },
|
|
204
|
+
{ uri: 'urn:gloss:ellipsis', presence: 'required', count: 'exactly_one', ellipsis: true },
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
it('renders an external member as a dashed pill with parenthesized text', () => {
|
|
210
|
+
const svg = makeSvg();
|
|
211
|
+
drawGenericPipes(svg, pos, {
|
|
212
|
+
isDark: false,
|
|
213
|
+
color: '#b45309',
|
|
214
|
+
isMuted: false,
|
|
215
|
+
relations: [makeGhostRelation()],
|
|
216
|
+
locale: 'eng',
|
|
217
|
+
});
|
|
218
|
+
const pill = svg.querySelector('rect.pipe-ghost-external');
|
|
219
|
+
expect(pill).toBeDefined();
|
|
220
|
+
expect(pill!.getAttribute('stroke-dasharray')).toBe('3 2');
|
|
221
|
+
const label = svg.querySelector('text.pipe-ghost-external-label');
|
|
222
|
+
expect(label).toBeDefined();
|
|
223
|
+
expect(label!.textContent).toBe('(in-system measurement unit)');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('renders an ellipsis member as a muted ... ghost', () => {
|
|
227
|
+
const svg = makeSvg();
|
|
228
|
+
drawGenericPipes(svg, pos, {
|
|
229
|
+
isDark: false,
|
|
230
|
+
color: '#b45309',
|
|
231
|
+
isMuted: false,
|
|
232
|
+
relations: [makeEllipsisRelation()],
|
|
233
|
+
locale: 'eng',
|
|
234
|
+
});
|
|
235
|
+
const ell = svg.querySelector('text.pipe-ghost-ellipsis');
|
|
236
|
+
expect(ell).toBeDefined();
|
|
237
|
+
expect(ell!.textContent).toBe('...');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('keeps a 1-resolved + 1-ghost rake alive (ghosts count as members)', () => {
|
|
241
|
+
const svg = makeSvg();
|
|
242
|
+
drawGenericPipes(svg, pos, {
|
|
243
|
+
isDark: false,
|
|
244
|
+
color: '#b45309',
|
|
245
|
+
isMuted: false,
|
|
246
|
+
relations: [makeGhostRelation()],
|
|
247
|
+
locale: 'eng',
|
|
248
|
+
});
|
|
249
|
+
/* 1 resolved member + 1 ghost = 2 layout members → 1 pipe + 2 threads. */
|
|
250
|
+
expect(svg.querySelectorAll('line.pipe-seg').length).toBe(3);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('draws ghost threads dashed so they read as non-node placeholders', () => {
|
|
254
|
+
const svg = makeSvg();
|
|
255
|
+
drawGenericPipes(svg, pos, {
|
|
256
|
+
isDark: false,
|
|
257
|
+
color: '#b45309',
|
|
258
|
+
isMuted: false,
|
|
259
|
+
relations: [makeEllipsisRelation()],
|
|
260
|
+
locale: 'eng',
|
|
261
|
+
});
|
|
262
|
+
const dashed = Array.from(svg.querySelectorAll('line.pipe-seg'))
|
|
263
|
+
.filter(l => l.getAttribute('stroke-dasharray') === '3 3');
|
|
264
|
+
expect(dashed.length).toBe(1);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('drops a rake whose members are ALL ghosts with no positioned anchor', () => {
|
|
268
|
+
const svg = makeSvg();
|
|
269
|
+
const emptyPos = new Map<string, { x: number; y: number }>([
|
|
270
|
+
['vim-2012/1.9', { x: 0, y: 0 }],
|
|
271
|
+
]);
|
|
272
|
+
drawGenericPipes(svg, emptyPos, {
|
|
273
|
+
isDark: false,
|
|
274
|
+
color: '#b45309',
|
|
275
|
+
isMuted: false,
|
|
276
|
+
relations: [makeGhostRelation()],
|
|
277
|
+
locale: 'eng',
|
|
278
|
+
});
|
|
279
|
+
/* 1.15 not in pos → zero positioned members → layout collapses onto
|
|
280
|
+
comp; ghosts have no anchor distinct from comp, so nothing draws. */
|
|
281
|
+
expect(svg.querySelectorAll('line.pipe-seg').length).toBe(0);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -60,6 +60,6 @@ describe('GL wire-key SSOT', () => {
|
|
|
60
60
|
// The point of this test: when adding a key, you remember to update
|
|
61
61
|
// the count; when removing, you notice the test fails. Catches
|
|
62
62
|
// accidental drift in either direction.
|
|
63
|
-
expect(Object.keys(GL).length).toBe(
|
|
63
|
+
expect(Object.keys(GL).length).toBe(93);
|
|
64
64
|
});
|
|
65
65
|
});
|
|
@@ -298,7 +298,9 @@ function mapRefFromJsonLd(rawRef: JsonLdRef | string | undefined): Record<string
|
|
|
298
298
|
refObj.id = rawRef[GL.LOCAL_ID] ?? rawRef.id;
|
|
299
299
|
refObj.version = rawRef[GL.VERSION] ?? rawRef.version;
|
|
300
300
|
if (rawRef[GL.TEXT]) refObj.text = rawRef[GL.TEXT];
|
|
301
|
-
|
|
301
|
+
if (rawRef[GL.EXTERNAL] === true) refObj.external = true;
|
|
302
|
+
if (rawRef[GL.ELLIPSIS] === true) refObj.ellipsis = true;
|
|
303
|
+
return (refObj.source ?? refObj.id ?? refObj.version ?? refObj.text ?? refObj.external ?? refObj.ellipsis) != null
|
|
302
304
|
? refObj : null;
|
|
303
305
|
}
|
|
304
306
|
|
package/src/adapters/types.ts
CHANGED
|
@@ -204,6 +204,14 @@ export interface GenericMemberWire {
|
|
|
204
204
|
* distinguishes this specific concept from its coordinate concepts.
|
|
205
205
|
* Required on every GenericMember. LocalizedString shape. */
|
|
206
206
|
delimitingCharacteristic: Record<string, string>;
|
|
207
|
+
/** Inline external concept (concept-model PR #91). No dataset node —
|
|
208
|
+
* rendered as a dashed `(text)` ghost, never resolved to a URI. */
|
|
209
|
+
external?: boolean;
|
|
210
|
+
/** Author-acknowledged gap (concept-model PR #91). Rendered as a
|
|
211
|
+
* muted `...` ghost. */
|
|
212
|
+
ellipsis?: boolean;
|
|
213
|
+
/** Display text for external members (the parenthetical label). */
|
|
214
|
+
text?: string;
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
export interface GraphNode {
|
|
@@ -108,6 +108,12 @@ export const GL = {
|
|
|
108
108
|
VERSION: 'gl:version',
|
|
109
109
|
TEXT: 'gl:text',
|
|
110
110
|
|
|
111
|
+
// ConceptRef external/ellipsis flags (concept-model PR #91).
|
|
112
|
+
// external = parenthetical (ISO 704:2022 §5.5.4.3.1); renders as `(text)`.
|
|
113
|
+
// ellipsis = author-acknowledged gap; renders as `...`.
|
|
114
|
+
EXTERNAL: 'gl:external',
|
|
115
|
+
ELLIPSIS: 'gl:ellipsis',
|
|
116
|
+
|
|
111
117
|
// Locally-scoped id within source/origin/ref (NOT JSON-LD's @id)
|
|
112
118
|
LOCAL_ID: 'gl:id',
|
|
113
119
|
|
|
@@ -80,7 +80,16 @@ export function drawGenericPipes(
|
|
|
80
80
|
if (!compPos) continue;
|
|
81
81
|
|
|
82
82
|
const memberPositions: PipeLayoutMember[] = [];
|
|
83
|
+
const ghostMembers: Array<{ member: GenericRelationWire['members'][number]; pos: NodePos }> = [];
|
|
83
84
|
for (const m of rel.members) {
|
|
85
|
+
/* Ghost members (external / ellipsis, concept-model PR #91) carry
|
|
86
|
+
synthetic urn:gloss:* URIs that never resolve to a node — they
|
|
87
|
+
get virtual positions on an arc beyond the middle node and are
|
|
88
|
+
drawn as dashed ghosts after the layout. */
|
|
89
|
+
if (m.ellipsis === true || m.external === true) {
|
|
90
|
+
ghostMembers.push({ member: m, pos: { x: 0, y: 0 } });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
84
93
|
const parsed = UriRouter.parseUri(m.uri);
|
|
85
94
|
if (!parsed) continue;
|
|
86
95
|
const nid = `${parsed.registerId}/${parsed.conceptId}`;
|
|
@@ -89,7 +98,31 @@ export function drawGenericPipes(
|
|
|
89
98
|
memberPositions.push({ pos: p, delimitingCharacteristic: m.delimitingCharacteristic });
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
/* Ghosts need a middle-node estimate to fan around. With ≥2
|
|
102
|
+
positioned members the layout gives it directly; with exactly 1
|
|
103
|
+
use the comp→member midpoint (computePipeLayout requires ≥2 and
|
|
104
|
+
would return null). Zero positioned members means the ghosts have
|
|
105
|
+
no anchor — drop the rake. */
|
|
106
|
+
if (memberPositions.length === 0 && ghostMembers.length > 0) continue;
|
|
107
|
+
let provisionalMiddle: NodePos | null = null;
|
|
108
|
+
if (memberPositions.length >= 2) {
|
|
109
|
+
const l = computePipeLayout(compPos, memberPositions);
|
|
110
|
+
provisionalMiddle = l ? l.middleNode : null;
|
|
111
|
+
} else if (memberPositions.length === 1) {
|
|
112
|
+
provisionalMiddle = {
|
|
113
|
+
x: (compPos.x + memberPositions[0].pos.x) / 2,
|
|
114
|
+
y: (compPos.y + memberPositions[0].pos.y) / 2,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (!provisionalMiddle) continue;
|
|
118
|
+
assignGhostPositions(provisionalMiddle, compPos, memberPositions, ghostMembers);
|
|
119
|
+
|
|
120
|
+
const allPositions = [
|
|
121
|
+
...memberPositions,
|
|
122
|
+
...ghostMembers.map(g => ({ pos: g.pos, delimitingCharacteristic: g.member.delimitingCharacteristic })),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const layout = computePipeLayout(compPos, allPositions);
|
|
93
126
|
if (!layout) continue;
|
|
94
127
|
|
|
95
128
|
/* 1. Thick parent pipe: comp → middle node. The characteristic
|
|
@@ -98,9 +131,25 @@ export function drawGenericPipes(
|
|
|
98
131
|
|
|
99
132
|
/* 2. Thin threads: middle → each member. No per-thread labels —
|
|
100
133
|
the characteristic is a single hyperedge-level field shared
|
|
101
|
-
by all members, rendered once on the pipe body.
|
|
102
|
-
|
|
134
|
+
by all members, rendered once on the pipe body.
|
|
135
|
+
Ghost members are excluded here (step 2b draws their dashed
|
|
136
|
+
threads separately) — layout.threads preserves the
|
|
137
|
+
allPositions order: positioned members first, ghosts last. */
|
|
138
|
+
layout.threads.forEach((thread, i) => {
|
|
139
|
+
if (i >= memberPositions.length) return;
|
|
103
140
|
drawSegment(svg, opts.color, thread.start, thread.end, THREAD_WIDTH);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/* 2b. Ghost members — external concepts and ellipsis gaps. Dashed
|
|
144
|
+
threads + dashed pills so they read as "present but not a
|
|
145
|
+
node of this dataset" (ISO 704:2022 §5.5.4.3.1). */
|
|
146
|
+
for (const ghost of ghostMembers) {
|
|
147
|
+
drawSegment(svg, opts.color, layout.middleNode, ghost.pos, THREAD_WIDTH, { dashed: true, opacity: 0.55 });
|
|
148
|
+
if (ghost.member.ellipsis === true) {
|
|
149
|
+
drawEllipsisGhost(svg, ghost.pos, opts.color);
|
|
150
|
+
} else {
|
|
151
|
+
drawExternalGhost(svg, ghost.pos, ghost.member.text ?? '', opts.color, opts.isDark);
|
|
152
|
+
}
|
|
104
153
|
}
|
|
105
154
|
|
|
106
155
|
/* 3. Square junction at the middle node (distinct from the rake's
|
|
@@ -129,7 +178,7 @@ export function drawGenericPipes(
|
|
|
129
178
|
}
|
|
130
179
|
}
|
|
131
180
|
|
|
132
|
-
function drawSegment(svg: SVGSVGElement, color: string, from: NodePos, to: NodePos, width: number): void {
|
|
181
|
+
function drawSegment(svg: SVGSVGElement, color: string, from: NodePos, to: NodePos, width: number, style?: { dashed?: boolean; opacity?: number }): void {
|
|
133
182
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
134
183
|
line.setAttribute('class', 'pipe-seg');
|
|
135
184
|
line.setAttribute('x1', String(from.x));
|
|
@@ -139,10 +188,93 @@ function drawSegment(svg: SVGSVGElement, color: string, from: NodePos, to: NodeP
|
|
|
139
188
|
line.setAttribute('stroke', color);
|
|
140
189
|
line.setAttribute('stroke-width', String(width));
|
|
141
190
|
line.setAttribute('stroke-linecap', 'round');
|
|
142
|
-
line.setAttribute('opacity',
|
|
191
|
+
line.setAttribute('opacity', String(style?.opacity ?? 0.9));
|
|
192
|
+
if (style?.dashed) line.setAttribute('stroke-dasharray', '3 3');
|
|
143
193
|
svg.appendChild(line);
|
|
144
194
|
}
|
|
145
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Place ghost members (external / ellipsis) on an arc beyond the middle
|
|
198
|
+
* node, on the side away from the comprehensive, fanned to match the
|
|
199
|
+
* spread of the positioned members. Pure 2D — ghosts never enter the
|
|
200
|
+
* 3D force layout.
|
|
201
|
+
*/
|
|
202
|
+
function assignGhostPositions(
|
|
203
|
+
middleNode: NodePos,
|
|
204
|
+
compPos: NodePos,
|
|
205
|
+
positioned: readonly { pos: NodePos }[],
|
|
206
|
+
ghosts: Array<{ pos: NodePos }>,
|
|
207
|
+
): void {
|
|
208
|
+
if (ghosts.length === 0) return;
|
|
209
|
+
/* Fan radius: a bit beyond the average positioned-member distance
|
|
210
|
+
from the middle node so ghosts sit outside the resolved set. */
|
|
211
|
+
const avgDist = positioned.length > 0
|
|
212
|
+
? positioned.reduce((s, m) => s + Math.hypot(m.pos.x - middleNode.x, m.pos.y - middleNode.y), 0) / positioned.length
|
|
213
|
+
: 40;
|
|
214
|
+
const radius = Math.max(avgDist, 40);
|
|
215
|
+
/* Base direction: away from the comprehensive. */
|
|
216
|
+
const awayX = middleNode.x - compPos.x;
|
|
217
|
+
const awayY = middleNode.y - compPos.y;
|
|
218
|
+
const awayLen = Math.hypot(awayX, awayY) || 1;
|
|
219
|
+
const baseAngle = Math.atan2(awayY / awayLen, awayX / awayLen);
|
|
220
|
+
const spread = 0.5; /* radians between consecutive ghosts */
|
|
221
|
+
ghosts.forEach((g, i) => {
|
|
222
|
+
const offset = ghosts.length === 1 ? 0 : (i - (ghosts.length - 1) / 2) * spread;
|
|
223
|
+
const a = baseAngle + offset;
|
|
224
|
+
g.pos.x = middleNode.x + Math.cos(a) * radius;
|
|
225
|
+
g.pos.y = middleNode.y + Math.sin(a) * radius;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Ellipsis ghost: a muted "..." — the author knew more members exist. */
|
|
230
|
+
function drawEllipsisGhost(svg: SVGSVGElement, at: NodePos, color: string): void {
|
|
231
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
232
|
+
text.setAttribute('class', 'pipe-ghost-ellipsis');
|
|
233
|
+
text.setAttribute('x', String(at.x));
|
|
234
|
+
text.setAttribute('y', String(at.y + 3));
|
|
235
|
+
text.setAttribute('text-anchor', 'middle');
|
|
236
|
+
text.setAttribute('fill', color);
|
|
237
|
+
text.setAttribute('font-size', '11');
|
|
238
|
+
text.setAttribute('font-family', 'JetBrains Mono, monospace');
|
|
239
|
+
text.setAttribute('font-weight', '600');
|
|
240
|
+
text.setAttribute('opacity', '0.8');
|
|
241
|
+
text.textContent = '...';
|
|
242
|
+
svg.appendChild(text);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** External ghost: dashed-border pill with the parenthetical label. */
|
|
246
|
+
function drawExternalGhost(svg: SVGSVGElement, at: NodePos, label: string, color: string, isDark: boolean): void {
|
|
247
|
+
const fontSize = 9;
|
|
248
|
+
const text = label ? `(${label})` : '(external)';
|
|
249
|
+
const w = Math.max(text.length * (fontSize * 0.55) + 10, 34);
|
|
250
|
+
const h = 15;
|
|
251
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
252
|
+
rect.setAttribute('class', 'pipe-ghost-external');
|
|
253
|
+
rect.setAttribute('x', String(at.x - w / 2));
|
|
254
|
+
rect.setAttribute('y', String(at.y - h / 2));
|
|
255
|
+
rect.setAttribute('width', String(w));
|
|
256
|
+
rect.setAttribute('height', String(h));
|
|
257
|
+
rect.setAttribute('rx', '4');
|
|
258
|
+
rect.setAttribute('fill', isDark ? 'rgba(26,18,8,0.85)' : 'rgba(255,255,255,0.92)');
|
|
259
|
+
rect.setAttribute('stroke', color);
|
|
260
|
+
rect.setAttribute('stroke-width', '1');
|
|
261
|
+
rect.setAttribute('stroke-dasharray', '3 2');
|
|
262
|
+
rect.setAttribute('opacity', '0.92');
|
|
263
|
+
svg.appendChild(rect);
|
|
264
|
+
|
|
265
|
+
const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
266
|
+
t.setAttribute('class', 'pipe-ghost-external-label');
|
|
267
|
+
t.setAttribute('x', String(at.x));
|
|
268
|
+
t.setAttribute('y', String(at.y + 3));
|
|
269
|
+
t.setAttribute('text-anchor', 'middle');
|
|
270
|
+
t.setAttribute('fill', color);
|
|
271
|
+
t.setAttribute('font-size', String(fontSize));
|
|
272
|
+
t.setAttribute('font-family', 'JetBrains Mono, monospace');
|
|
273
|
+
t.setAttribute('font-style', 'italic');
|
|
274
|
+
t.textContent = text;
|
|
275
|
+
svg.appendChild(t);
|
|
276
|
+
}
|
|
277
|
+
|
|
146
278
|
function drawSquare(svg: SVGSVGElement, cx: number, cy: number, half: number, fill: string, stroke: string): void {
|
|
147
279
|
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
148
280
|
rect.setAttribute('class', 'pipe-junction');
|
|
@@ -190,7 +190,26 @@ export function useConceptEdges(
|
|
|
190
190
|
const isGeneric = rel instanceof GenericHyperedge;
|
|
191
191
|
const members: any[] = (rel?.members ?? rel?.partitives ?? [])
|
|
192
192
|
.map((m: any): any | null => {
|
|
193
|
-
const
|
|
193
|
+
const ref = m?.ref;
|
|
194
|
+
/* Ghost members (concept-model PR #91): external parenthetical
|
|
195
|
+
concepts and ellipsis gaps have no dataset node, so they get a
|
|
196
|
+
synthetic urn: URI plus flags. Renderers draw them as ghosts
|
|
197
|
+
without resolving the URI. */
|
|
198
|
+
if (ref?.ellipsis === true) {
|
|
199
|
+
return { uri: 'urn:gloss:ellipsis', presence: m?.presence, count: m?.count, ellipsis: true };
|
|
200
|
+
}
|
|
201
|
+
if (ref?.external === true || ref?.isExternal === true) {
|
|
202
|
+
const text = ref?.text ?? '';
|
|
203
|
+
return {
|
|
204
|
+
uri: `urn:gloss:ext:${text}`,
|
|
205
|
+
presence: m?.presence,
|
|
206
|
+
count: m?.count,
|
|
207
|
+
external: true,
|
|
208
|
+
text,
|
|
209
|
+
...(isGeneric ? { delimitingCharacteristic: m?.delimitingCharacteristic } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const uri = resolveConceptRefUri(ref);
|
|
194
213
|
if (!uri || uri === source) return null;
|
|
195
214
|
const base: any = {
|
|
196
215
|
uri,
|