dsh-model-everything 0.0.0-stage → 0.1.0

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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.en-US.md +319 -0
  3. package/README.md +319 -3
  4. package/cordis.patch.yml +20 -0
  5. package/examples/README.md +48 -0
  6. package/examples/control-dispatcher.json +113 -0
  7. package/examples/control-dispatcher.overbudget.json +113 -0
  8. package/examples/ecommerce-data-model.json +185 -0
  9. package/examples/order-state-machine.after.json +116 -0
  10. package/examples/order-state-machine.before.json +101 -0
  11. package/examples/user-migration.after.json +29 -0
  12. package/examples/user-migration.before.json +29 -0
  13. package/gate/locale/en.json +6 -0
  14. package/gate/locale/zh.json +6 -0
  15. package/gate/package.json +5 -0
  16. package/lib/baseline.js +249 -0
  17. package/lib/compose-tool.js +76 -0
  18. package/lib/concurrency-tool.js +40 -0
  19. package/lib/concurrency.js +206 -0
  20. package/lib/data-engine.js +1141 -0
  21. package/lib/data-tool.js +79 -0
  22. package/lib/design-engine.js +1149 -0
  23. package/lib/design-tool.js +55 -0
  24. package/lib/engine.js +4343 -0
  25. package/lib/export-tool.js +78 -0
  26. package/lib/exporters.js +434 -0
  27. package/lib/gate.js +274 -0
  28. package/lib/granularity.js +237 -0
  29. package/lib/index.js +237 -0
  30. package/lib/json-value.js +18 -0
  31. package/lib/option-shape.js +62 -0
  32. package/lib/report-diff-tool.js +63 -0
  33. package/lib/report-view.js +408 -0
  34. package/lib/structure-tool.js +91 -0
  35. package/lib/structure.js +711 -0
  36. package/lib/tool.js +85 -0
  37. package/lib/types/baseline.d.ts +65 -0
  38. package/lib/types/compose-tool.d.ts +7 -0
  39. package/lib/types/concurrency-tool.d.ts +8 -0
  40. package/lib/types/concurrency.d.ts +37 -0
  41. package/lib/types/data-engine.d.ts +211 -0
  42. package/lib/types/data-tool.d.ts +10 -0
  43. package/lib/types/design-engine.d.ts +215 -0
  44. package/lib/types/design-tool.d.ts +11 -0
  45. package/lib/types/engine.d.ts +494 -0
  46. package/lib/types/export-tool.d.ts +7 -0
  47. package/lib/types/exporters.d.ts +37 -0
  48. package/lib/types/gate.d.ts +91 -0
  49. package/lib/types/granularity.d.ts +106 -0
  50. package/lib/types/index.d.ts +45 -0
  51. package/lib/types/json-value.d.ts +21 -0
  52. package/lib/types/option-shape.d.ts +22 -0
  53. package/lib/types/report-diff-tool.d.ts +8 -0
  54. package/lib/types/report-view.d.ts +43 -0
  55. package/lib/types/structure-tool.d.ts +10 -0
  56. package/lib/types/structure.d.ts +152 -0
  57. package/lib/types/tool.d.ts +11 -0
  58. package/lib/types/uml-communication.d.ts +109 -0
  59. package/lib/types/uml-composite.d.ts +107 -0
  60. package/lib/types/uml-interaction-overview.d.ts +91 -0
  61. package/lib/types/uml-profile.d.ts +112 -0
  62. package/lib/types/uml-structure.d.ts +53 -0
  63. package/lib/types/uml-syntax.d.ts +296 -0
  64. package/lib/types/uml-timing.d.ts +152 -0
  65. package/lib/types/uml-tool.d.ts +12 -0
  66. package/lib/types/uml-usecase.d.ts +58 -0
  67. package/lib/types/uml.d.ts +303 -0
  68. package/lib/uml-communication.js +353 -0
  69. package/lib/uml-composite.js +442 -0
  70. package/lib/uml-interaction-overview.js +277 -0
  71. package/lib/uml-profile.js +373 -0
  72. package/lib/uml-structure.js +306 -0
  73. package/lib/uml-syntax.js +545 -0
  74. package/lib/uml-timing.js +974 -0
  75. package/lib/uml-tool.js +213 -0
  76. package/lib/uml-usecase.js +532 -0
  77. package/lib/uml.js +2110 -0
  78. package/locale/en.json +6 -0
  79. package/locale/zh.json +6 -0
  80. package/package.json +124 -6
  81. package/skills/model-everything/SKILL.md +482 -0
  82. package/skills/model-everything/references/dsh-model-schema.md +437 -0
  83. package/skills/model-everything/references/gap-routing-guide.md +42 -0
  84. package/skills/model-everything/references/hash-spec.md +152 -0
  85. package/skills/model-everything/references/logic-verification-guide.md +422 -0
  86. package/skills/model-everything-datamodel/SKILL.md +130 -0
  87. package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
  88. package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
  89. package/skills/model-everything-design/SKILL.md +115 -0
  90. package/skills/model-everything-interleaving/SKILL.md +63 -0
  91. package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
  92. package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
  93. package/skills/model-everything-report-reading/SKILL.md +137 -0
  94. package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
  95. package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
  96. package/skills/model-everything-structure/SKILL.md +96 -0
  97. package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
  98. package/skills/model-everything-uml/SKILL.md +60 -0
  99. package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
  100. package/src/baseline.ts +288 -0
  101. package/src/compose-tool.ts +78 -0
  102. package/src/concurrency-tool.ts +44 -0
  103. package/src/concurrency.ts +254 -0
  104. package/src/data-engine.ts +1208 -0
  105. package/src/data-tool.ts +83 -0
  106. package/src/design-engine.ts +1263 -0
  107. package/src/design-tool.ts +60 -0
  108. package/src/engine.ts +4564 -0
  109. package/src/export-tool.ts +81 -0
  110. package/src/exporters.ts +399 -0
  111. package/src/gate.ts +330 -0
  112. package/src/granularity.ts +312 -0
  113. package/src/index.ts +289 -0
  114. package/src/json-value.ts +20 -0
  115. package/src/option-shape.ts +64 -0
  116. package/src/report-diff-tool.ts +67 -0
  117. package/src/report-view.ts +451 -0
  118. package/src/structure-tool.ts +93 -0
  119. package/src/structure.ts +786 -0
  120. package/src/tool.ts +94 -0
  121. package/src/uml-communication.ts +428 -0
  122. package/src/uml-composite.ts +520 -0
  123. package/src/uml-interaction-overview.ts +321 -0
  124. package/src/uml-profile.ts +439 -0
  125. package/src/uml-structure.ts +317 -0
  126. package/src/uml-syntax.ts +764 -0
  127. package/src/uml-timing.ts +1106 -0
  128. package/src/uml-tool.ts +230 -0
  129. package/src/uml-usecase.ts +566 -0
  130. package/src/uml.ts +2139 -0
