@glossarist/concept-browser 0.7.95 → 0.7.97
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__/composables/live-19-pipeline.test.ts +112 -0
- package/src/__tests__/sphere-math.test.ts +25 -0
- package/src/adapters/model-bridge/jsonld-types.ts +2 -0
- package/src/adapters/model-bridge.ts +57 -0
- package/src/components/RelationSphere.vue +32 -4
- package/src/utils/sphere-math.ts +30 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.97",
|
|
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": {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import type { Router } from 'vue-router';
|
|
4
|
+
import { useConceptEdges } from '../../composables/use-concept-edges';
|
|
5
|
+
import { useVocabularyStore } from '../../stores/vocabulary';
|
|
6
|
+
import { conceptFromJson } from '../../adapters/model-bridge';
|
|
7
|
+
import type { Manifest, GraphEdge } from '../../adapters/types';
|
|
8
|
+
import { makeManifest, setupPinia } from '../test-helpers';
|
|
9
|
+
import { getFactory, resetFactory } from '../../adapters/factory';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* End-to-end pipeline test using the actual live JSON-LD shape from
|
|
13
|
+
* the deployed oimlsmart/vocab site (vim-2012 concept 1.9).
|
|
14
|
+
*
|
|
15
|
+
* Reproduces the live bug: 1.9 has gl:genericRelations but the sphere
|
|
16
|
+
* renders nothing because conceptGenericRelations is empty.
|
|
17
|
+
*/
|
|
18
|
+
describe('1.9 live pipeline — generic projection through model-bridge', () => {
|
|
19
|
+
const URN = 'urn:oiml:pub:v:2:2012';
|
|
20
|
+
const manifest: Manifest = makeManifest({
|
|
21
|
+
id: 'vim-2012',
|
|
22
|
+
datasetUri: URN,
|
|
23
|
+
uriBase: 'https://oimlsmart.github.io/vocab',
|
|
24
|
+
});
|
|
25
|
+
const noEdges: GraphEdge[] = [];
|
|
26
|
+
const noopRouter = { push() {} } as unknown as Router;
|
|
27
|
+
|
|
28
|
+
let store: ReturnType<typeof useVocabularyStore>;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
setupPinia();
|
|
32
|
+
resetFactory();
|
|
33
|
+
store = useVocabularyStore();
|
|
34
|
+
store.manifests.set(manifest.id, manifest);
|
|
35
|
+
|
|
36
|
+
const factory = getFactory();
|
|
37
|
+
factory.resolveRelatedRef = vi.fn((ref: { source: string | null; id: string | null } | null) =>
|
|
38
|
+
ref?.source === URN && ref.id
|
|
39
|
+
? { registerId: manifest.id, conceptId: ref.id }
|
|
40
|
+
: null,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('projects gl:genericRelations → conceptGenericRelations through conceptFromJson', () => {
|
|
45
|
+
const liveJson = {
|
|
46
|
+
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
47
|
+
'@id': 'https://oimlsmart.github.io/vocab/vim-2012/concept/1.9',
|
|
48
|
+
'@type': 'gl:Concept',
|
|
49
|
+
'gl:identifier': '1.9',
|
|
50
|
+
'gl:status': 'valid',
|
|
51
|
+
'gl:localizedConcept': {
|
|
52
|
+
eng: {
|
|
53
|
+
'@type': 'gl:LocalizedConcept',
|
|
54
|
+
'gl:languageCode': 'eng',
|
|
55
|
+
'gl:entryStatus': 'valid',
|
|
56
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'measurement unit' }],
|
|
57
|
+
'gl:definition': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'real scalar quantity' }],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
'gl:related': [
|
|
61
|
+
{ '@type': 'gl:RelatedConcept', 'gl:relationshipType': 'broader_generic',
|
|
62
|
+
'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.12' } },
|
|
63
|
+
],
|
|
64
|
+
'gl:genericRelations': [
|
|
65
|
+
{
|
|
66
|
+
'@type': 'gl:GenericRelation',
|
|
67
|
+
'gl:comprehensive': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.9' },
|
|
68
|
+
'gl:criterion': { eng: 'by magnitude relationship to a reference measurement unit' },
|
|
69
|
+
'gl:hasGeneric': [
|
|
70
|
+
{ '@type': 'gl:GenericMember',
|
|
71
|
+
'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.17' },
|
|
72
|
+
'gl:presence': 'required', 'gl:count': 'at_least_one',
|
|
73
|
+
'gl:delimitingCharacteristic': { eng: 'multiple of a unit' } },
|
|
74
|
+
{ '@type': 'gl:GenericMember',
|
|
75
|
+
'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.18' },
|
|
76
|
+
'gl:presence': 'required', 'gl:count': 'at_least_one',
|
|
77
|
+
'gl:delimitingCharacteristic': { eng: 'submultiple of a unit' } },
|
|
78
|
+
{ '@type': 'gl:GenericMember',
|
|
79
|
+
'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.12' },
|
|
80
|
+
'gl:presence': 'required', 'gl:count': 'at_least_one',
|
|
81
|
+
'gl:delimitingCharacteristic': { eng: 'coherent derived unit' } },
|
|
82
|
+
],
|
|
83
|
+
'gl:completeness': 'partial',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const concept = conceptFromJson(liveJson as any);
|
|
89
|
+
|
|
90
|
+
/* Sanity: model-bridge produced a Concept with relations. */
|
|
91
|
+
expect(concept).toBeTruthy();
|
|
92
|
+
|
|
93
|
+
/* Project through useConceptEdges, exactly as ConceptView does. */
|
|
94
|
+
const { conceptGenericRelations } = useConceptEdges(
|
|
95
|
+
computed(() => concept),
|
|
96
|
+
computed(() => 'vim-2012'),
|
|
97
|
+
computed(() => manifest),
|
|
98
|
+
computed(() => noEdges),
|
|
99
|
+
noopRouter,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const rels = conceptGenericRelations.value;
|
|
103
|
+
/* This is the live-bug assertion: must NOT be empty. */
|
|
104
|
+
expect(rels).toHaveLength(1);
|
|
105
|
+
expect(rels[0].members).toHaveLength(3);
|
|
106
|
+
expect(rels[0].members.map(m => m.uri)).toEqual([
|
|
107
|
+
'https://oimlsmart.github.io/vocab/vim-2012/concept/1.17',
|
|
108
|
+
'https://oimlsmart.github.io/vocab/vim-2012/concept/1.18',
|
|
109
|
+
'https://oimlsmart.github.io/vocab/vim-2012/concept/1.12',
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { hashSeed, expandParams, portSide, portPoint, idToUriGet, visibleNodeIds } from '../utils/sphere-math';
|
|
3
|
+
import type { SphereHyperedgeLike } from '../utils/sphere-math';
|
|
3
4
|
|
|
4
5
|
describe('sphere-math', () => {
|
|
5
6
|
describe('hashSeed', () => {
|
|
@@ -148,6 +149,30 @@ describe('sphere-math', () => {
|
|
|
148
149
|
expect(visible.has('a')).toBe(false);
|
|
149
150
|
});
|
|
150
151
|
|
|
152
|
+
it('hyperedge member with no bilateral edges stays visible while its bundle is not muted', () => {
|
|
153
|
+
const nodes = [focus, a, b];
|
|
154
|
+
const links: Array<{ source: string; target: string; type: string }> = [];
|
|
155
|
+
const hyperedges: SphereHyperedgeLike[] = [
|
|
156
|
+
{ comprehensive: 'focus', members: ['a', 'b'], muteKey: '__partitive__' },
|
|
157
|
+
];
|
|
158
|
+
const visible = visibleNodeIds(nodes, links, new Set(), new Set(), hyperedges);
|
|
159
|
+
expect(visible.has('focus')).toBe(true);
|
|
160
|
+
expect(visible.has('a')).toBe(true);
|
|
161
|
+
expect(visible.has('b')).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('hyperedge members disappear when the bundle is muted', () => {
|
|
165
|
+
const nodes = [focus, a, b];
|
|
166
|
+
const links: Array<{ source: string; target: string; type: string }> = [];
|
|
167
|
+
const hyperedges: SphereHyperedgeLike[] = [
|
|
168
|
+
{ comprehensive: 'focus', members: ['a', 'b'], muteKey: '__partitive__' },
|
|
169
|
+
];
|
|
170
|
+
const visible = visibleNodeIds(nodes, links, new Set(['__partitive__']), new Set(), hyperedges);
|
|
171
|
+
expect(visible.has('focus')).toBe(true);
|
|
172
|
+
expect(visible.has('a')).toBe(false);
|
|
173
|
+
expect(visible.has('b')).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
151
176
|
it('all-visible when no mutes are set', () => {
|
|
152
177
|
const nodes = [focus, a, b, c, d];
|
|
153
178
|
const links = buildLinks([
|
|
@@ -141,6 +141,8 @@ export interface JsonLdPartitiveMember {
|
|
|
141
141
|
'gl:count'?: string;
|
|
142
142
|
/** ISO 704:2022: delimiting part (3× stroke width in diagrams) */
|
|
143
143
|
'gl:isDelimiting'?: boolean;
|
|
144
|
+
/** ISO 704:2022 §5.5.4.2.1: GenericMember delimiting characteristic (required on generic members) */
|
|
145
|
+
'gl:delimitingCharacteristic'?: Record<string, string> | string;
|
|
144
146
|
/** Legacy v3 field — one-string multiplicity, split into presence+count */
|
|
145
147
|
'gl:multiplicity'?: string;
|
|
146
148
|
/** Legacy v2 field — migrated to presence+count */
|
|
@@ -516,6 +516,59 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
516
516
|
return out;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Map a GenericRelation JSON-LD entry to the dict shape that
|
|
521
|
+
* glossarist-js's GenericHyperedge constructor expects.
|
|
522
|
+
*
|
|
523
|
+
* Mirrors mapPartitiveRelationFromJsonLd but reads `gl:hasGeneric` and
|
|
524
|
+
* carries each member's `gl:delimitingCharacteristic` (ISO 704:2022
|
|
525
|
+
* §5.5.4.2.1 — required on every GenericMember).
|
|
526
|
+
*/
|
|
527
|
+
function mapGenericRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<string, unknown> | null {
|
|
528
|
+
const comprehensive = r[GL.COMPREHENSIVE] ? mapRefFromJsonLd(r[GL.COMPREHENSIVE]) : null;
|
|
529
|
+
if (!comprehensive) return null;
|
|
530
|
+
|
|
531
|
+
const members = (r[GL.HAS_GENERIC] ?? [])
|
|
532
|
+
.map((m): Record<string, unknown> | null => {
|
|
533
|
+
const ref = m[GL.REF] ? mapRefFromJsonLd(m[GL.REF]) : null;
|
|
534
|
+
if (!ref) return null;
|
|
535
|
+
const out: Record<string, unknown> = { ref };
|
|
536
|
+
const presence = m[GL.PRESENCE];
|
|
537
|
+
const count = m[GL.COUNT];
|
|
538
|
+
if (isPartitivePresence(presence) && isPartitiveCount(count)) {
|
|
539
|
+
out.presence = presence;
|
|
540
|
+
out.count = count;
|
|
541
|
+
} else {
|
|
542
|
+
const raw = m[GL.MULTIPLICITY] ?? splitLegacyCertainty(m[GL.CERTAINTY]);
|
|
543
|
+
if (raw) {
|
|
544
|
+
const parts = splitMultiplicity(raw);
|
|
545
|
+
out.presence = parts.presence;
|
|
546
|
+
out.count = parts.count;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const dc = m[GL.DELIMITING_CHARACTERISTIC];
|
|
550
|
+
if (dc) {
|
|
551
|
+
out.delimitingCharacteristic = typeof dc === 'string' ? { default: dc } : dc;
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
})
|
|
555
|
+
.filter((m): m is Record<string, unknown> => m !== null);
|
|
556
|
+
|
|
557
|
+
if (members.length < 2) return null;
|
|
558
|
+
|
|
559
|
+
const out: Record<string, unknown> = { comprehensive, members };
|
|
560
|
+
|
|
561
|
+
if (r[GL.COMPLETENESS]) out.completeness = r[GL.COMPLETENESS];
|
|
562
|
+
|
|
563
|
+
if (r[GL.CRITERION]) {
|
|
564
|
+
out.criterion = typeof r[GL.CRITERION] === 'string'
|
|
565
|
+
? { default: r[GL.CRITERION] }
|
|
566
|
+
: r[GL.CRITERION];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
|
|
519
572
|
/** Migrate legacy v2 certainty → old v3 multiplicity string. */
|
|
520
573
|
function splitLegacyCertainty(certainty: string | undefined): string | null {
|
|
521
574
|
if (!certainty) return null;
|
|
@@ -549,6 +602,9 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
|
|
|
549
602
|
const partitiveRelations = (doc[GL.PARTITIVE_RELATIONS] ?? [])
|
|
550
603
|
.map(mapPartitiveRelationFromJsonLd)
|
|
551
604
|
.filter((r): r is Record<string, unknown> => r !== null);
|
|
605
|
+
const genericRelations = (doc[GL.GENERIC_RELATIONS] ?? [])
|
|
606
|
+
.map(mapGenericRelationFromJsonLd)
|
|
607
|
+
.filter((r): r is Record<string, unknown> => r !== null);
|
|
552
608
|
const tagsArr = doc[GL.TAGS];
|
|
553
609
|
const tags = Array.isArray(tagsArr) ? [...tagsArr] : [];
|
|
554
610
|
|
|
@@ -559,6 +615,7 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
|
|
|
559
615
|
localizations,
|
|
560
616
|
related,
|
|
561
617
|
partitive_relations: partitiveRelations,
|
|
618
|
+
generic_relations: genericRelations,
|
|
562
619
|
tags,
|
|
563
620
|
figures: normalizeEntityRefs(doc[GL.FIGURE_REF]),
|
|
564
621
|
tables: normalizeEntityRefs(doc[GL.TABLE_REF]),
|
|
@@ -97,6 +97,13 @@ const mutedRegisters = ref<Set<string>>(new Set());
|
|
|
97
97
|
const panelOpen = ref(true);
|
|
98
98
|
const MAX_NODES = 36;
|
|
99
99
|
|
|
100
|
+
/** Convert a concept URI to the internal `${registerId}/${conceptId}` node ID. */
|
|
101
|
+
function uriToNodeId(uri: string): string {
|
|
102
|
+
const parsed = UriRouter.parseUri(uri);
|
|
103
|
+
if (parsed) return `${parsed.registerId}/${parsed.conceptId}`;
|
|
104
|
+
return uri;
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
/* Sphere-display language. Defaults to the i18n UI locale (which reads
|
|
101
108
|
localStorage) so e.g. a user returning in French mode sees French
|
|
102
109
|
terms by default. The user can override via the Language selector. */
|
|
@@ -735,8 +742,29 @@ function renderDOM() {
|
|
|
735
742
|
|
|
736
743
|
/* Cards with no visible edges (after applying type + register mutes)
|
|
737
744
|
float with no connections — hide them so toggling a relation type
|
|
738
|
-
cleans up orphaned cards. Focus node (depth 0) always renders.
|
|
739
|
-
|
|
745
|
+
cleans up orphaned cards. Focus node (depth 0) always renders.
|
|
746
|
+
Hyperedge members are always visible while their bundle is not
|
|
747
|
+
muted, even when they have no bilateral edges, so the rake has a
|
|
748
|
+
card to point at. */
|
|
749
|
+
const hyperedgesForVisibility = [
|
|
750
|
+
...(props.partitiveRelations ?? []).map(rel => ({
|
|
751
|
+
comprehensive: uriToNodeId(rel.comprehensive),
|
|
752
|
+
members: rel.partitives.map(m => uriToNodeId(m.uri)),
|
|
753
|
+
muteKey: '__partitive__',
|
|
754
|
+
})),
|
|
755
|
+
...(props.genericRelations ?? []).map(rel => ({
|
|
756
|
+
comprehensive: uriToNodeId(rel.comprehensive),
|
|
757
|
+
members: rel.members.map(m => uriToNodeId(m.uri)),
|
|
758
|
+
muteKey: '__generic__',
|
|
759
|
+
})),
|
|
760
|
+
];
|
|
761
|
+
const visibleIds = visibleNodeIds(
|
|
762
|
+
nodes,
|
|
763
|
+
links,
|
|
764
|
+
mutedTypes.value,
|
|
765
|
+
mutedRegisters.value,
|
|
766
|
+
hyperedgesForVisibility,
|
|
767
|
+
);
|
|
740
768
|
|
|
741
769
|
for (const n of nodes) {
|
|
742
770
|
if (!visibleIds.has(n.id)) continue;
|
|
@@ -1050,7 +1078,7 @@ const legendItems = computed(() => {
|
|
|
1050
1078
|
if (partitiveCount > 0) {
|
|
1051
1079
|
typeMap.set('__partitive__', {
|
|
1052
1080
|
type: '__partitive__',
|
|
1053
|
-
label: '
|
|
1081
|
+
label: 'Partitive Relation',
|
|
1054
1082
|
color: uiStore.isDark ? '#2dd4bf' : '#0d9488',
|
|
1055
1083
|
count: partitiveCount,
|
|
1056
1084
|
});
|
|
@@ -1060,7 +1088,7 @@ const legendItems = computed(() => {
|
|
|
1060
1088
|
if (genericCount > 0) {
|
|
1061
1089
|
typeMap.set('__generic__', {
|
|
1062
1090
|
type: '__generic__',
|
|
1063
|
-
label: '
|
|
1091
|
+
label: 'General Relation',
|
|
1064
1092
|
color: uiStore.isDark ? '#fbbf24' : '#b45309',
|
|
1065
1093
|
count: genericCount,
|
|
1066
1094
|
});
|
package/src/utils/sphere-math.ts
CHANGED
|
@@ -65,13 +65,27 @@ export interface SphereLinkLike {
|
|
|
65
65
|
type: string;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export interface SphereHyperedgeLike {
|
|
69
|
+
/** Node ID of the comprehensive concept. */
|
|
70
|
+
comprehensive: string;
|
|
71
|
+
/** Node IDs of the member concepts. */
|
|
72
|
+
members: readonly string[];
|
|
73
|
+
/** Mute key — when present in mutedTypes, the whole hyperedge is hidden. */
|
|
74
|
+
muteKey?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
/**
|
|
69
78
|
* IDs of nodes that should render given the current mute state.
|
|
70
79
|
*
|
|
71
80
|
* The focus node (depth 0) is always visible. Every other node must have
|
|
72
81
|
* at least one edge whose type is not muted AND whose other endpoint is
|
|
73
|
-
* not in a muted register —
|
|
74
|
-
*
|
|
82
|
+
* not in a muted register — OR be a member of a non-muted hyperedge —
|
|
83
|
+
* otherwise the card floats with no connections and should disappear
|
|
84
|
+
* when its relations are hidden.
|
|
85
|
+
*
|
|
86
|
+
* Hyperedge membership counts as a "visible edge" because rake bundles
|
|
87
|
+
* are the connection: a member with no bilateral edges still needs its
|
|
88
|
+
* card rendered so the rake has something to point at.
|
|
75
89
|
*
|
|
76
90
|
* Extracted as a pure function so the visibility contract can be tested
|
|
77
91
|
* without mounting the sphere component.
|
|
@@ -81,10 +95,19 @@ export function visibleNodeIds(
|
|
|
81
95
|
links: readonly SphereLinkLike[],
|
|
82
96
|
mutedTypes: ReadonlySet<string>,
|
|
83
97
|
mutedRegisters: ReadonlySet<string>,
|
|
98
|
+
hyperedges: readonly SphereHyperedgeLike[] = [],
|
|
84
99
|
): Set<string> {
|
|
85
100
|
const byId = new Map<string, SphereNodeLike>();
|
|
86
101
|
for (const n of nodes) byId.set(n.id, n);
|
|
87
102
|
|
|
103
|
+
/* Pre-compute node IDs that participate in a non-muted hyperedge. */
|
|
104
|
+
const hyperedgeNodeIds = new Set<string>();
|
|
105
|
+
for (const he of hyperedges) {
|
|
106
|
+
if (he.muteKey && mutedTypes.has(he.muteKey)) continue;
|
|
107
|
+
hyperedgeNodeIds.add(he.comprehensive);
|
|
108
|
+
for (const m of he.members) hyperedgeNodeIds.add(m);
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
const visible = new Set<string>();
|
|
89
112
|
for (const n of nodes) {
|
|
90
113
|
if (n.depth === 0) {
|
|
@@ -92,6 +115,10 @@ export function visibleNodeIds(
|
|
|
92
115
|
continue;
|
|
93
116
|
}
|
|
94
117
|
if (mutedRegisters.has(n.register)) continue;
|
|
118
|
+
if (hyperedgeNodeIds.has(n.id)) {
|
|
119
|
+
visible.add(n.id);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
95
122
|
for (const l of links) {
|
|
96
123
|
if (mutedTypes.has(l.type)) continue;
|
|
97
124
|
const otherId = l.source === n.id ? l.target : l.target === n.id ? l.source : null;
|
|
@@ -106,3 +133,4 @@ export function visibleNodeIds(
|
|
|
106
133
|
return visible;
|
|
107
134
|
}
|
|
108
135
|
|
|
136
|
+
|