@peter.prib/renderengine 0.2.6 → 0.2.7

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.
Files changed (49) hide show
  1. package/README.md +4 -0
  2. package/REFERENCE.md +180 -2
  3. package/dist/renderengine-workspace.umd.js +8704 -5310
  4. package/dist/renderengine.umd.js +5675 -3398
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +223 -13
  7. package/package.json +1 -1
  8. package/src/bian.js +182 -0
  9. package/src/charting/DiagramEditor.js +5 -26
  10. package/src/charting/DiagramTypeRegistry.js +8 -2
  11. package/src/charting/ModelCheckDialog.js +63 -0
  12. package/src/charting/ModelChecker.js +26 -0
  13. package/src/charting/ViewTabStrip.js +94 -81
  14. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  15. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  16. package/src/charting/datamodel/DataModel.js +356 -10
  17. package/src/charting/datamodel/DataModelChecker.js +92 -0
  18. package/src/charting/datamodel/DataModelCompared.js +127 -0
  19. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  20. package/src/charting/datamodel/DataModelPerspective.js +45 -0
  21. package/src/charting/datamodel/DataType.js +108 -0
  22. package/src/charting/datamodel/EnumerationType.js +51 -0
  23. package/src/charting/datamodel/ServiceDomain.js +177 -0
  24. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  25. package/src/charting/datamodel/StructuredType.js +63 -0
  26. package/src/charting/datamodel/index.js +10 -0
  27. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  28. package/src/charting/diagramModel/OpenViewList.js +75 -0
  29. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  30. package/src/charting/diagrams/DataModelRelationshipStrategy.js +12 -4
  31. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  32. package/src/charting/diagrams/GraphDiagram.js +11 -1
  33. package/src/charting/diagrams/NodeGeometry.js +169 -158
  34. package/src/charting/glyphs/PersonGlyph.js +24 -23
  35. package/src/index.js +2666 -2664
  36. package/src/workspace/EditorShell.js +166 -67
  37. package/src/workspace/PanelLayout.js +56 -0
  38. package/src/workspace/layout/TabsNode.js +205 -191
  39. package/src/workspace/parts/ArchimateCanvasPart.js +5 -7
  40. package/src/workspace/parts/DataModelPart.js +339 -29
  41. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  42. package/src/workspace/parts/EditorPart.js +461 -407
  43. package/src/workspace/parts/ModelFile.js +216 -187
  44. package/src/workspace/parts/ModelsPart.js +483 -192
  45. package/src/workspace/storage/BackupFolder.js +145 -0
  46. package/src/workspace/storage/KeyValueStore.js +92 -0
  47. package/src/workspace/storage/ModelLibrary.js +65 -0
  48. package/src/workspace/workArea.js +417 -413
  49. package/src/yaml/YamlReader.js +372 -0