@@ -0,0 +1,764 @@
1
+
2
+ /**
3
+ * The notation syntax engine.
4
+ *
5
+ * One place knows what Mermaid and PlantUML can spell, which UML 2.5 diagram type a
6
+ * piece of text is, and whether a requested (notation, family, action) cell exists at
7
+ * all. Family modules know what to do with the elements this engine finds.
8
+ *
9
+ * Before this module, detection fell through: any PlantUML text that was not a
10
+ * sequence diagram went to the state parser. A use case diagram drawn with
11
+ * `:Actor: --> (UseCase)` came back as a state machine with no finding, which is
12
+ * the false guarantee this plugin exists to prevent. Detection is now total. A text
13
+ * either names a family this engine knows, or it is refused by name.
14
+ *
15
+ * What lives here:
16
+ * - the notation table: comment prefixes, headers, chrome and edge operators;
17
+ * - the family table: all fourteen UML 2.5 diagram types, their group, and what each
18
+ * notation can express for them;
19
+ * - the capability decision: (notation, family, action) -> supported | refusal,
20
+ * separate from whether this build implements the family (that is the registry's
21
+ * job, in `uml.ts`);
22
+ * - notation-agnostic lexical primitives: directives, line scanning, edge splitting,
23
+ * identifier and label rules.
24
+ *
25
+ * What deliberately does not live here: family semantics. A family module turns the
26
+ * elements this engine finds into its own model, renders that model back, and reviews
27
+ * it. `uml-usecase.ts` is the first such module. The machine families (state,
28
+ * activity) and the trace family (sequence) predate the module contract and keep their
29
+ * own path in `uml.ts`.
30
+ *
31
+ * @module model-everything-uml-syntax
32
+ */
33
+
34
+ /**
35
+ * A UML front-end refusal. `code` lets the tool report *why* it refused instead
36
+ * of collapsing every refusal into a generic input error.
37
+ */
38
+ export class UmlError extends Error {
39
+ readonly code: string
40
+
41
+ constructor(message: string, code = 'UML_INPUT') {
42
+ super(message)
43
+ this.code = code
44
+ }
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Notations
49
+ // ---------------------------------------------------------------------------
50
+
51
+ export type UmlNotation = 'mermaid' | 'plantuml'
52
+
53
+ export const UML_NOTATIONS: readonly UmlNotation[] = ['mermaid', 'plantuml']
54
+
55
+ export function isUmlNotation(value: unknown): value is UmlNotation {
56
+ return typeof value === 'string' && (UML_NOTATIONS as readonly string[]).includes(value)
57
+ }
58
+
59
+ /** Mermaid node ids that cannot stand alone; the renderer aliases around them. */
60
+ export const MERMAID_RESERVED_NODE_IDS: ReadonlySet<string> = new Set(['end', 'graph', 'subgraph', 'class', 'classDef', 'click', 'style', 'linkStyle', 'direction'])
61
+
62
+ /** Ids a notation can spell without an alias. */
63
+ export const UML_PLAIN_ID = /^[A-Za-z_][A-Za-z0-9_]*$/
64
+
65
+ /** Characters that would break a generated label, whichever notation is used. */
66
+ export const UML_UNSAFE_LABEL = /[[\]/\n\r\t]/
67
+
68
+ /** The comment prefix each notation uses for its dsh-model-everything: directives. */
69
+ export function commentPrefix(notation: UmlNotation): string {
70
+ return notation === 'mermaid' ? '%%' : "'"
71
+ }
72
+
73
+ export function escapeRegExp(text: string): string {
74
+ return text.replace(/[.*+?^${{}()|[\]\\]/g, '\\$&')
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Families: the fourteen UML 2.5 diagram types
79
+ // ---------------------------------------------------------------------------
80
+
81
+ export type UmlFamilyId =
82
+ | 'state'
83
+ | 'activity'
84
+ | 'sequence'
85
+ | 'use-case'
86
+ | 'class'
87
+ | 'object'
88
+ | 'package'
89
+ | 'component'
90
+ | 'composite-structure'
91
+ | 'deployment'
92
+ | 'profile'
93
+ | 'communication'
94
+ | 'interaction-overview'
95
+ | 'timing'
96
+
97
+ /**
98
+ * How faithfully a notation expresses one UML diagram type.
99
+ *
100
+ * - `faithful`: the notation models this diagram type; a parse reconstructs it.
101
+ * - `approximate`: the notation can draw something in this area, but reading it
102
+ * back would produce a model of a *different* diagram. Rendering is allowed with a
103
+ * warning; parsing is refused.
104
+ * - `none`: the notation has no view of this diagram type at all.
105
+ */
106
+ export type UmlFidelity = 'faithful' | 'approximate' | 'none'
107
+
108
+ export interface UmlNotationSupport {
109
+ fidelity: UmlFidelity
110
+ /** Header keywords that declare the family outright (Mermaid: `classDiagram`). */
111
+ headers?: readonly string[]
112
+ /** Declaration keywords the family owns (PlantUML: `actor`, `usecase`). */
113
+ declarations?: readonly string[]
114
+ /** Why the fidelity is what it is, in the words the refusal uses. */
115
+ note?: string
116
+ }
117
+
118
+ export interface UmlFamilySpec {
119
+ id: UmlFamilyId
120
+ /** English name of the UML 2.5 diagram type. */
121
+ name: string
122
+ /** Chinese name, for the bilingual documentation. */
123
+ nameZh: string
124
+ group: 'structure' | 'behaviour' | 'interaction'
125
+ /**
126
+ * True when a faithful parse of this family is a LogicModelV1 the verification
127
+ * engine can run. False for every structure family and for the trace families.
128
+ */
129
+ machine: boolean
130
+ /**
131
+ * False when a parse could not reconstruct the diagram even in a faithful notation.
132
+ * A sequence diagram is a trace: it shows the paths that were walked, not the
133
+ * branches that were not.
134
+ */
135
+ parseable: boolean
136
+ /**
137
+ * Whether this build can read the family back out of a notation that only approximates
138
+ * it. Absent means no.
139
+ *
140
+ * This is not derivable from the notation's fidelity. `fidelity` says how much of the
141
+ * specification the drawing carries; this says whether what the model *does* carry
142
+ * survives a round trip, which is a measurement, not a label. A composite structure is
143
+ * drawn approximately and reads back exactly - nesting, port direction and both
144
+ * connector ends are all spelled - while a profile is drawn approximately and cannot be
145
+ * read back at all. Refusing the first because the second is impossible would leave a
146
+ * working view switched off.
147
+ */
148
+ parseThroughApproximation?: boolean
149
+ /** Why a parse is refused, when `parseable` is false. */
150
+ parseRefusal?: string
151
+ /** Code the parse refusal carries; defaults to `UML_NOT_A_STATE_DIAGRAM`. */
152
+ parseRefusalCode?: string
153
+ mermaid?: UmlNotationSupport
154
+ plantuml?: UmlNotationSupport
155
+ }
156
+
157
+ const PLANTUML_ACTIVITY_NOTE = 'plantuml has no faithful activity view here (its activity syntax is a structured flowchart language; a graph with merges or cycles needs a while/if reconstruction this plugin does not perform) — use notation "mermaid" for the activity view, or diagram "state"'
158
+
159
+ const SEQUENCE_PARSE_REFUSAL = 'a sequence diagram is a trace, not a machine: parsing it would drop every branch the trace did not walk. Render diagram "state" or "activity" and parse that instead.'
160
+
161
+ /**
162
+ * The family table. Order matters twice: a Mermaid header is matched in this order,
163
+ * and a PlantUML declaration keyword is too. The machine families come first because
164
+ * their headers are the most specific.
165
+ */
166
+ export const UML_FAMILIES: readonly UmlFamilySpec[] = [
167
+ {
168
+ id: 'state', name: 'state machine diagram', nameZh: '状态机图', group: 'behaviour', machine: true, parseable: true,
169
+ mermaid: { fidelity: 'faithful', headers: ['stateDiagram-v2', 'stateDiagram'] },
170
+ plantuml: { fidelity: 'faithful', declarations: ['state'] },
171
+ },
172
+ {
173
+ id: 'activity', name: 'activity diagram', nameZh: '活动图', group: 'behaviour', machine: true, parseable: true,
174
+ mermaid: { fidelity: 'faithful', headers: ['flowchart', 'graph'] },
175
+ plantuml: {
176
+ // The syntax exists, the faithful view does not: a structured flowchart cannot
177
+ // carry a graph with merges or cycles, so this cell is refused both ways.
178
+ fidelity: 'none',
179
+ declarations: ['start', 'stop', 'end', 'if', 'else', 'elseif', 'endif', 'while', 'endwhile', 'repeat', 'repeatwhile', 'fork', 'split', 'partition'],
180
+ note: PLANTUML_ACTIVITY_NOTE,
181
+ },
182
+ },
183
+ {
184
+ id: 'sequence', name: 'sequence diagram', nameZh: '顺序图', group: 'interaction', machine: false, parseable: false,
185
+ parseRefusal: SEQUENCE_PARSE_REFUSAL, parseRefusalCode: 'UML_NOT_A_STATE_DIAGRAM',
186
+ mermaid: { fidelity: 'faithful', headers: ['sequenceDiagram'] },
187
+ plantuml: { fidelity: 'faithful', declarations: ['participant', 'boundary', 'control', 'entity', 'database', 'collections', 'queue'] },
188
+ },
189
+ {
190
+ id: 'use-case', name: 'use case diagram', nameZh: '用例图', group: 'behaviour', machine: false, parseable: true,
191
+ plantuml: { fidelity: 'faithful', declarations: ['usecase', 'actor'] },
192
+ mermaid: {
193
+ fidelity: 'approximate',
194
+ headers: ['usecase-beta'],
195
+ note: 'mermaid draws a use case diagram with usecase-beta (Mermaid 12+): actors, a system boundary, associations, include and extend. This build renders that view; reading it back is not implemented yet, so a parse stays in plantuml. Render it with notation "plantuml" for the round trip.',
196
+ },
197
+ },
198
+ {
199
+ id: 'class', name: 'class diagram', nameZh: '类图', group: 'structure', machine: false, parseable: true,
200
+ mermaid: { fidelity: 'faithful', headers: ['classDiagram'] },
201
+ plantuml: { fidelity: 'faithful', declarations: ['class', 'interface', 'enum', 'enumeration', 'abstract', 'annotation', 'struct', 'protocol', 'exception', 'metaclass', 'stereotype'] },
202
+ },
203
+ {
204
+ id: 'object', name: 'object diagram', nameZh: '对象图', group: 'structure', machine: false, parseable: true,
205
+ plantuml: { fidelity: 'faithful', declarations: ['object'] },
206
+ mermaid: { fidelity: 'none', note: 'mermaid has no object diagram: classDiagram draws classifiers, not instances. Render it with notation "plantuml".' },
207
+ },
208
+ {
209
+ id: 'package', name: 'package diagram', nameZh: '包图', group: 'structure', machine: false, parseable: true,
210
+ plantuml: { fidelity: 'faithful', declarations: ['package', 'namespace', 'folder', 'frame', 'module'] },
211
+ mermaid: { fidelity: 'none', note: 'mermaid has no package diagram. Render it with notation "plantuml", or review the same text as a dependency graph with uml_structure_verify.' },
212
+ },
213
+ {
214
+ id: 'component', name: 'component diagram', nameZh: '组件图', group: 'structure', machine: false, parseable: true,
215
+ plantuml: { fidelity: 'faithful', declarations: ['component', 'componentDiagram', 'artifact', 'boundary', 'control', 'entity', 'agent', 'circle', 'hexagon', 'label'] },
216
+ mermaid: { fidelity: 'none', note: 'mermaid architecture-beta draws a cloud architecture, not UML components: the shapes and the ports do not line up, and this build does not emit it under the component name. Render it with notation "plantuml".' },
217
+ },
218
+ {
219
+ id: 'composite-structure', name: 'composite structure diagram', nameZh: '复合结构图', group: 'structure', machine: false, parseable: true, parseThroughApproximation: true,
220
+ plantuml: { fidelity: 'approximate', declarations: ['port', 'portin', 'portout'], note: 'plantuml documents no composite structure diagram; this build draws one with the deployment diagram\'s vocabulary - a nested element for the classifier, the nestable elements for its parts, `port`/`portin`/`portout` for its boundary, and the arrows for its connectors. Nesting, port direction and both connector ends are spelled, so the text reads back; a part\'s type, a port\'s interfaces, a connector\'s name and a collaboration are not expressible and do not enter the model.' },
221
+ mermaid: { fidelity: 'none', note: 'mermaid has no composite structure diagram. Render it with notation "plantuml".' },
222
+ },
223
+ {
224
+ id: 'deployment', name: 'deployment diagram', nameZh: '部署图', group: 'structure', machine: false, parseable: true,
225
+ plantuml: { fidelity: 'faithful', declarations: ['node', 'deployment', 'database', 'storage', 'cloud', 'stack'] },
226
+ mermaid: { fidelity: 'none', note: 'mermaid architecture-beta draws a service topology, not UML deployment nodes and artifacts, and this build does not emit it under the deployment name. Render it with notation "plantuml".' },
227
+ },
228
+ {
229
+ id: 'profile', name: 'profile diagram', nameZh: 'Profile 图', group: 'structure', machine: false, parseable: false,
230
+ parseRefusal: 'plantuml draws a profile with the class diagram\'s elements and has no profile element, and an extension and a stereotype generalization are both a --|> arrow: a parse would have to guess which fact the arrow carries. Keep the profile as its own model and review that.',
231
+ plantuml: { fidelity: 'approximate', declarations: ['stereotype'], note: 'plantuml has metaclass and stereotype elements and draws `class X <<stereotype>>`, but no profile element and no ProfileApplication: the profile package itself is not drawn, and an extension is drawn as the same --|> arrow as a generalization. This build draws that approximation and never reads it back.' },
232
+ mermaid: { fidelity: 'none', note: 'mermaid has no profile diagram: classDiagram draws classifiers and carries <<annotation>> markers as labels, with no metaclass, stereotype or extension element. Render it with notation "plantuml".' },
233
+ },
234
+ {
235
+ id: 'communication', name: 'communication diagram', nameZh: '通信图', group: 'interaction', machine: false, parseable: true, parseThroughApproximation: true,
236
+ plantuml: { fidelity: 'approximate', declarations: ['object'], note: 'plantuml has no communication diagram type - the page is a 404 and the navigation lists none - but this build draws one with the object diagram\'s vocabulary: objects for the lifelines, arrows for the links, and the sequence number in the arrow\'s label, `C --> O : 1: create`. Lifelines, links and the numbers read back; the nesting a dotted number like `1.1` suggests is a claim about the design, so the model keeps the number as written and says nothing about the tree.' },
237
+ mermaid: { fidelity: 'none', note: 'mermaid has no communication diagram: nothing documents lifelines-plus-links with numbered messages. Model the same interaction as a sequence diagram, or render it with notation "plantuml".' },
238
+ },
239
+ {
240
+ id: 'interaction-overview', name: 'interaction overview diagram', nameZh: '交互概览图', group: 'interaction', machine: false, parseable: false,
241
+ parseRefusal: 'an interaction overview is drawn here as a mermaid flowchart whose nodes name interactions, and a flowchart node carries a name rather than the interaction it stands for: reading one back would have to guess whether a node is a reference to another diagram, an inline interaction, or a plain step. Keep the overview as its own model and review that.',
242
+ mermaid: { fidelity: 'approximate', declarations: ['flowchart'], note: 'mermaid has no interaction overview diagram, but this build draws one as a flowchart whose nodes name interactions and whose edges carry the branch conditions - the flow, the references and the conditions are all carried. The combined fragments of an inline interaction are not drawn: the drawing has one node for it.' },
243
+ plantuml: { fidelity: 'none', note: 'plantuml has no interaction overview page, and the one place a `ref` step exists - its activity syntax - is refused by this build, because a structured flowchart language cannot carry a graph with merges or cycles. Render it with notation "mermaid".' },
244
+ },
245
+ {
246
+ id: 'timing', name: 'timing diagram', nameZh: '定时图', group: 'interaction', machine: false, parseable: true,
247
+ plantuml: { fidelity: 'faithful', declarations: ['robust', 'concise', 'binary', 'clock'] },
248
+ mermaid: { fidelity: 'none', note: 'mermaid has no timing diagram. Render it with notation "plantuml", or model the deadline as a LogicModelV1 with maxTicks and tickEvents and let model_verify check it.' },
249
+ },
250
+ ]
251
+
252
+ export const UML_FAMILY_IDS: readonly UmlFamilyId[] = UML_FAMILIES.map((family) => family.id)
253
+
254
+ export function isUmlFamilyId(value: unknown): value is UmlFamilyId {
255
+ return typeof value === 'string' && (UML_FAMILY_IDS as readonly string[]).includes(value)
256
+ }
257
+
258
+ export function umlFamilySpec(id: UmlFamilyId): UmlFamilySpec {
259
+ const spec = UML_FAMILIES.find((family) => family.id === id)
260
+ if (spec === undefined) throw new UmlError('unknown diagram family "' + String(id) + '"; expected ' + UML_FAMILY_IDS.join(' | '))
261
+ return spec
262
+ }
263
+
264
+ /** What one notation can do with one family. `none` when the table says nothing. */
265
+ export function umlNotationSupport(notation: UmlNotation, family: UmlFamilyId): UmlNotationSupport {
266
+ const spec = umlFamilySpec(family)
267
+ const support = notation === 'mermaid' ? spec.mermaid : spec.plantuml
268
+ return support ?? {
269
+ fidelity: 'none',
270
+ note: notation + ' has no ' + spec.name + ': there is no view to render and no text to read back.',
271
+ }
272
+ }
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // Mermaid families that are not UML at all
276
+ // ---------------------------------------------------------------------------
277
+
278
+ /**
279
+ * Mermaid headers that name something other than a UML diagram type. Recognizing them
280
+ * matters: the text must not be reported as "cannot tell whether this is Mermaid or
281
+ * PlantUML", and it must not be parsed as a state diagram, whose every line would be
282
+ * ignored or misread. ER, gantt, mindmap and the C4 family are not UML 2.5 diagrams.
283
+ */
284
+ export const FOREIGN_MERMAID_FAMILIES: readonly string[] = [
285
+ 'erDiagram', 'gantt', 'pie', 'journey', 'mindmap', 'gitGraph',
286
+ 'C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment',
287
+ 'requirementDiagram', 'timeline', 'quadrantChart', 'sankey-beta', 'block-beta',
288
+ 'packet-beta', 'radar-beta', 'treemap-beta', 'xychart-beta',
289
+ ]
290
+
291
+ /** The non-UML Mermaid family this text declares, if any. */
292
+ export function foreignMermaidFamily(text: string): string | undefined {
293
+ for (const family of FOREIGN_MERMAID_FAMILIES) {
294
+ if (new RegExp('^\\s*' + escapeRegExp(family) + '\\b', 'm').test(text)) return family
295
+ }
296
+ return undefined
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Detection
301
+ // ---------------------------------------------------------------------------
302
+
303
+ /** Every Mermaid header this engine knows, for the "expected one of" message. */
304
+ export function mermaidHeaderList(): string {
305
+ const headers: string[] = []
306
+ for (const spec of UML_FAMILIES) for (const header of spec.mermaid?.headers ?? []) headers.push('`' + header + '`')
307
+ for (const family of FOREIGN_MERMAID_FAMILIES) headers.push('`' + family + '`')
308
+ return headers.join(' / ')
309
+ }
310
+
311
+ export function detectUmlNotation(text: string): UmlNotation {
312
+ if (/^\s*@start/m.test(text)) return 'plantuml'
313
+ for (const spec of UML_FAMILIES) {
314
+ for (const header of spec.mermaid?.headers ?? []) {
315
+ if (new RegExp('^\\s*' + escapeRegExp(header) + '\\b', 'm').test(text)) return 'mermaid'
316
+ }
317
+ }
318
+ // A known Mermaid family is still Mermaid: say so, and let family detection refuse
319
+ // it by name instead of blaming the notation.
320
+ if (foreignMermaidFamily(text) !== undefined) return 'mermaid'
321
+ throw new UmlError('cannot tell whether this is Mermaid or PlantUML text: expected ' + mermaidHeaderList() + ', or `@startuml`')
322
+ }
323
+
324
+ export interface UmlFamilyDetection {
325
+ family: UmlFamilyId
326
+ /** The header or keyword that decided it, for the report. */
327
+ evidence: string
328
+ }
329
+
330
+ /** A Mermaid header: the most specific evidence there is. */
331
+ function mermaidHeader(text: string): UmlFamilyDetection | undefined {
332
+ for (const spec of UML_FAMILIES) {
333
+ for (const header of spec.mermaid?.headers ?? []) {
334
+ if (new RegExp('^\\s*' + escapeRegExp(header) + '\\b', 'm').test(text)) return { family: spec.id, evidence: header }
335
+ }
336
+ }
337
+ return undefined
338
+ }
339
+
340
+ function plantumlDeclaration(text: string, family: UmlFamilyId): string | undefined {
341
+ for (const keyword of umlFamilySpec(family).plantuml?.declarations ?? []) {
342
+ if (new RegExp('^\\s*' + escapeRegExp(keyword) + '\\b', 'im').test(text)) return keyword
343
+ }
344
+ return undefined
345
+ }
346
+
347
+ /**
348
+ * Use case syntax that carries no keyword: `:Actor:`, `(UseCase)`, and either of them
349
+ * with a relation on the same line (`:User: --> (Login)`). The node reference has to be
350
+ * recognized inside the relation line too, or the most common hand-written form would
351
+ * fall through to the state parser - which is the defect this engine exists to fix.
352
+ */
353
+ const PLANTUML_ACTOR_REF = /^\s*:[^:\n]+:(?:\s+as\s+[A-Za-z_][A-Za-z0-9_]*)?\s*(?:$|<\|--|--\|>|\.\.>|\.>|-->|->|--)/
354
+ const PLANTUML_USE_CASE_REF = /^\s*\([^()\n]+\)(?:\s+as\s+\(?[^()\n]+\)?)?\s*(?:$|<\|--|--\|>|\.\.>|\.>|-->|->|--)/
355
+
356
+ export function plantumlUseCaseSyntax(text: string): boolean {
357
+ return text.split(/\r?\n/).some((line) => PLANTUML_ACTOR_REF.test(line) || PLANTUML_USE_CASE_REF.test(line))
358
+ }
359
+
360
+ /** PlantUML timing syntax: a declared timeline, or a tick. */
361
+ const PLANTUML_TIMING_LINE = /^\s*(?:@\d+|\d+\s+is\s+|(?:robust|concise|binary|clock)\s+)/
362
+
363
+ export function plantumlTimingSyntax(text: string): boolean {
364
+ return text.split(/\r?\n/).some((line) => PLANTUML_TIMING_LINE.test(line))
365
+ }
366
+
367
+ /** Profile markers: a profile is drawn as a class diagram carrying these stereotypes. */
368
+ const PLANTUML_PROFILE_MARKER = /<<\s*(stereotype|metaclass)\s*>>/i
369
+
370
+ /**
371
+ * Composite structure syntax: a port declaration.
372
+ *
373
+ * `port`, `portin` and `portout` exist nowhere else in PlantUML, so one of them inside a
374
+ * block says what the diagram is. Without this the text is read as a component diagram -
375
+ * the classifier is a component and the ports become components beside it - which is a
376
+ * different model that happens to parse.
377
+ */
378
+ const PLANTUML_COMPOSITE_PORT = /^\s*(?:port|portin|portout)\s+[A-Za-z_][A-Za-z0-9_]*\s*$/im
379
+
380
+ export function plantumlCompositeSyntax(text: string): boolean {
381
+ return PLANTUML_COMPOSITE_PORT.test(text)
382
+ }
383
+
384
+ /**
385
+ * ER-diagram syntax: a crow's-foot cardinality marker on a link.
386
+ *
387
+ * An entity-relationship diagram is not a UML 2.5 diagram family, so this plugin does not
388
+ * model one. It has to be *refused* rather than read, because PlantUML's ER vocabulary
389
+ * overlaps the class diagram's: `USER ||--o{ ORDER` - a cardinality, not a node - becomes
390
+ * two components called `USER ||` and `o{ ORDER`, and the result is a graph that looks
391
+ * reviewed while saying nothing about the schema.
392
+ *
393
+ * The marker is the only signal that is not ambiguous. A raised `entity X { ... }` block
394
+ * is *not*: `entity` is a class-diagram keyword too, its members are spelled the same way,
395
+ * and its `--` separator is also valid inside a class. Keying on that would refuse a class
396
+ * diagram that happens to use the keyword.
397
+ */
398
+ const PLANTUML_ER_CARDINALITY = /(?:^|\s)(?:\|\||\}\||\}\o|\|\{|o\{|o\||\|o)(?:--|\.\.)/m
399
+
400
+ export function plantumlErSyntax(text: string): boolean {
401
+ return PLANTUML_ER_CARDINALITY.test(text)
402
+ }
403
+
404
+ /**
405
+ * Communication-diagram syntax: an arrow whose label is a sequence number.
406
+ *
407
+ * `C --> O : 1: create` is the whole tell. An object diagram's arrows carry names
408
+ * (`C --> O : owns`), and only a communication diagram numbers them. Without this the text
409
+ * reads as an object diagram - which parses, and which drops every number into a label
410
+ * nobody checks.
411
+ */
412
+ const PLANTUML_COMMUNICATION_ARROW = /^\s*\S+\s*(?:-->|->|--|\.\.>|\.\.)\s*\S+\s*:\s*\d+(?:\.\d+)*[a-zA-Z]?\s*:/m
413
+
414
+ export function plantumlCommunicationSyntax(text: string): boolean {
415
+ return PLANTUML_COMMUNICATION_ARROW.test(text)
416
+ }
417
+
418
+ function sequenceMarkers(text: string): boolean {
419
+ return /^\s*participant\b/m.test(text) || /->>\s*/.test(text)
420
+ }
421
+
422
+ export interface UmlDetectOptions {
423
+ /** The family a `dsh-model-everything:uml` directive declared, when the text carries one. */
424
+ hint?: UmlFamilyId
425
+ }
426
+
427
+ /**
428
+ * Which UML diagram type this text is, in this notation.
429
+ *
430
+ * Total by construction: every branch either names a family or throws. A foreign
431
+ * Mermaid family (ER, gantt, mindmap, C4, ...) is refused by name, so the caller can
432
+ * report *what* the text is instead of blaming the notation.
433
+ */
434
+ export function detectUmlFamily(text: string, notation: UmlNotation, options: UmlDetectOptions = {}): UmlFamilyDetection {
435
+ if (options.hint !== undefined) return { family: options.hint, evidence: 'dsh-model-everything: directive' }
436
+
437
+ if (notation === 'mermaid') {
438
+ const header = mermaidHeader(text)
439
+ if (header !== undefined) return header
440
+ const foreign = foreignMermaidFamily(text)
441
+ if (foreign !== undefined) {
442
+ throw new UmlError('`' + foreign + '` is not a UML diagram type: this plugin models the UML 2.5 diagram families, so this text cannot be read into one of them. Discarded: ' + foreign + ' x1. Render the diagram you mean with uml action=render and keep this one as its own view.', 'UML_NOT_A_STATE_DIAGRAM')
443
+ }
444
+ throw new UmlError('cannot tell which UML diagram type this Mermaid text declares: expected a header such as ' + mermaidHeaderList())
445
+ }
446
+
447
+ // PlantUML declares most families by a keyword, and a few by their syntax alone.
448
+ // The keyword-free syntaxes come first: a use case diagram has no header at all.
449
+ if (plantumlUseCaseSyntax(text)) return { family: 'use-case', evidence: 'use case syntax (:Actor: / (UseCase))' }
450
+ if (PLANTUML_PROFILE_MARKER.test(text)) return { family: 'profile', evidence: 'profile marker (stereotype / metaclass)' }
451
+ // An ER diagram is not one of the fourteen UML 2.5 families, so it is refused before any
452
+ // family is picked: its syntax overlaps the class and component diagrams closely enough
453
+ // that reading it produces a wrong graph rather than an error.
454
+ if (plantumlErSyntax(text)) {
455
+ throw new UmlError('this is an entity-relationship diagram, which is not a UML 2.5 diagram family: this plugin models the fourteen UML diagram types. Its notation overlaps the class and component diagrams - `USER ||--o{ ORDER` is a cardinality, not a node - so reading it would report on a graph the diagram does not draw. Review the schema with dsh-model-everything-datamodel instead.', 'UML_NOT_A_STATE_DIAGRAM')
456
+ }
457
+ // A port declaration outranks a nested-element keyword: a composite structure is drawn
458
+ // with a component block, and the ports are what make it that diagram rather than a
459
+ // component diagram.
460
+ if (plantumlCompositeSyntax(text)) return { family: 'composite-structure', evidence: 'composite syntax (port / portin / portout)' }
461
+ // A numbered arrow outranks the object declaration that carries it: the numbering is what
462
+ // makes the diagram a communication diagram.
463
+ if (plantumlCommunicationSyntax(text)) return { family: 'communication', evidence: 'numbered message labels (C --> O : 1: create)' }
464
+ if (plantumlTimingSyntax(text)) return { family: 'timing', evidence: 'timing syntax (robust/concise/binary/clock or @tick)' }
465
+
466
+ // Declaration keywords, most specific family first. component before package
467
+ // matters: a component diagram may contain packages, and the component is the topic.
468
+ const order: readonly UmlFamilyId[] = ['class', 'object', 'component', 'deployment', 'package', 'sequence', 'activity']
469
+ for (const family of order) {
470
+ const keyword = plantumlDeclaration(text, family)
471
+ if (keyword === undefined) continue
472
+ // actor and database are lifelines in a sequence diagram: the sequence markers
473
+ // win when both are present.
474
+ if (family !== 'sequence' && sequenceMarkers(text)) return { family: 'sequence', evidence: 'sequence markers (participant / ->>)' }
475
+ return { family, evidence: 'declaration: ' + keyword }
476
+ }
477
+ if (sequenceMarkers(text)) return { family: 'sequence', evidence: 'sequence markers (participant / ->>)' }
478
+
479
+ // PlantUML state diagrams declare no kind: [*] --> A is the whole header. That is the
480
+ // default, and it is now reached only after every other family said no.
481
+ return { family: 'state', evidence: 'state syntax ([*], state, -->)' }
482
+ }
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // Capability
486
+ // ---------------------------------------------------------------------------
487
+
488
+ /** The four actions the UML front end offers. */
489
+ export type UmlIntentAction = 'render' | 'parse' | 'review' | 'labels'
490
+
491
+ export type UmlCapability =
492
+ | { ok: true; warning?: string }
493
+ | { ok: false; errorCode: string; message: string }
494
+
495
+ export interface UmlCapabilityOptions {
496
+ /**
497
+ * Whether this call needs a parse. A review of a *model* in a family the notation can
498
+ * only approximate does not, and refusing it for a reason that belongs to the diagram
499
+ * would be refusing the part that works.
500
+ */
501
+ requireParse?: boolean
502
+ }
503
+
504
+ /**
505
+ * Whether this (notation, family, action) cell exists.
506
+ *
507
+ * This is the notation's answer, not this build's: a family the table says is faithful
508
+ * is still refused when no module implements it, and that refusal belongs to the
509
+ * registry (see `uml.ts`). Keeping the two apart is what lets the engine document
510
+ * all fourteen diagram types while the implementation lands family by family.
511
+ */
512
+ export function umlCapability(notation: UmlNotation, family: UmlFamilyId, action: UmlIntentAction, options: UmlCapabilityOptions = {}): UmlCapability {
513
+ if (!isUmlNotation(notation)) {
514
+ return { ok: false, errorCode: 'UML_INPUT', message: 'unknown notation "' + String(notation) + '"; expected ' + UML_NOTATIONS.join(' | ') }
515
+ }
516
+ if (!isUmlFamilyId(family)) {
517
+ return { ok: false, errorCode: 'UML_INPUT', message: 'unknown diagram "' + String(family) + '"; expected ' + UML_FAMILY_IDS.join(' | ') }
518
+ }
519
+ if (action === 'labels') return { ok: true }
520
+ const spec = umlFamilySpec(family)
521
+ const support = umlNotationSupport(notation, family)
522
+
523
+ if (action === 'render') {
524
+ if (support.fidelity === 'none') {
525
+ return { ok: false, errorCode: 'UML_VIEW_UNREPRESENTABLE', message: support.note ?? (notation + ' cannot draw a ' + spec.name) }
526
+ }
527
+ if (support.fidelity === 'approximate') {
528
+ return { ok: true, warning: 'UML_RENDER_APPROXIMATE_VIEW: ' + (support.note ?? (notation + ' draws an approximation of a ' + spec.name)) }
529
+ }
530
+ return { ok: true }
531
+ }
532
+
533
+ if (options.requireParse === false) return { ok: true }
534
+ if (!spec.parseable) {
535
+ return { ok: false, errorCode: spec.parseRefusalCode ?? 'UML_NOT_A_STATE_DIAGRAM', message: spec.parseRefusal ?? ('a ' + spec.name + ' cannot be read back into a model') }
536
+ }
537
+ if (support.fidelity !== 'faithful') {
538
+ if (support.fidelity === 'none') return { ok: false, errorCode: 'UML_VIEW_UNREPRESENTABLE', message: support.note ?? (notation + ' cannot draw a ' + spec.name) }
539
+ // An approximation is readable only when the family says so. The families that do are
540
+ // the ones whose round trip has been measured; the rest refuse rather than guess.
541
+ if (spec.parseThroughApproximation === true) return { ok: true, warning: 'UML_PARSE_APPROXIMATE_VIEW: ' + (support.note ?? (notation + ' draws an approximation of a ' + spec.name)) }
542
+ return {
543
+ ok: false,
544
+ errorCode: 'UML_VIEW_UNREPRESENTABLE',
545
+ message: 'reading a ' + spec.name + ' back out of ' + notation + ' would produce a model of a different diagram: ' + (support.note ?? 'the notation only approximates it') + ' Render the diagram instead, and keep the model in the form the notation can carry.',
546
+ }
547
+ }
548
+ return { ok: true }
549
+ }
550
+
551
+ /** `umlCapability`, as a throw. */
552
+ export function assertUmlCapability(notation: UmlNotation, family: UmlFamilyId, action: UmlIntentAction): UmlCapability {
553
+ const capability = umlCapability(notation, family, action)
554
+ if (!capability.ok) throw new UmlError(capability.message, capability.errorCode)
555
+ return capability
556
+ }
557
+
558
+ // ---------------------------------------------------------------------------
559
+ // Family modules
560
+ // ---------------------------------------------------------------------------
561
+
562
+ export interface UmlFamilyParse<Model> {
563
+ model: Model
564
+ /** Display labels found in the diagram, keyed by element id. */
565
+ labels: Record<string, string>
566
+ warnings: string[]
567
+ /** Declarations the parser could not represent, with the line that carried them. */
568
+ discardedConstructs: Array<{ construct: string; line: number; text: string }>
569
+ discardedEdges: number
570
+ }
571
+
572
+ export interface UmlFamilyRenderOptions {
573
+ /** Cap on a trace length, for the families that render one. */
574
+ maxSteps: number
575
+ }
576
+
577
+ export interface UmlFamilyRender {
578
+ primary: string
579
+ warnings: string[]
580
+ }
581
+
582
+ export interface UmlFamilyFinding {
583
+ code: string
584
+ severity: 'error' | 'warning' | 'info'
585
+ message: string
586
+ detail?: string
587
+ evidence?: Record<string, unknown>
588
+ }
589
+
590
+ /**
591
+ * What a family module must provide. The engine finds the elements; the module gives
592
+ * them meaning, draws them back, and says what a reader would get wrong.
593
+ */
594
+ export interface UmlFamilyModule<Model> {
595
+ id: UmlFamilyId
596
+ /**
597
+ * The notation this family is drawn in, when only one can draw it.
598
+ *
599
+ * Absent means "either, as the caller asks", which is true for most families. A family
600
+ * whose table gives one notation `none` has exactly one answer, and saying it here is
601
+ * what lets a review of a *model* render the family instead of reporting that the model
602
+ * cannot be drawn at all.
603
+ */
604
+ notation?: UmlNotation
605
+ render(input: unknown, notation: UmlNotation, options: UmlFamilyRenderOptions): UmlFamilyRender
606
+ parse(text: string, notation: UmlNotation): UmlFamilyParse<Model>
607
+ /**
608
+ * The model in the family's own canonical shape, or undefined when the input is not one.
609
+ *
610
+ * Fidelity is checked by rendering a model and comparing what comes back with what went
611
+ * in. Where the parser necessarily normalizes - filling in a default the diagram always
612
+ * draws, or recording a drawn name as a label - comparing against the *raw* input reports
613
+ * the module's own convention as a mismatch. This is how a family says which model the
614
+ * comparison is about; a family that normalizes nothing can leave it out.
615
+ */
616
+ normalize?(input: unknown): Model | undefined
617
+ /** Family findings for the review: what the diagram itself gets wrong. */
618
+ review?(model: Model, labels: Record<string, string>, notation: UmlNotation): UmlFamilyFinding[]
619
+ }
620
+
621
+ // ---------------------------------------------------------------------------
622
+ // Directives
623
+ // ---------------------------------------------------------------------------
624
+
625
+ /** Marker every generated diagram carries; renderers ignore it, the parser uses it. */
626
+ export const DIRECTIVE_NAMESPACE = 'dsh-model-everything:'
627
+
628
+ /** The directive body of a comment line, or null when the line is not a directive. */
629
+ export function directiveBody(line: string, notation: UmlNotation): string | null {
630
+ const prefix = commentPrefix(notation)
631
+ const trimmed = line.trim()
632
+ if (!trimmed.startsWith(prefix)) return null
633
+ const body = trimmed.slice(prefix.length).trim()
634
+ if (!body.startsWith(DIRECTIVE_NAMESPACE)) return null
635
+ return body.slice(DIRECTIVE_NAMESPACE.length)
636
+ }
637
+
638
+ /** One directive line, in the notation's own comment syntax. */
639
+ export function directiveLine(notation: UmlNotation, text: string): string {
640
+ return commentPrefix(notation) + DIRECTIVE_NAMESPACE + text
641
+ }
642
+
643
+ export interface UmlDirectives {
644
+ /** What the `uml v1` header declared, when it carried one. */
645
+ declared: { notation?: string; family?: string; diagram?: string }
646
+ /** Every other directive, keyed by its first word, values in file order. */
647
+ values: Map<string, string[]>
648
+ /** The lines that are not directives. */
649
+ body: string[]
650
+ }
651
+
652
+ /**
653
+ * Read the `dsh-model-everything:` directives out of a diagram.
654
+ *
655
+ * `diagram=` is the pre-family spelling and is still read: a file rendered by an
656
+ * earlier version must parse the same way it did.
657
+ */
658
+ export function readUmlDirectives(lines: readonly string[], notation: UmlNotation): UmlDirectives {
659
+ const declared: UmlDirectives['declared'] = {}
660
+ const values = new Map<string, string[]>()
661
+ const body: string[] = []
662
+ for (const line of lines) {
663
+ const text = directiveBody(line, notation)
664
+ if (text === null) { body.push(line); continue }
665
+ if (text.startsWith('uml ')) {
666
+ const notationMatch = /notation=([a-z]+)/.exec(text)
667
+ const familyMatch = /family=([a-z-]+)/.exec(text)
668
+ const diagramMatch = /diagram=([a-z-]+)/.exec(text)
669
+ if (notationMatch !== null) declared.notation = notationMatch[1]
670
+ if (familyMatch !== null) declared.family = familyMatch[1]
671
+ if (diagramMatch !== null) declared.diagram = diagramMatch[1]
672
+ continue
673
+ }
674
+ const split = text.indexOf(' ')
675
+ const key = split < 0 ? text.trim() : text.slice(0, split).trim()
676
+ const value = split < 0 ? '' : text.slice(split + 1).trim()
677
+ if (key === '') continue
678
+ const list = values.get(key)
679
+ if (list === undefined) values.set(key, [value])
680
+ else list.push(value)
681
+ }
682
+ return { declared, values, body }
683
+ }
684
+
685
+ /** The family a rendered diagram declares, in either spelling. */
686
+ export function declaredFamily(directives: UmlDirectives): UmlFamilyId | undefined {
687
+ const raw = directives.declared.family ?? directives.declared.diagram
688
+ if (raw === undefined) return undefined
689
+ return isUmlFamilyId(raw) ? raw : undefined
690
+ }
691
+
692
+ // ---------------------------------------------------------------------------
693
+ // Scanning
694
+ // ---------------------------------------------------------------------------
695
+
696
+ export interface UmlSourceLine {
697
+ /** 1-based line number in the source text. */
698
+ number: number
699
+ /** The line, trimmed. */
700
+ text: string
701
+ }
702
+
703
+ /**
704
+ * The content lines of a diagram: blank lines, comments and notation chrome removed,
705
+ * each with its source line number. Chrome is what a renderer needs and a reader does
706
+ * not: `@startuml`, `@enduml`, `direction`, `classDef`, `skinparam`,
707
+ * `scale`, `title`, `hide` and `autonumber`.
708
+ */
709
+ export function umlSourceLines(text: string, notation: UmlNotation, skip?: RegExp): UmlSourceLine[] {
710
+ const chrome = notation === 'mermaid'
711
+ ? /^(classDef|class|style|linkStyle|click|direction|accTitle|accDescr)\b/
712
+ : /^(@startuml|@enduml|scale|skinparam|title|hide|autonumber|!theme|left to right direction|top to bottom direction)\b/
713
+ const out: UmlSourceLine[] = []
714
+ const lines = text.split(/\r?\n/)
715
+ const prefix = commentPrefix(notation)
716
+ for (let index = 0; index < lines.length; index += 1) {
717
+ const trimmed = lines[index].trim()
718
+ if (trimmed === '') continue
719
+ if (trimmed.startsWith(prefix)) continue
720
+ if (chrome.test(trimmed)) continue
721
+ if (skip !== undefined && skip.test(trimmed)) continue
722
+ out.push({ number: index + 1, text: trimmed })
723
+ }
724
+ return out
725
+ }
726
+
727
+ export interface UmlEdgeSplit {
728
+ from: string
729
+ operator: string
730
+ to: string
731
+ label?: string
732
+ }
733
+
734
+ const MERMAID_EDGE = /^(.*?)\s*(<\|--|--\|>|\.\.\|>|<\|\.\.|-->>|->>|-->|==>|-\.->|\.\.>|---|--|\.\.)\s*(.*)$/
735
+ const PLANTUML_EDGE = /^(.*?)\s*(<\|--|--\|>|\.\.\|>|<\|\.\.|-->|->>|->|\.\.>|\.>|==>|--|==|\.\.|\*--|o--|--\*|--o)\s*(.*)$/
736
+
737
+ /**
738
+ * Split one line into from / operator / to, with a trailing `: label` separated.
739
+ *
740
+ * The operator tables are the notation's, not the family's: a class diagram writes
741
+ * `A <|-- B` and a use case diagram writes `A --> B` in the same notation, and both
742
+ * come through here. Whether an operator *means* a dependency, an association or a
743
+ * generalization is the family module's call.
744
+ */
745
+ export function splitUmlEdge(line: string, notation: UmlNotation): UmlEdgeSplit | undefined {
746
+ const match = (notation === 'mermaid' ? MERMAID_EDGE : PLANTUML_EDGE).exec(line)
747
+ if (match === null) return undefined
748
+ const from = stripEdgeEndpoints(match[1])
749
+ if (from === '') return undefined
750
+ let rest = match[3].trim()
751
+ let label: string | undefined
752
+ const colon = rest.indexOf(':')
753
+ if (colon >= 0) {
754
+ label = rest.slice(colon + 1).trim().replace(/^"|"$/g, '').trim()
755
+ rest = rest.slice(0, colon).trim()
756
+ }
757
+ const to = stripEdgeEndpoints(rest)
758
+ if (to === '') return undefined
759
+ return { from, operator: match[2], to, ...(label === undefined || label === '' ? {} : { label }) }
760
+ }
761
+
762
+ function stripEdgeEndpoints(text: string): string {
763
+ return text.trim().replace(/^["']|["']$/g, '').trim()
764
+ }