@yassimba/pi-loom-mermaid 0.2.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.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/index.ts +1 -0
- package/package.json +57 -0
- package/src/index.ts +103 -0
- package/src/loom-mermaid/ansi.ts +65 -0
- package/src/loom-mermaid/canvas.ts +466 -0
- package/src/loom-mermaid/class-style.ts +65 -0
- package/src/loom-mermaid/css-colors.ts +153 -0
- package/src/loom-mermaid/diagrams/class.ts +297 -0
- package/src/loom-mermaid/diagrams/er.ts +189 -0
- package/src/loom-mermaid/diagrams/flowchart.ts +519 -0
- package/src/loom-mermaid/diagrams/gitgraph.ts +230 -0
- package/src/loom-mermaid/diagrams/mindmap.ts +107 -0
- package/src/loom-mermaid/diagrams/pie.ts +121 -0
- package/src/loom-mermaid/diagrams/sequence.ts +305 -0
- package/src/loom-mermaid/diagrams/state.ts +279 -0
- package/src/loom-mermaid/diagrams/timeline.ts +117 -0
- package/src/loom-mermaid/graph-render.ts +195 -0
- package/src/loom-mermaid/graph.ts +205 -0
- package/src/loom-mermaid/index.ts +78 -0
- package/src/loom-mermaid/labels.ts +353 -0
- package/src/loom-mermaid/layout-seq.ts +234 -0
- package/src/loom-mermaid/layout.ts +1749 -0
- package/src/loom-mermaid/paint.ts +325 -0
- package/src/loom-mermaid/placement.ts +255 -0
- package/src/loom-mermaid/registry.ts +88 -0
- package/src/loom-mermaid/source-box.ts +111 -0
- package/src/loom-mermaid/statements.ts +228 -0
- package/src/loom-mermaid/types.ts +64 -0
- package/src/loom-mermaid/width-data.ts +993 -0
- package/src/loom-mermaid/width.ts +69 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The raw source in a framed box.
|
|
3
|
+
*
|
|
4
|
+
* What to show when `render` returns `null`, or returns art too wide for the
|
|
5
|
+
* space at hand. Both are the caller's call, so this is theirs to invoke — and
|
|
6
|
+
* theirs to caption, since only they know whether some other view of the
|
|
7
|
+
* diagram exists to point the reader at.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { srcLines, stripControls } from './labels.ts'
|
|
11
|
+
import { sat } from './layout.ts'
|
|
12
|
+
import type { MermaidArt, Span } from './types.ts'
|
|
13
|
+
import { measured, stringWidth } from './width.ts'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Frame `src` in a titled box, hard-wrapping its lines to `maxWidth` columns.
|
|
17
|
+
*
|
|
18
|
+
* The result can still exceed `maxWidth`: the body wraps to
|
|
19
|
+
* `max(8, maxWidth - 4)` and the ` mermaid: <kind> ` title is never truncated,
|
|
20
|
+
* so a long first token sets a floor. Check `width` if it matters.
|
|
21
|
+
*/
|
|
22
|
+
export function sourceBox(src: string, maxWidth?: number): MermaidArt {
|
|
23
|
+
src = stripControls(src)
|
|
24
|
+
const header = src.split(/\s+/).filter((w) => w !== '')[0] ?? 'diagram'
|
|
25
|
+
const title = ` mermaid: ${header} `
|
|
26
|
+
const limit = maxWidth === undefined ? undefined : Math.max(8, sat(maxWidth, 4))
|
|
27
|
+
|
|
28
|
+
const body = srcLines(src)
|
|
29
|
+
.map((l) => expandTabs(l).replace(/\s+$/, ''))
|
|
30
|
+
.reduce<{ started: boolean; lines: string[] }>(
|
|
31
|
+
(acc, l) => {
|
|
32
|
+
if (!acc.started && l === '') return acc
|
|
33
|
+
acc.started = true
|
|
34
|
+
acc.lines.push(...chunkLine(l, limit))
|
|
35
|
+
return acc
|
|
36
|
+
},
|
|
37
|
+
{ started: false, lines: [] },
|
|
38
|
+
).lines
|
|
39
|
+
|
|
40
|
+
// reduce, not a spread: a spread of stringWidths overflows the arg limit
|
|
41
|
+
// on very large sources.
|
|
42
|
+
const contentW = body.reduce((w, l) => Math.max(w, stringWidth(l)), stringWidth(title))
|
|
43
|
+
const inner = contentW + 2
|
|
44
|
+
|
|
45
|
+
const plain: string[] = []
|
|
46
|
+
const styled: Span[][] = []
|
|
47
|
+
|
|
48
|
+
const rule = '─'.repeat(sat(inner, stringWidth(title)))
|
|
49
|
+
plain.push(`╭${title}${rule}╮`)
|
|
50
|
+
styled.push([
|
|
51
|
+
{ text: '╭', role: 'border' },
|
|
52
|
+
{ text: title, role: 'title' },
|
|
53
|
+
{ text: `${rule}╮`, role: 'border' },
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
for (const line of body) {
|
|
57
|
+
const pad = ' '.repeat(sat(contentW, stringWidth(line)))
|
|
58
|
+
plain.push(`│ ${line}${pad} │`)
|
|
59
|
+
styled.push([
|
|
60
|
+
{ text: '│ ', role: 'border' },
|
|
61
|
+
{ text: line, role: 'text' },
|
|
62
|
+
{ text: `${pad} │`, role: 'border' },
|
|
63
|
+
])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const bottom = `╰${'─'.repeat(inner)}╯`
|
|
67
|
+
plain.push(bottom)
|
|
68
|
+
styled.push([{ text: bottom, role: 'border' }])
|
|
69
|
+
|
|
70
|
+
return { plain, styled, width: inner + 2, classDefs: {}, warnings: [] }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Expand tabs to 4-column stops. A literal tab measures one column here but
|
|
75
|
+
* whatever the terminal's tab stops say there, so the frame would misalign.
|
|
76
|
+
*/
|
|
77
|
+
function expandTabs(line: string): string {
|
|
78
|
+
if (!line.includes('\t')) return line
|
|
79
|
+
let out = ''
|
|
80
|
+
let col = 0
|
|
81
|
+
for (const [c, cw] of measured(line)) {
|
|
82
|
+
if (c === '\t') {
|
|
83
|
+
const pad = 4 - (col % 4)
|
|
84
|
+
out += ' '.repeat(pad)
|
|
85
|
+
col += pad
|
|
86
|
+
} else {
|
|
87
|
+
out += c
|
|
88
|
+
col += cw
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Hard-break a line at `limit` columns, never splitting a wide glyph. */
|
|
95
|
+
function chunkLine(line: string, limit: number | undefined): string[] {
|
|
96
|
+
if (limit === undefined || stringWidth(line) <= limit) return [line]
|
|
97
|
+
const out: string[] = []
|
|
98
|
+
let cur = ''
|
|
99
|
+
let curW = 0
|
|
100
|
+
for (const [c, cw] of measured(line)) {
|
|
101
|
+
if (curW + cw > limit && cur !== '') {
|
|
102
|
+
out.push(cur)
|
|
103
|
+
cur = ''
|
|
104
|
+
curW = 0
|
|
105
|
+
}
|
|
106
|
+
cur += c
|
|
107
|
+
curW += cw
|
|
108
|
+
}
|
|
109
|
+
if (cur !== '') out.push(cur)
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared statement layer: source text to statements, plus the small
|
|
3
|
+
* string-reading helpers every grammar leans on.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { asciiLower, srcLines } from './labels.ts'
|
|
7
|
+
|
|
8
|
+
function flushStatement(cur: string, out: string[]): string {
|
|
9
|
+
const trimmed = cur.trim()
|
|
10
|
+
if (trimmed !== '') out.push(trimmed)
|
|
11
|
+
return ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split one source line into statements on `;`, stopping at a `%%` comment.
|
|
16
|
+
*
|
|
17
|
+
* Quoted spans are opaque, so a label may contain `;` and `%%`.
|
|
18
|
+
*/
|
|
19
|
+
function splitStatements(line: string, out: string[]): void {
|
|
20
|
+
const chars = [...line]
|
|
21
|
+
let cur = ''
|
|
22
|
+
let inQuotes = false
|
|
23
|
+
for (let i = 0; i < chars.length; i++) {
|
|
24
|
+
const c = chars[i]
|
|
25
|
+
if (inQuotes) {
|
|
26
|
+
if (c === '"') inQuotes = false
|
|
27
|
+
cur += c
|
|
28
|
+
} else if (c === '"') {
|
|
29
|
+
inQuotes = true
|
|
30
|
+
cur += c
|
|
31
|
+
} else if (c === '%' && chars[i + 1] === '%') {
|
|
32
|
+
break
|
|
33
|
+
} else if (c === ';') {
|
|
34
|
+
cur = flushStatement(cur, out)
|
|
35
|
+
} else {
|
|
36
|
+
cur += c
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
flushStatement(cur, out)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Index just past a leading YAML frontmatter block (`---` … `---`), or 0 when
|
|
44
|
+
* there is none. While the block is still unterminated everything is
|
|
45
|
+
* frontmatter, so a streamed diagram stays blank until it closes.
|
|
46
|
+
*/
|
|
47
|
+
export function frontmatterEnd(lines: string[]): number {
|
|
48
|
+
let i = 0
|
|
49
|
+
while (i < lines.length && lines[i].trim() === '') i++
|
|
50
|
+
if (lines[i]?.trim() !== '---') return 0
|
|
51
|
+
i++
|
|
52
|
+
while (i < lines.length && lines[i].trim() !== '---') i++
|
|
53
|
+
return i + 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* All statements in a source block, in order. A leading YAML frontmatter
|
|
58
|
+
* block, part of the mermaid grammar since v10, is skipped.
|
|
59
|
+
*/
|
|
60
|
+
export function statementsOf(src: string): string[] {
|
|
61
|
+
const lines = srcLines(src)
|
|
62
|
+
const out: string[] = []
|
|
63
|
+
for (const line of lines.slice(frontmatterEnd(lines))) splitStatements(line, out)
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The `title:` of a leading frontmatter block, or null. The one frontmatter
|
|
69
|
+
* key with terminal meaning — `config` and friends style mermaid's own
|
|
70
|
+
* renderers and are deliberately ignored.
|
|
71
|
+
*/
|
|
72
|
+
export function frontmatterTitle(src: string): string | null {
|
|
73
|
+
const lines = srcLines(src)
|
|
74
|
+
const end = frontmatterEnd(lines)
|
|
75
|
+
for (const line of lines.slice(0, end)) {
|
|
76
|
+
const kv = splitOnce(line, ':')
|
|
77
|
+
// Untrimmed on the left: an indented `title:` is nested under some other
|
|
78
|
+
// key, not the diagram's.
|
|
79
|
+
if (kv === null || kv[0].trimEnd() !== 'title') continue
|
|
80
|
+
const t = kv[1].trim()
|
|
81
|
+
const quoted =
|
|
82
|
+
t.length > 1 &&
|
|
83
|
+
((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")))
|
|
84
|
+
const title = (quoted ? t.slice(1, -1) : t).trim()
|
|
85
|
+
return title === '' ? null : title
|
|
86
|
+
}
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Per-char flags: 1 where the char lies inside a double-quoted span (quotes included). */
|
|
91
|
+
export function quoteMask(chars: string[]): Uint8Array {
|
|
92
|
+
const mask = new Uint8Array(chars.length)
|
|
93
|
+
let inQuotes = false
|
|
94
|
+
for (let i = 0; i < chars.length; i++) {
|
|
95
|
+
if (chars[i] === '"') {
|
|
96
|
+
mask[i] = 1
|
|
97
|
+
inQuotes = !inQuotes
|
|
98
|
+
} else if (inQuotes) {
|
|
99
|
+
mask[i] = 1
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return mask
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Split on separator chars sitting outside double quotes and parentheses,
|
|
107
|
+
* dropping empty segments — the one splitter every grammar shares, so quote
|
|
108
|
+
* rules cannot drift between them.
|
|
109
|
+
*/
|
|
110
|
+
export function splitTop(s: string, isSep: (c: string) => boolean): string[] {
|
|
111
|
+
const out: string[] = []
|
|
112
|
+
let cur = ''
|
|
113
|
+
let inQuotes = false
|
|
114
|
+
let depth = 0
|
|
115
|
+
for (const c of s) {
|
|
116
|
+
if (c === '"') inQuotes = !inQuotes
|
|
117
|
+
else if (!inQuotes && c === '(') depth++
|
|
118
|
+
else if (!inQuotes && c === ')' && depth > 0) depth--
|
|
119
|
+
if (!inQuotes && depth === 0 && isSep(c)) {
|
|
120
|
+
if (cur !== '') out.push(cur)
|
|
121
|
+
cur = ''
|
|
122
|
+
} else {
|
|
123
|
+
cur += c
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (cur !== '') out.push(cur)
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Split `head : rest` at the first label colon, skipping `:::` tag runs so
|
|
132
|
+
* `A:::hot : desc` keeps its tag with the id. `null` when there is no colon.
|
|
133
|
+
*/
|
|
134
|
+
export function splitColon(s: string): [string, string] | null {
|
|
135
|
+
const chars = [...s]
|
|
136
|
+
for (let i = 0; i < chars.length; i++) {
|
|
137
|
+
if (chars[i] !== ':') continue
|
|
138
|
+
let run = i
|
|
139
|
+
while (run < chars.length && chars[run] === ':') run++
|
|
140
|
+
if (run - i >= 3) {
|
|
141
|
+
i = run - 1
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
return [chars.slice(0, i).join(''), chars.slice(i + 1).join('')]
|
|
145
|
+
}
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Strip trailing `:::name` tags from an id token: `A:::hot` → id `A`, classes `[hot]`. */
|
|
150
|
+
export function takeTags(token: string): { id: string; classes: string[] } {
|
|
151
|
+
const parts = token.split(':::')
|
|
152
|
+
if (parts.length === 1 || parts[0] === '') return { id: token, classes: [] }
|
|
153
|
+
return { id: parts[0], classes: parts.slice(1).filter((c) => c !== '') }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The body of a `class A,B name` statement → `[ids, names]`. The last
|
|
158
|
+
* whitespace-separated token is the name list, everything before it the ids —
|
|
159
|
+
* so a space after a comma (`class A, B warn`) still reads as two ids.
|
|
160
|
+
*/
|
|
161
|
+
export function parseClassAssign(rest: string): [string[], string[]] | null {
|
|
162
|
+
const body = rest.trim()
|
|
163
|
+
const ws = body.search(/\s\S*$/)
|
|
164
|
+
if (ws === -1) return null
|
|
165
|
+
const split = (s: string): string[] =>
|
|
166
|
+
s
|
|
167
|
+
.split(',')
|
|
168
|
+
.map((t) => t.trim())
|
|
169
|
+
.filter((t) => t !== '')
|
|
170
|
+
return [split(body.slice(0, ws)), split(body.slice(ws))]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* `A "url" [tooltip]` / `A href "url" …` → `[id, url]`. The callback forms
|
|
175
|
+
* (`call`/`callback`) return null — their quoted string is a tooltip.
|
|
176
|
+
*/
|
|
177
|
+
export function parseHref(rest: string): [string, string] | null {
|
|
178
|
+
const [id, second] = words(rest)
|
|
179
|
+
if (id === undefined || second === 'call' || second === 'callback') return null
|
|
180
|
+
const url = rest.match(/"([^"]+)"/)
|
|
181
|
+
return url === null ? null : [id, url[1]]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const firstWord = (s: string): string => s.split(/\s+/).filter((w) => w !== '')[0] ?? ''
|
|
185
|
+
export const words = (s: string): string[] => s.split(/\s+/).filter((w) => w !== '')
|
|
186
|
+
|
|
187
|
+
/** Split on the first occurrence of `sep`, Rust's `split_once`. */
|
|
188
|
+
export function splitOnce(s: string, sep: string): [string, string] | null {
|
|
189
|
+
const i = s.indexOf(sep)
|
|
190
|
+
return i === -1 ? null : [s.slice(0, i), s.slice(i + sep.length)]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const nonEmpty = (s: string): string | null => (s === '' ? null : s)
|
|
194
|
+
|
|
195
|
+
/** Diagram kind from the header statement, lowercased. */
|
|
196
|
+
export function headerKind(statements: string[]): string | null {
|
|
197
|
+
const header = statements[0]
|
|
198
|
+
if (header === undefined) return null
|
|
199
|
+
const kind = firstWord(header)
|
|
200
|
+
return kind === '' ? null : asciiLower(kind)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse the body of a `classDef` statement: `name[,name2] k1:v1,k2:v2`.
|
|
205
|
+
* Values are kept verbatim; malformed pairs are skipped.
|
|
206
|
+
*/
|
|
207
|
+
export function parseClassDef(
|
|
208
|
+
rest: string,
|
|
209
|
+
): { names: string[]; props: Record<string, string> } | null {
|
|
210
|
+
const body = rest.trim()
|
|
211
|
+
const ws = body.search(/\s/)
|
|
212
|
+
if (ws === -1) return null
|
|
213
|
+
const names = body
|
|
214
|
+
.slice(0, ws)
|
|
215
|
+
.split(',')
|
|
216
|
+
.map((s) => s.trim())
|
|
217
|
+
.filter((s) => s !== '')
|
|
218
|
+
const props: Record<string, string> = {}
|
|
219
|
+
// splitTop keeps `rgb(255,0,0)` whole; a bare comma still separates pairs.
|
|
220
|
+
for (const pair of splitTop(body.slice(ws).trim(), (c) => c === ',')) {
|
|
221
|
+
const kv = splitOnce(pair, ':')
|
|
222
|
+
if (kv === null) continue
|
|
223
|
+
const k = kv[0].trim()
|
|
224
|
+
const v = kv[1].trim()
|
|
225
|
+
if (k !== '' && v !== '') props[k] = v
|
|
226
|
+
}
|
|
227
|
+
return names.length === 0 ? null : { names, props }
|
|
228
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic role of a run of cells — what a cell *is*, decided by the
|
|
3
|
+
* renderer. The renderer never knows about colour; consumers map roles to
|
|
4
|
+
* their own theme (see `toAnsi` for the common case).
|
|
5
|
+
*
|
|
6
|
+
* - `border` box outlines, subgraph frames, compartment rules
|
|
7
|
+
* - `text` node / participant / compartment labels
|
|
8
|
+
* - `edge` connector lines and arrowheads
|
|
9
|
+
* - `edgeLabel` text sitting on an edge
|
|
10
|
+
* - `title` the `mermaid: <kind>` header of a source box
|
|
11
|
+
* - `none` blank filler
|
|
12
|
+
*
|
|
13
|
+
* Distinct from `classes`, which is what the *author* assigned.
|
|
14
|
+
*/
|
|
15
|
+
export type Role = 'border' | 'text' | 'edge' | 'edgeLabel' | 'title' | 'none'
|
|
16
|
+
|
|
17
|
+
/** A run of adjacent cells sharing one role and one set of author classes. */
|
|
18
|
+
export interface Span {
|
|
19
|
+
text: string
|
|
20
|
+
role: Role
|
|
21
|
+
/**
|
|
22
|
+
* Author-assigned class names of the node these cells belong to, from a
|
|
23
|
+
* `:::name` shorthand or a `class A,B name` statement. The renderer never
|
|
24
|
+
* interprets them — pair with `MermaidArt.classDefs` to style. Absent on
|
|
25
|
+
* cells that belong to no classed node.
|
|
26
|
+
*/
|
|
27
|
+
classes?: string[]
|
|
28
|
+
/**
|
|
29
|
+
* Link target of the node these cells belong to, from a `click A "url"`
|
|
30
|
+
* (flowchart) or `link A "url"` (class diagram) statement. `toAnsi` emits
|
|
31
|
+
* it as an OSC 8 hyperlink; other consumers map it to their own linking.
|
|
32
|
+
*/
|
|
33
|
+
href?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A rendered diagram. `plain[i]` and `styled[i]` describe the same row:
|
|
38
|
+
* `plain` is right-trimmed for display width and copy/paste, `styled` keeps
|
|
39
|
+
* the run structure needed to colour it.
|
|
40
|
+
*
|
|
41
|
+
* `width` is the display columns the widest row needs — the number to compare
|
|
42
|
+
* against the space you have. It cannot be recovered from `plain`, whose rows
|
|
43
|
+
* are strings of code points, not columns.
|
|
44
|
+
*
|
|
45
|
+
* `classDefs` are the diagram's `classDef` declarations, parsed:
|
|
46
|
+
* `classDef warning fill:#f96,stroke:#333` becomes
|
|
47
|
+
* `{ warning: { fill: '#f96', stroke: '#333' } }`. The renderer ignores them;
|
|
48
|
+
* a consumer can map them onto its own styling of the classes spans carry.
|
|
49
|
+
*
|
|
50
|
+
* `warnings` lists source the grammar could not read and dropped, and any
|
|
51
|
+
* size-cap truncation. Non-empty means the art is real but incomplete — some
|
|
52
|
+
* of what was written is not in it.
|
|
53
|
+
*
|
|
54
|
+
* They are advisory. Do not gate rendering on them: the art is the best drawing
|
|
55
|
+
* of the source either way, and a diagram being typed or streamed warns at
|
|
56
|
+
* nearly every intermediate state. Show them alongside, or once it settles.
|
|
57
|
+
*/
|
|
58
|
+
export interface MermaidArt {
|
|
59
|
+
plain: string[]
|
|
60
|
+
styled: Span[][]
|
|
61
|
+
width: number
|
|
62
|
+
classDefs: Record<string, Record<string, string>>
|
|
63
|
+
warnings: string[]
|
|
64
|
+
}
|