@@ -0,0 +1,185 @@
1
+ import { BianNotation } from '../../bian.js';
2
+ import { ServiceDomain } from './ServiceDomain.js';
3
+ import { parseArchiFile } from '../../archimateArchive.js';
4
+ import { parseExchangeModel } from '../../archimateExchangeFormat.js';
5
+ import { ZipArchive } from '../../zip/ZipArchive.js';
6
+
7
+ /** A stereotype as compared: no case, no spaces ('Service Domain' is 'servicedomain'). */
8
+ const plainWord = (text) => String(text ?? '').toLowerCase().replace(/[\s_-]+/g, '');
9
+
10
+ /**
11
+ * BianLandscapeReader — service domains from BIAN's published service
12
+ * landscape, an ArchiMate model (an Archi file or the Exchange Format, as
13
+ * parseArchiFile / parseExchangeModel give it), rather than retyped.
14
+ *
15
+ * BIAN's own model (its v14 landscape) marks each element with a stereotype:
16
+ * a business area is a Grouping marked BusinessArea, composed of business
17
+ * domains (Capabilities marked BusinessDomain), each composed of service
18
+ * domains (Capabilities marked ServiceDomain); other Groupings aggregate
19
+ * service domains as overlays (a value chain, a release wave). Where the file
20
+ * keeps the stereotypes (an Archi specialization, or a "Stereotype" property:
21
+ * `stereotypeKeys`), they say which element is which: a service domain's
22
+ * business domain is the container marked so, its area the one that holds that.
23
+ * Without them it goes by structure: a service domain is a service-domain-type
24
+ * element nothing of those types sits inside, its nearest container its
25
+ * business domain and the outermost its area.
26
+ *
27
+ * What contains what: a composition from the container to what it holds before
28
+ * an aggregation (an overlay's), and failing both, a box drawn inside another's
29
+ * in a view. A service domain's functional pattern is the one realizing it (an
30
+ * element named as a BIAN pattern, as BIAN's model has them), else a property
31
+ * (`patternKeys`); its asset a property (`assetKeys`). A pattern BIAN lacks is
32
+ * left for the model check to report.
33
+ */
34
+ export class BianLandscapeReader {
35
+ /**
36
+ * @param {Object} [options]
37
+ * @param {string[]} [options.serviceDomainTypes] - Element types a service domain may be
38
+ * @param {string[]} [options.containerTypes] - Element types a business area or domain may be
39
+ * @param {string[]} [options.containmentTypes] - Relationship types from a container to what it holds, the first preferred
40
+ * @param {{serviceDomain: string, businessDomain: string, businessArea: string}} [options.stereotypes] - BIAN's stereotypes for each
41
+ * @param {string[]} [options.stereotypeKeys] - Property keys a stereotype may be under
42
+ * @param {string[]} [options.patternKeys] - Property keys naming its functional pattern
43
+ * @param {string[]} [options.assetKeys] - Property keys naming the asset it manages
44
+ */
45
+ constructor({
46
+ serviceDomainTypes = ['Capability'],
47
+ containerTypes = ['Grouping', 'Capability'],
48
+ containmentTypes = ['CompositionRelationship', 'AggregationRelationship'],
49
+ stereotypes = { serviceDomain: 'ServiceDomain', businessDomain: 'BusinessDomain', businessArea: 'BusinessArea' },
50
+ stereotypeKeys = ['Stereotype', 'Stereotypes', 'stereotype', 'stereotypes'],
51
+ patternKeys = ['Functional Pattern', 'functional pattern', 'FunctionalPattern', 'pattern'],
52
+ assetKeys = ['Asset Type', 'asset type', 'Asset', 'asset'],
53
+ } = {}) {
54
+ Object.assign(this, { serviceDomainTypes, containerTypes, containmentTypes, stereotypes, stereotypeKeys, patternKeys, assetKeys });
55
+ }
56
+
57
+ /**
58
+ * An ArchiMate model from a file: an Archi file (XML or an archive) or the
59
+ * Exchange Format, as parseArchiFile / parseExchangeModel give it.
60
+ * @param {string|Uint8Array|ArrayBuffer} data
61
+ */
62
+ static parse(data) {
63
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
64
+ if (typeof bytes !== 'string' && ZipArchive.isZip(bytes)) return parseArchiFile(bytes);
65
+ const text = typeof bytes === 'string' ? bytes : new TextDecoder().decode(bytes);
66
+ return /opengroup\.org\/xsd\/archimate/.test(text.slice(0, 2000)) ? parseExchangeModel(text) : parseArchiFile(text);
67
+ }
68
+
69
+ /**
70
+ * Service domain specs (ServiceDomain's), one per service domain in the landscape.
71
+ * @param {{elements: Array, relationships?: Array, views?: Array}} parsed
72
+ * @returns {Array<Object>}
73
+ */
74
+ read(parsed) {
75
+ const elements = new Map((parsed.elements ?? []).map(e => [e.id, e]));
76
+ const containers = this.containers(parsed, elements);
77
+ const stereotypeOf = this._stereotypes(parsed);
78
+ const is = (id, which) => stereotypeOf(elements.get(id)).includes(plainWord(this.stereotypes[which]));
79
+ const name = (id) => elements.get(id)?.name ?? '';
80
+ const marked = [...elements.keys()].some(id => is(id, 'serviceDomain'));
81
+ let found;
82
+ if (marked) {
83
+ // Marked so, or (as some in BIAN's own model are not) a service-domain-type element in a business domain.
84
+ const isServiceDomain = (e) => is(e.id, 'serviceDomain') || (this.serviceDomainTypes.includes(e.type)
85
+ && !is(e.id, 'businessDomain') && !is(e.id, 'businessArea') && (containers.get(e.id) ?? []).some(c => is(c, 'businessDomain')));
86
+ found = [...elements.values()].filter(isServiceDomain).map(e => {
87
+ const domain = (containers.get(e.id) ?? []).find(c => is(c, 'businessDomain'));
88
+ const area = [...(containers.get(domain) ?? []), ...(containers.get(e.id) ?? [])].find(c => is(c, 'businessArea'));
89
+ return [e, area ? name(area) : '', domain ? name(domain) : ''];
90
+ });
91
+ } else {
92
+ const holdsAny = new Set([...containers.values()].flat());
93
+ found = [...elements.values()].filter(e => this.serviceDomainTypes.includes(e.type) && !holdsAny.has(e.id)).map(e => {
94
+ const chain = [];
95
+ for (let at = containers.get(e.id)?.[0]; at && !chain.includes(at); at = containers.get(at)?.[0]) chain.push(at);
96
+ return [e, chain.length ? name(chain[chain.length - 1]) : '', chain.length > 1 ? name(chain[0]) : ''];
97
+ });
98
+ }
99
+ // A model may hold copies of one service domain (BIAN's has some, for other diagrams): the placed one is it.
100
+ const placed = new Set(found.filter(([, , domain]) => domain).map(([e]) => plainWord(e.name)));
101
+ const seen = new Set();
102
+ found = found.filter(([e, , domain]) => {
103
+ const key = plainWord(e.name);
104
+ if (seen.has(key) || (!domain && placed.has(key))) return false;
105
+ if (domain || !placed.has(key)) seen.add(key);
106
+ return true;
107
+ });
108
+ const realizedBy = this._patternsRealizing(parsed, elements);
109
+ return found.map(([e, businessArea, businessDomain]) => {
110
+ const pattern = realizedBy.get(e.id) ?? this._property(e, this.patternKeys);
111
+ return {
112
+ kind: ServiceDomain.kind,
113
+ id: e.id,
114
+ name: e.name ?? '',
115
+ ...(e.documentation && { description: e.documentation }),
116
+ businessArea, businessDomain,
117
+ asset: this._property(e, this.assetKeys) ?? '',
118
+ pattern: BianNotation.findPattern(pattern)?.name ?? pattern ?? '',
119
+ };
120
+ });
121
+ }
122
+
123
+ /**
124
+ * What each element sits in (child id -> its containers, the likeliest
125
+ * first): by the containment relationships, a type's before the next's
126
+ * (composition before aggregation), then by being drawn inside a container
127
+ * in a view.
128
+ */
129
+ containers(parsed, elements = new Map((parsed.elements ?? []).map(e => [e.id, e]))) {
130
+ const isContainer = (id) => this.containerTypes.includes(elements.get(id)?.type);
131
+ const of = new Map();
132
+ const add = (child, container) => {
133
+ if (!elements.has(child) || child === container) return;
134
+ const list = of.get(child) ?? [];
135
+ if (!list.includes(container)) of.set(child, [...list, container]);
136
+ };
137
+ for (const type of this.containmentTypes) {
138
+ for (const r of parsed.relationships ?? []) if (r.type === type && isContainer(r.source)) add(r.target, r.source);
139
+ }
140
+ const walk = (visuals, container) => {
141
+ for (const v of visuals ?? []) {
142
+ const id = v.archimateElement;
143
+ if (id && container) add(id, container);
144
+ walk(v.children, id && isContainer(id) ? id : container);
145
+ }
146
+ };
147
+ for (const view of parsed.views ?? []) walk(view.children, null);
148
+ return of;
149
+ }
150
+
151
+ /** An element's stereotypes (compared plainly): its Archi specialization's name, and any stereotype property. @private */
152
+ _stereotypes(parsed) {
153
+ const profiles = new Map((parsed.archi?.elements ?? []).filter(e => e.tag === 'profile').map(p => [p.attributes?.id, p.attributes?.name]));
154
+ return (element) => {
155
+ if (!element) return [];
156
+ const profile = element.archi?.attributes?.profiles;
157
+ return [
158
+ ...(profile ? String(profile).split(/\s+/).map(id => profiles.get(id)).filter(Boolean) : []),
159
+ ...String(this._property(element, this.stereotypeKeys) ?? '').split(/[,;|]/),
160
+ ].map(plainWord).filter(Boolean);
161
+ };
162
+ }
163
+
164
+ /** Each service domain's pattern by what realizes it: an element named as a BIAN functional pattern. @private */
165
+ _patternsRealizing(parsed, elements) {
166
+ const patternOf = new Map();
167
+ for (const r of parsed.relationships ?? []) {
168
+ if (r.type !== 'RealizationRelationship' || patternOf.has(r.target)) continue;
169
+ const pattern = BianNotation.findPattern(elements.get(r.source)?.name);
170
+ if (pattern) patternOf.set(r.target, pattern.name);
171
+ }
172
+ return patternOf;
173
+ }
174
+
175
+ /** The first of some property keys an element has a value for. @private */
176
+ _property(element, keys) {
177
+ const props = element.properties ?? {};
178
+ const pairs = Array.isArray(props) ? props.map(p => [p.key, p.value]) : Object.entries(props);
179
+ for (const key of keys) {
180
+ const hit = pairs.find(([k, v]) => k === key && v);
181
+ if (hit) return String(hit[1]).trim();
182
+ }
183
+ return null;
184
+ }
185
+ }
@@ -0,0 +1,183 @@
1
+ import { BianNotation } from '../../bian.js';
2
+ import { YamlReader } from '../../yaml/YamlReader.js';
3
+ import { ServiceDomain } from './ServiceDomain.js';
4
+ import { EnumerationType } from './EnumerationType.js';
5
+ import { StructuredType } from './StructuredType.js';
6
+ import { DataType } from './DataType.js';
7
+
8
+ /** 'CurrentAccountFacility' → 'Current Account Facility'. */
9
+ const spaced = (name) => String(name).replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
10
+ /** 'Current Account Facility' → 'current-account-facility'. */
11
+ const idOf = (name) => spaced(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
12
+ /** A schema's name from a `$ref` ('#/components/schemas/Amount' → 'Amount'), of an item's for an array. */
13
+ const refName = (prop) => (prop?.$ref ?? prop?.items?.$ref)?.split('/').pop() ?? null;
14
+
15
+ /**
16
+ * BianSemanticApiReader — a service domain from one of BIAN's semantic APIs
17
+ * (an OpenAPI file, YAML or JSON, one per service domain, as BIAN publishes
18
+ * them: the plain ones and those extended with ISO 20022), rather than retyped.
19
+ *
20
+ * Its operations' tags name its control record (`CR - CurrentAccountFacility`)
21
+ * and behaviour qualifiers (`BQ - Interest`); each becomes a logical entity
22
+ * with the attributes its schema has (BIAN's names), the qualifiers joined to
23
+ * the control record. The schemas those attributes are become data types, as
24
+ * deep as `depth` (1: those the attributes name): an `enum` an EnumerationType
25
+ * (each value's meaning from BIAN's description), an object a StructuredType,
26
+ * any other a DataType. A schema named as a logical entity the model already
27
+ * has (`entityNames`) stays a reference to it. Where the ISO 20022 version
28
+ * names an attribute's ISO 20022 type, the attribute (and its data type) keep it.
29
+ *
30
+ * The service domain is named by the API's title, its pattern by the artifact
31
+ * its control record's name ends with (BianNotation.patternOfControlRecord),
32
+ * its asset by the rest of that name.
33
+ */
34
+ export class BianSemanticApiReader {
35
+ /**
36
+ * @param {Object} [options]
37
+ * @param {number} [options.depth] - How deep schemas become data types (0: none)
38
+ * @param {Iterable<string>} [options.entityNames] - Names of logical entities already there (any case)
39
+ */
40
+ constructor({ depth = 1, entityNames = [] } = {}) {
41
+ this.depth = depth;
42
+ this.entityNames = new Set([...entityNames].map(n => String(n).toLowerCase()));
43
+ }
44
+
45
+ /** A specification from a file's text: JSON, else YAML (YamlReader). */
46
+ static parse(text) {
47
+ const body = String(text).replace(/^/, '');
48
+ return /^\s*\{/.test(body) ? JSON.parse(body) : YamlReader.parse(body);
49
+ }
50
+
51
+ /**
52
+ * What a semantic API says, for DataModel#bringCommand.
53
+ * @param {Object} spec - An OpenAPI document (BianSemanticApiReader.parse)
54
+ * @returns {{ serviceDomains: Array<Object>, entities: Array<Object>, relationships: Array<Object>, dataTypes: Array<Object> }}
55
+ */
56
+ read(spec) {
57
+ const title = spec?.info?.title;
58
+ if (!title || !spec.paths) throw new Error('Not an OpenAPI specification (no info.title or paths)');
59
+ const schemas = spec.components?.schemas ?? {};
60
+ const { controlRecords, qualifiers } = BianSemanticApiReader.parts(spec);
61
+ // Some of BIAN's APIs have no control record operations (or none at all): what is there is read.
62
+ const [cr = null] = controlRecords;
63
+ const schemaOf = (name) => schemas[name] ?? ['Retrieve', 'Initiate', 'Update', 'Register', 'Create', 'Evaluate', 'Provide']
64
+ .map(verb => schemas[`${verb}${name}`]).find(Boolean) ?? null;
65
+
66
+ const used = new Map(); // schema name -> depth first reached
67
+ const attributesOf = (schema, depth) => Object.entries(schema?.properties ?? {}).map(([name, prop]) => {
68
+ const ref = refName(prop);
69
+ const iso20022 = BianSemanticApiReader.iso20022Of(prop.description);
70
+ if (ref && !used.has(ref)) used.set(ref, depth);
71
+ return {
72
+ name,
73
+ ...(ref ? { type: ref } : (prop.format || prop.type) && { type: prop.format || prop.type }),
74
+ ...(BianSemanticApiReader.cleanDescription(prop.description) && { description: BianSemanticApiReader.cleanDescription(prop.description) }),
75
+ ...(iso20022 && { iso20022 }),
76
+ };
77
+ });
78
+ const entity = (name) => ({
79
+ id: idOf(name), name: spaced(name),
80
+ ...(BianSemanticApiReader.cleanDescription(schemaOf(name)?.description) && { description: BianSemanticApiReader.cleanDescription(schemaOf(name)?.description) }),
81
+ attributes: attributesOf(schemaOf(name), 1),
82
+ });
83
+ const entities = [...(cr ? [cr] : []), ...qualifiers].map(entity);
84
+ const relationships = !cr ? [] : qualifiers.map(q => ({ id: `${idOf(cr)}-has-${idOf(q)}`, source: idOf(cr), target: idOf(q), name: 'has', sourceCardinality: '1', targetCardinality: '0..*' }));
85
+
86
+ // The schemas the attributes are, then theirs, as deep as asked: data types, unless entities already.
87
+ const dataTypes = [];
88
+ const isoOf = new Map();
89
+ for (const a of entities.flatMap(e => e.attributes)) if (a.iso20022 && a.type && !isoOf.has(a.type)) isoOf.set(a.type, a.iso20022);
90
+ const done = new Set();
91
+ for (let level = 1; level <= this.depth; level++) {
92
+ for (const [name, at] of [...used]) {
93
+ if (at !== level || done.has(name)) continue;
94
+ done.add(name);
95
+ if (this.entityNames.has(name.toLowerCase()) || this.entityNames.has(spaced(name).toLowerCase())) continue;
96
+ const schema = schemas[name];
97
+ if (!schema) continue;
98
+ const type = this.dataType(name, schema, level + 1, attributesOf);
99
+ if (isoOf.has(name) && !type.iso20022) type.iso20022 = isoOf.get(name);
100
+ dataTypes.push(type);
101
+ }
102
+ }
103
+
104
+ // Its pattern by its control record's artifact; with none, by its own name's ('Operations Log').
105
+ const crName = spaced(cr ?? title);
106
+ const pattern = BianNotation.patternOfControlRecord(crName);
107
+ const ending = pattern ? [pattern.artifact, ...pattern.formerArtifacts].find(a => crName.toLowerCase().endsWith(a.toLowerCase())) : null;
108
+ const serviceDomain = {
109
+ kind: ServiceDomain.kind,
110
+ id: `sd-${idOf(title)}`,
111
+ name: title,
112
+ description: BianSemanticApiReader.cleanDescription(spec.info.description),
113
+ asset: ending ? crName.slice(0, -ending.length).trim() : crName,
114
+ pattern: pattern?.name ?? '',
115
+ controlRecord: cr ? idOf(cr) : null,
116
+ qualifiers: qualifiers.map(q => ({ name: spaced(q), entity: idOf(q) })),
117
+ };
118
+ return { serviceDomains: [serviceDomain], entities, relationships, dataTypes };
119
+ }
120
+
121
+ /** A data type spec for a schema: an enumeration, a structure, or a plain type. */
122
+ dataType(name, schema, depth, attributesOf) {
123
+ const base = { id: `bian-${idOf(name)}`, name, ...(BianSemanticApiReader.enumerationMeanings(schema.description).size === 0 && BianSemanticApiReader.cleanDescription(schema.description) && { description: BianSemanticApiReader.cleanDescription(schema.description) }) };
124
+ if (Array.isArray(schema.enum)) {
125
+ const meanings = BianSemanticApiReader.enumerationMeanings(schema.description);
126
+ return { kind: EnumerationType.kind, ...base, values: schema.enum.map(v => ({ name: String(v), ...(meanings.get(String(v)) && { description: meanings.get(String(v)) }) })) };
127
+ }
128
+ if (schema.properties) {
129
+ return { kind: StructuredType.kind, ...base, fields: attributesOf(schema, depth) };
130
+ }
131
+ return { kind: DataType.kind, ...base, ...((schema.format || schema.type) && { basedOn: schema.format || schema.type }) };
132
+ }
133
+
134
+ /** The control records and behaviour qualifiers its operations' tags name, in order. */
135
+ static parts(spec) {
136
+ const controlRecords = [], qualifiers = [];
137
+ for (const path of Object.values(spec.paths ?? {})) {
138
+ for (const op of Object.values(path ?? {})) {
139
+ for (const tag of op?.tags ?? []) {
140
+ const m = /^(CR|BQ)\b[^-]*-\s*(\S+)\s*$/.exec(String(tag));
141
+ if (!m) continue;
142
+ const list = m[1] === 'CR' ? controlRecords : qualifiers;
143
+ if (!list.includes(m[2])) list.push(m[2]);
144
+ }
145
+ }
146
+ }
147
+ return { controlRecords, qualifiers };
148
+ }
149
+
150
+ /** The ISO 20022 type a description links to (`…/standardsrepository/type/CurrencyCode`), or null. */
151
+ static iso20022Of(description) {
152
+ return /iso20022\.org\/standardsrepository\/type\/([A-Za-z0-9_]+)/.exec(String(description ?? ''))?.[1] ?? null;
153
+ }
154
+
155
+ /** What an enumeration's description says each value means: lines "* `Value` - meaning". */
156
+ static enumerationMeanings(description) {
157
+ const meanings = new Map();
158
+ for (const m of String(description ?? '').matchAll(/\*\s*`([^`]+)`\s*-\s*([^\r\n]*)/g)) {
159
+ if (m[2].trim()) meanings.set(m[1], m[2].trim());
160
+ }
161
+ return meanings;
162
+ }
163
+
164
+ /**
165
+ * A description as said: without the ISO 20022 version's "(DDD Entity) - "
166
+ * and "BIAN Service Domain (…) | … |" prefixes, or its trailing
167
+ * "Type|Type ([link])|Standard|ISO20022" reference.
168
+ */
169
+ static cleanDescription(description) {
170
+ let text = String(description ?? '').replace(/\r/g, '').trim();
171
+ if (!text || text.startsWith('>')) return '';
172
+ text = text.replace(/^\(DDD [^)]*\)\s*-\s*/, '');
173
+ if (/^BIAN Service Domain/.test(text)) text = text.slice(text.lastIndexOf('|') + 1).trim();
174
+ const bar = text.indexOf('|');
175
+ if (bar >= 0) {
176
+ // "… instance CashAccountContract|CashAccountContract (link)|Standard|ISO20022": the type named twice goes too.
177
+ const repeated = text.slice(bar + 1).trim().split(/[\s(|]/)[0];
178
+ text = text.slice(0, bar).trim();
179
+ if (repeated && text.endsWith(repeated)) text = text.slice(0, -repeated.length).trim();
180
+ }
181
+ return text.replace(/\s+/g, ' ').trim();
182
+ }
183
+ }