@zombie-mermaid/core 2.2.1
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/LICENSE +22 -0
- package/dist/index.cjs +29 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1236 -0
- package/dist/index.d.ts +1236 -0
- package/dist/index.js +888 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
- package/src/__tests__/click-directive.test.ts +63 -0
- package/src/__tests__/color-utils.test.ts +186 -0
- package/src/__tests__/diagram-type.test.ts +28 -0
- package/src/__tests__/text-metrics.test.ts +486 -0
- package/src/__tests__/theme-palette-contrast.test.ts +84 -0
- package/src/__tests__/theme.test.ts +162 -0
- package/src/click-directive.ts +106 -0
- package/src/color-utils.ts +205 -0
- package/src/diagram-type.ts +35 -0
- package/src/direction-override.ts +46 -0
- package/src/direction.ts +56 -0
- package/src/generated/mono-font-subset.ts +32 -0
- package/src/index.ts +34 -0
- package/src/init-directive.ts +248 -0
- package/src/multiline-utils.ts +275 -0
- package/src/statements.ts +197 -0
- package/src/style-directives.ts +214 -0
- package/src/text-metrics.ts +520 -0
- package/src/theme.ts +712 -0
- package/src/types.ts +670 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// zombie-mermaid — shared statement splitting
|
|
3
|
+
//
|
|
4
|
+
// Every parser entry point (flowchart/state in src/parser.ts, the four
|
|
5
|
+
// specialized parsers dispatched from src/index.ts and src/ascii/index.ts)
|
|
6
|
+
// needs the same thing: turn raw Mermaid source into a list of trimmed,
|
|
7
|
+
// comment-free statements. They each used to do it inline with
|
|
8
|
+
// `.split('\n').map(trim).filter(...)`, which meant semicolon separators —
|
|
9
|
+
// valid Mermaid, and the form `detectDiagramType` already assumed when
|
|
10
|
+
// isolating the header — were understood by nobody.
|
|
11
|
+
//
|
|
12
|
+
// This module is the single source of truth for "what counts as one
|
|
13
|
+
// statement".
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Matches a character reference in either spelling Mermaid accepts.
|
|
18
|
+
*
|
|
19
|
+
* HTML/XML form: `&`, `#`, `😀`.
|
|
20
|
+
* Mermaid's own form, which uses `#` where HTML uses `&`: `#59;`, `#9829;`,
|
|
21
|
+
* `#quot;` — the documented way to put a literal semicolon, quote, or symbol
|
|
22
|
+
* into a label.
|
|
23
|
+
*
|
|
24
|
+
* Both end in a semicolon that belongs to the reference, not to statement
|
|
25
|
+
* separation. Splitting one apart corrupts the label *and* invents a
|
|
26
|
+
* statement out of the remainder.
|
|
27
|
+
*/
|
|
28
|
+
const ENTITY_AT = /^[&#](?:#?[0-9]+|#?[xX][0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/
|
|
29
|
+
|
|
30
|
+
/** Characters that can open a character reference. */
|
|
31
|
+
const ENTITY_STARTERS = new Set(['&', '#'])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Index of the `%%` that starts a comment on this line, or -1.
|
|
35
|
+
*
|
|
36
|
+
* A Mermaid comment runs to the end of the line, so everything after it —
|
|
37
|
+
* including any further `;`-separated fragments — is commented out. Scanning
|
|
38
|
+
* for it before splitting is what makes that true: dropping fragments that
|
|
39
|
+
* merely *begin* with `%%` afterwards leaves `graph TD; A-->B; %% note; C-->D`
|
|
40
|
+
* still parsing `C-->D` as code.
|
|
41
|
+
*
|
|
42
|
+
* Quote- and reference-aware for the same reasons the splitter is, so a `%%`
|
|
43
|
+
* inside a label is text rather than a comment marker.
|
|
44
|
+
*/
|
|
45
|
+
function commentStart(line: string): number {
|
|
46
|
+
let quote: '"' | "'" | null = null
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < line.length; i++) {
|
|
49
|
+
const ch = line[i]!
|
|
50
|
+
|
|
51
|
+
if (quote !== null) {
|
|
52
|
+
if (ch === quote) quote = null
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
if (ch === '"' || ch === "'") {
|
|
56
|
+
quote = ch
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (ENTITY_STARTERS.has(ch)) {
|
|
60
|
+
const entity = ENTITY_AT.exec(line.slice(i))
|
|
61
|
+
if (entity) {
|
|
62
|
+
i += entity[0].length - 1
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (ch === '%' && line[i + 1] === '%') return i
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return -1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Split one already-newline-separated line on its statement-separating
|
|
74
|
+
* semicolons.
|
|
75
|
+
*
|
|
76
|
+
* A semicolon separates statements unless it is:
|
|
77
|
+
* - inside a quoted string (`A["a; b"]`), or
|
|
78
|
+
* - the terminator of a character reference (`A[&]`).
|
|
79
|
+
*
|
|
80
|
+
* Quote tracking is deliberately simple — Mermaid has no escape sequence
|
|
81
|
+
* inside quoted labels, so a quote character always opens or closes a span.
|
|
82
|
+
*/
|
|
83
|
+
function splitOnSemicolons(line: string): string[] {
|
|
84
|
+
const parts: string[] = []
|
|
85
|
+
let current = ''
|
|
86
|
+
let quote: '"' | "'" | null = null
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < line.length; i++) {
|
|
89
|
+
const ch = line[i]!
|
|
90
|
+
|
|
91
|
+
if (quote !== null) {
|
|
92
|
+
current += ch
|
|
93
|
+
if (ch === quote) quote = null
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (ch === '"' || ch === "'") {
|
|
98
|
+
quote = ch
|
|
99
|
+
current += ch
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (ENTITY_STARTERS.has(ch)) {
|
|
104
|
+
// Copy a whole character reference in one go so its trailing ';' can't
|
|
105
|
+
// be read as a separator. Guarded on '#' as well as '&': Mermaid's own
|
|
106
|
+
// `#59;` spelling is far more common in diagrams than the HTML one.
|
|
107
|
+
const entity = ENTITY_AT.exec(line.slice(i))
|
|
108
|
+
if (entity) {
|
|
109
|
+
current += entity[0]
|
|
110
|
+
i += entity[0].length - 1
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ch === ';') {
|
|
116
|
+
parts.push(current)
|
|
117
|
+
current = ''
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
current += ch
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
parts.push(current)
|
|
125
|
+
return parts
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* One statement produced by the splitter, paired with the 1-based physical
|
|
130
|
+
* source line it came from.
|
|
131
|
+
*
|
|
132
|
+
* `line` is captured while walking `text.split('\n')`, before blank lines
|
|
133
|
+
* and `%%` comment lines are dropped — so it survives being the original
|
|
134
|
+
* source line number even though a statement's array index no longer does.
|
|
135
|
+
* This is what lets every parser built on top of `splitStatements`/
|
|
136
|
+
* `splitStatementsByLine` report *where* an error is, not just quote back
|
|
137
|
+
* the offending statement's text (see issue #760).
|
|
138
|
+
*/
|
|
139
|
+
export interface Statement {
|
|
140
|
+
text: string
|
|
141
|
+
line: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Split Mermaid source into trimmed statements, grouped by the physical
|
|
146
|
+
* source line each one came from — dropping blank lines and `%%` comment
|
|
147
|
+
* lines.
|
|
148
|
+
*
|
|
149
|
+
* Newlines and semicolons both separate statements, matching Mermaid's own
|
|
150
|
+
* `graph TD; A-->B;` form. Comments are removed *before* semicolon splitting
|
|
151
|
+
* so that a `;` inside a comment can't resurrect the rest of that line as
|
|
152
|
+
* code.
|
|
153
|
+
*
|
|
154
|
+
* The grouping is what lets a caller tell a genuine multi-line continuation
|
|
155
|
+
* (crossing from one line's group into the next) apart from an explicit
|
|
156
|
+
* `;`-separated statement on the *same* line (a later entry within one
|
|
157
|
+
* group) — see `src/parser.ts`'s `mergeContinuationLines`, which only
|
|
158
|
+
* treats the former as mergeable.
|
|
159
|
+
*/
|
|
160
|
+
export function splitStatementsByLine(text: string): Statement[][] {
|
|
161
|
+
const groups: Statement[][] = []
|
|
162
|
+
const rawLines = text.split('\n')
|
|
163
|
+
|
|
164
|
+
for (let lineIndex = 0; lineIndex < rawLines.length; lineIndex++) {
|
|
165
|
+
let line = rawLines[lineIndex]!.trim()
|
|
166
|
+
const lineNumber = lineIndex + 1
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Cut the comment off first. A Mermaid comment runs to end of line, so
|
|
170
|
+
* anything after `%%` is commented out — including further `;`-separated
|
|
171
|
+
* fragments. Splitting first and discarding fragments that start with
|
|
172
|
+
* `%%` would leave `A-->B; %% note; C-->D` parsing `C-->D` as code.
|
|
173
|
+
*/
|
|
174
|
+
const comment = commentStart(line)
|
|
175
|
+
if (comment !== -1) line = line.slice(0, comment).trim()
|
|
176
|
+
if (line.length === 0) continue
|
|
177
|
+
|
|
178
|
+
const statements: Statement[] = []
|
|
179
|
+
for (const part of splitOnSemicolons(line)) {
|
|
180
|
+
const statement = part.trim()
|
|
181
|
+
if (statement.length === 0) continue
|
|
182
|
+
statements.push({ text: statement, line: lineNumber })
|
|
183
|
+
}
|
|
184
|
+
if (statements.length > 0) groups.push(statements)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return groups
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Split Mermaid source into trimmed statements, dropping blank lines and
|
|
192
|
+
* `%%` comment lines. See `splitStatementsByLine` for the line-grouped form
|
|
193
|
+
* this flattens.
|
|
194
|
+
*/
|
|
195
|
+
export function splitStatements(text: string): Statement[] {
|
|
196
|
+
return splitStatementsByLine(text).flat()
|
|
197
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Style directives — `classDef`, `class A,B name`, `cssClass "A,B" name`,
|
|
3
|
+
// `style A ...`, and the `A:::name` shorthand.
|
|
4
|
+
//
|
|
5
|
+
// Mermaid's flowchart and class diagrams share one styling grammar
|
|
6
|
+
// (https://mermaid.js.org/syntax/flowchart.html#styling-and-classes,
|
|
7
|
+
// https://mermaid.js.org/syntax/classDiagram.html#styling). The flowchart
|
|
8
|
+
// parser grew the first implementation; this module is that implementation
|
|
9
|
+
// lifted out so the class-diagram parser can reuse it rather than copy it
|
|
10
|
+
// (issue #420). Both parsers feed a `StyleDirectives` bag, and both layouts
|
|
11
|
+
// resolve a node's final style through `resolveNodeStyle` with the same
|
|
12
|
+
// cascade — so a `classDef default` means the same thing in every diagram
|
|
13
|
+
// type that supports it.
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/** The three maps a styling-aware diagram carries. `MermaidGraph` and
|
|
17
|
+
* `ClassDiagram` both satisfy this structurally. */
|
|
18
|
+
export interface StyleDirectives {
|
|
19
|
+
/** Maps class names to their style properties (from `classDef name prop:val`) */
|
|
20
|
+
classDefs: Map<string, Record<string, string>>
|
|
21
|
+
/** Maps node IDs to their assigned class name (from `class A,B name`, `cssClass "A,B" name`, or `A:::name`) */
|
|
22
|
+
classAssignments: Map<string, string>
|
|
23
|
+
/** Maps node IDs to inline styles (from `style X fill:#f00,stroke:#333`) */
|
|
24
|
+
nodeStyles: Map<string, Record<string, string>>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parse "fill:#f00,stroke:#333" style property strings into a Record */
|
|
28
|
+
export function parseStyleProps(propsStr: string): Record<string, string> {
|
|
29
|
+
// Strip trailing semicolons — Mermaid tolerates them (e.g. `stroke:#f00;`)
|
|
30
|
+
const cleaned = propsStr.replace(/;\s*$/, '')
|
|
31
|
+
const props: Record<string, string> = {}
|
|
32
|
+
for (const pair of cleaned.split(',')) {
|
|
33
|
+
const colonIdx = pair.indexOf(':')
|
|
34
|
+
if (colonIdx > 0) {
|
|
35
|
+
const key = pair.slice(0, colonIdx).trim()
|
|
36
|
+
const val = pair.slice(colonIdx + 1).trim()
|
|
37
|
+
if (key && val) {
|
|
38
|
+
props[key] = val
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return props
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `classDef name prop:val,prop:val` — define a named style class.
|
|
47
|
+
*
|
|
48
|
+
* Mermaid's `classList` rule accepts a comma-separated list of names
|
|
49
|
+
* (`classDef a,b font-size:12pt`) in both the flowchart and class grammars;
|
|
50
|
+
* every name in the list gets the same properties.
|
|
51
|
+
*
|
|
52
|
+
* Returns true when `line` was a classDef statement (and has been applied).
|
|
53
|
+
*/
|
|
54
|
+
export function tryApplyClassDef(
|
|
55
|
+
line: string,
|
|
56
|
+
target: StyleDirectives,
|
|
57
|
+
): boolean {
|
|
58
|
+
const match = line.match(/^classDef\s+([\w,-]+)\s+(.+)$/)
|
|
59
|
+
if (!match) return false
|
|
60
|
+
const props = parseStyleProps(match[2]!)
|
|
61
|
+
for (const name of match[1]!.split(',')) {
|
|
62
|
+
const trimmed = name.trim()
|
|
63
|
+
if (trimmed) target.classDefs.set(trimmed, props)
|
|
64
|
+
}
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `class A,B className` — attach a style class to one or more nodes.
|
|
70
|
+
*
|
|
71
|
+
* Allows an optional trailing semicolon (`class A,B foo;`) — Mermaid treats
|
|
72
|
+
* it as valid/optional, and `classDef`/`style` already tolerate it via their
|
|
73
|
+
* `(.+)$` capture. Without this, the semicolon form fails to match here and
|
|
74
|
+
* falls through to node parsing, rendering a stray node labelled "class".
|
|
75
|
+
*
|
|
76
|
+
* Returns true when `line` was a class-assignment statement.
|
|
77
|
+
*/
|
|
78
|
+
export function tryApplyClassAssignment(
|
|
79
|
+
line: string,
|
|
80
|
+
target: StyleDirectives,
|
|
81
|
+
): boolean {
|
|
82
|
+
const match = line.match(/^class\s+([\w,-]+)\s+([\w-]+)\s*;?\s*$/)
|
|
83
|
+
if (!match) return false
|
|
84
|
+
const className = match[2]!
|
|
85
|
+
for (const id of match[1]!.split(',')) {
|
|
86
|
+
target.classAssignments.set(id.trim(), className)
|
|
87
|
+
}
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `cssClass "A,B" className` — the class-diagram grammar's own attachment
|
|
93
|
+
* form (`cssClassStatement: CSSCLASS STR ALPHA` in classDiagram.jison).
|
|
94
|
+
* Same effect as `class A,B className`.
|
|
95
|
+
*
|
|
96
|
+
* Returns true when `line` was a cssClass statement.
|
|
97
|
+
*/
|
|
98
|
+
export function tryApplyCssClass(
|
|
99
|
+
line: string,
|
|
100
|
+
target: StyleDirectives,
|
|
101
|
+
): boolean {
|
|
102
|
+
const match = line.match(/^cssClass\s+"([^"]*)"\s+([\w-]+)\s*;?\s*$/)
|
|
103
|
+
if (!match) return false
|
|
104
|
+
const className = match[2]!
|
|
105
|
+
for (const id of match[1]!.split(',')) {
|
|
106
|
+
const trimmed = id.trim()
|
|
107
|
+
if (trimmed) target.classAssignments.set(trimmed, className)
|
|
108
|
+
}
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `style A,B fill:#f00,stroke:#333` — inline style on specific nodes.
|
|
114
|
+
* Repeated `style` statements for the same node merge property by property.
|
|
115
|
+
*
|
|
116
|
+
* Returns true when `line` was a style statement.
|
|
117
|
+
*/
|
|
118
|
+
export function tryApplyStyleStatement(
|
|
119
|
+
line: string,
|
|
120
|
+
target: StyleDirectives,
|
|
121
|
+
): boolean {
|
|
122
|
+
const match = line.match(/^style\s+([\w,-]+)\s+(.+)$/)
|
|
123
|
+
if (!match) return false
|
|
124
|
+
const props = parseStyleProps(match[2]!)
|
|
125
|
+
for (const id of match[1]!.split(',').map((s) => s.trim())) {
|
|
126
|
+
target.nodeStyles.set(id, { ...target.nodeStyles.get(id), ...props })
|
|
127
|
+
}
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Split the `:::className` shorthand off a node/class identifier.
|
|
133
|
+
*
|
|
134
|
+
* `Animal:::someclass` → `{ id: 'Animal', className: 'someclass' }`;
|
|
135
|
+
* an identifier without the shorthand comes back unchanged with no class.
|
|
136
|
+
* Class names follow CSS identifier conventions (word characters and
|
|
137
|
+
* hyphens), the same constraint the flowchart parser's CLASS_SHORTHAND_REGEX
|
|
138
|
+
* applies.
|
|
139
|
+
*/
|
|
140
|
+
export function splitClassShorthand(identifier: string): {
|
|
141
|
+
id: string
|
|
142
|
+
className?: string
|
|
143
|
+
} {
|
|
144
|
+
const match = identifier.match(/^(.+?):::([\w][\w-]*)$/)
|
|
145
|
+
if (!match) return { id: identifier }
|
|
146
|
+
return { id: match[1]!, className: match[2]! }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the final inline style for a node from classDefs and nodeStyles.
|
|
151
|
+
*
|
|
152
|
+
* Cascade, weakest to strongest:
|
|
153
|
+
* 1. `classDef default` — Mermaid's implicit base for every node
|
|
154
|
+
* 2. the node's own assigned class (`class A foo` / `A:::foo`)
|
|
155
|
+
* 3. an explicit `style A ...` directive
|
|
156
|
+
*
|
|
157
|
+
* Returns undefined when nothing in the cascade applies, so callers can fall
|
|
158
|
+
* back to theme defaults without an empty-object check.
|
|
159
|
+
*/
|
|
160
|
+
export function resolveNodeStyle(
|
|
161
|
+
nodeId: string,
|
|
162
|
+
directives: StyleDirectives,
|
|
163
|
+
): Record<string, string> | undefined {
|
|
164
|
+
let result: Record<string, string> | undefined
|
|
165
|
+
|
|
166
|
+
/*
|
|
167
|
+
* `classDef default` applies to every node without being assigned. It was
|
|
168
|
+
* previously honored only when a node named it explicitly (`class X
|
|
169
|
+
* default`), which silently diverges from Mermaid: a diagram styling all
|
|
170
|
+
* its nodes via `classDef default` rendered unstyled with no error.
|
|
171
|
+
*/
|
|
172
|
+
const defaultDef = directives.classDefs.get('default')
|
|
173
|
+
if (defaultDef) {
|
|
174
|
+
result = { ...defaultDef }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Then the node's own class, overriding the default property by property.
|
|
178
|
+
const className = directives.classAssignments.get(nodeId)
|
|
179
|
+
if (className) {
|
|
180
|
+
const classDef = directives.classDefs.get(className)
|
|
181
|
+
if (classDef) {
|
|
182
|
+
result = result ? { ...result, ...classDef } : { ...classDef }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Then, apply explicit style directives (override class styles)
|
|
187
|
+
const nodeStyle = directives.nodeStyles.get(nodeId)
|
|
188
|
+
if (nodeStyle) {
|
|
189
|
+
result = result ? { ...result, ...nodeStyle } : { ...nodeStyle }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validate a user-authored class name (from `:::className` or
|
|
197
|
+
* `class A className`) before it's emitted into the SVG `class` attribute.
|
|
198
|
+
*
|
|
199
|
+
* The parsers already constrain class names to word characters and hyphens
|
|
200
|
+
* (see `splitClassShorthand` above and the flowchart parser's
|
|
201
|
+
* CLASS_SHORTHAND_REGEX), so this is a defense-in-depth allowlist rather
|
|
202
|
+
* than an escaping step — a class name can't be made "safe" by escaping
|
|
203
|
+
* since any character other than a valid CSS identifier character would
|
|
204
|
+
* break the class token itself, not just the surrounding attribute quotes.
|
|
205
|
+
* Anything that doesn't match a valid CSS identifier (letters, digits,
|
|
206
|
+
* underscore, hyphen; not starting with a digit or a hyphen+digit) is
|
|
207
|
+
* dropped rather than emitted.
|
|
208
|
+
*/
|
|
209
|
+
export function sanitizeClassName(
|
|
210
|
+
className: string | undefined,
|
|
211
|
+
): string | undefined {
|
|
212
|
+
if (!className) return undefined
|
|
213
|
+
return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(className) ? className : undefined
|
|
214
|
+
}
|