chronos/src/parser.typ

1036 lines
23 KiB
Typst

#import "diagram.typ": diagram, _par, _evt, _gap
#import "participant.typ": SHAPES
#import "separator.typ": _sep, _delay
#import "sequence.typ": _seq, _ret
#import "group.typ": _grp, _alt
#import "note.typ": _note, SIDES
#let COLORS = (
aliceblue: rgb("#f0f8ff"),
antiquewhite: rgb("#faebd7"),
aqua: rgb("#00ffff"),
aquamarine: rgb("#7fffd4"),
azure: rgb("#f0ffff"),
beige: rgb("#f5f5dc"),
bisque: rgb("#ffe4c4"),
black: rgb("#000000"),
blanchedalmond: rgb("#ffebcd"),
blue: rgb("#0000ff"),
blueviolet: rgb("#8a2be2"),
brown: rgb("#a52a2a"),
burlywood: rgb("#deb887"),
cadetblue: rgb("#5f9ea0"),
chartreuse: rgb("#7fff00"),
chocolate: rgb("#d2691e"),
coral: rgb("#ff7f50"),
cornflowerblue: rgb("#6495ed"),
cornsilk: rgb("#fff8dc"),
crimson: rgb("#dc143c"),
cyan: rgb("#00ffff"),
darkblue: rgb("#00008b"),
darkcyan: rgb("#008b8b"),
darkgoldenrod: rgb("#b8860b"),
darkgray: rgb("#a9a9a9"),
darkgreen: rgb("#006400"),
darkgrey: rgb("#a9a9a9"),
darkkhaki: rgb("#bdb76b"),
darkmagenta: rgb("#8b008b"),
darkolivegreen: rgb("#556b2f"),
darkorange: rgb("#ff8c00"),
darkorchid: rgb("#9932cc"),
darkred: rgb("#8b0000"),
darksalmon: rgb("#e9967a"),
darkseagreen: rgb("#8fbc8f"),
darkslateblue: rgb("#483d8b"),
darkslategray: rgb("#2f4f4f"),
darkslategrey: rgb("#2f4f4f"),
darkturquoise: rgb("#00ced1"),
darkviolet: rgb("#9400d3"),
deeppink: rgb("#ff1493"),
deepskyblue: rgb("#00bfff"),
dimgray: rgb("#696969"),
dimgrey: rgb("#696969"),
dodgerblue: rgb("#1e90ff"),
firebrick: rgb("#b22222"),
floralwhite: rgb("#fffaf0"),
forestgreen: rgb("#228b22"),
fuchsia: rgb("#ff00ff"),
gainsboro: rgb("#dcdcdc"),
ghostwhite: rgb("#f8f8ff"),
gold: rgb("#ffd700"),
goldenrod: rgb("#daa520"),
gray: rgb("#808080"),
green: rgb("#008000"),
greenyellow: rgb("#adff2f"),
grey: rgb("#808080"),
honeydew: rgb("#f0fff0"),
hotpink: rgb("#ff69b4"),
indianred: rgb("#cd5c5c"),
indigo: rgb("#4b0082"),
ivory: rgb("#fffff0"),
khaki: rgb("#f0e68c"),
lavender: rgb("#e6e6fa"),
lavenderblush: rgb("#fff0f5"),
lawngreen: rgb("#7cfc00"),
lemonchiffon: rgb("#fffacd"),
lightblue: rgb("#add8e6"),
lightcoral: rgb("#f08080"),
lightcyan: rgb("#e0ffff"),
lightgoldenrodyellow: rgb("#fafad2"),
lightgray: rgb("#d3d3d3"),
lightgreen: rgb("#90ee90"),
lightgrey: rgb("#d3d3d3"),
lightpink: rgb("#ffb6c1"),
lightsalmon: rgb("#ffa07a"),
lightseagreen: rgb("#20b2aa"),
lightskyblue: rgb("#87cefa"),
lightslategray: rgb("#778899"),
lightslategrey: rgb("#778899"),
lightsteelblue: rgb("#b0c4de"),
lightyellow: rgb("#ffffe0"),
lime: rgb("#00ff00"),
limegreen: rgb("#32cd32"),
linen: rgb("#faf0e6"),
magenta: rgb("#ff00ff"),
maroon: rgb("#800000"),
mediumaquamarine: rgb("#66cdaa"),
mediumblue: rgb("#0000cd"),
mediumorchid: rgb("#ba55d3"),
mediumpurple: rgb("#9370db"),
mediumseagreen: rgb("#3cb371"),
mediumslateblue: rgb("#7b68ee"),
mediumspringgreen: rgb("#00fa9a"),
mediumturquoise: rgb("#48d1cc"),
mediumvioletred: rgb("#c71585"),
midnightblue: rgb("#191970"),
mintcream: rgb("#f5fffa"),
mistyrose: rgb("#ffe4e1"),
moccasin: rgb("#ffe4b5"),
navajowhite: rgb("#ffdead"),
navy: rgb("#000080"),
oldlace: rgb("#fdf5e6"),
olive: rgb("#808000"),
olivedrab: rgb("#6b8e23"),
orange: rgb("#ffa500"),
orangered: rgb("#ff4500"),
orchid: rgb("#da70d6"),
palegoldenrod: rgb("#eee8aa"),
palegreen: rgb("#98fb98"),
paleturquoise: rgb("#afeeee"),
palevioletred: rgb("#db7093"),
papayawhip: rgb("#ffefd5"),
peachpuff: rgb("#ffdab9"),
peru: rgb("#cd853f"),
pink: rgb("#ffc0cb"),
plum: rgb("#dda0dd"),
powderblue: rgb("#b0e0e6"),
purple: rgb("#800080"),
rebeccapurple: rgb("#663399"),
red: rgb("#ff0000"),
rosybrown: rgb("#bc8f8f"),
royalblue: rgb("#4169e1"),
saddlebrown: rgb("#8b4513"),
salmon: rgb("#fa8072"),
sandybrown: rgb("#f4a460"),
seagreen: rgb("#2e8b57"),
seashell: rgb("#fff5ee"),
sienna: rgb("#a0522d"),
silver: rgb("#c0c0c0"),
skyblue: rgb("#87ceeb"),
slateblue: rgb("#6a5acd"),
slategray: rgb("#708090"),
slategrey: rgb("#708090"),
snow: rgb("#fffafa"),
springgreen: rgb("#00ff7f"),
steelblue: rgb("#4682b4"),
tan: rgb("#d2b48c"),
teal: rgb("#008080"),
thistle: rgb("#d8bfd8"),
tomato: rgb("#ff6347"),
turquoise: rgb("#40e0d0"),
violet: rgb("#ee82ee"),
wheat: rgb("#f5deb3"),
white: rgb("#ffffff"),
whitesmoke: rgb("#f5f5f5"),
yellow: rgb("#ffff00"),
yellowgreen: rgb("#9acd32"),
)
#let CREOLE = (
"bold": (
open: "**",
close: "**",
func: strong
),
"italic": (
open: "//",
close: "//",
func: emph
),
"mono": (
open: "\"\"",
close: "\"\"",
func: it => text(it, font: "DejaVu Sans Mono")
),
"stricken": (
open: "--",
close: "--",
func: strike
),
"under": (
open: "__",
close: "__",
func: underline
),
"wavy": (
open: "~~",
close: "~~",
func: it => panic("Wavy underline is not supported (see https://github.com/typst/typst/issues/2835)")
)
)
#let parse-creole(txt) = {
txt = txt.replace("\\n", "\n")
.replace("<<", sym.quote.angle.double.l)
.replace(">>", sym.quote.angle.double.r)
let part-stack = ()
let style-stack = ()
let cur-style = none
let steps = ()
let tmp = ""
for char in txt {
cur-style = if style-stack.len() == 0 {
none
} else {
style-stack.last()
}
tmp += char
for (name, style) in CREOLE.pairs() {
if tmp.ends-with(style.close) {
//if cur-style == name {
if name in style-stack {
tmp = tmp.slice(0, tmp.len() - style.close.len())
let to-append = ""
let rev-style-stack = style-stack.rev()
let i = rev-style-stack.position(s => s == name)
for _ in range(i) {
let style = style-stack.pop()
tmp = CREOLE.at(style).open + tmp
}
style-stack.pop()
if tmp.len() == 0 {
if part-stack.len() != 0 {
to-append += (style.func)(part-stack.pop())
}
} else {
to-append += (style.func)(tmp)
}
tmp = ""
part-stack.last() = part-stack.last() + to-append
break
}
}
if tmp.ends-with(style.open) {
tmp = tmp.slice(0, tmp.len() - style.open.len())
part-stack.push(tmp)
tmp = ""
style-stack.push(name)
break
}
}
steps.push((part-stack, tmp, style-stack))
}
if part-stack.len() == 0 {
part-stack.push(tmp)
} else {
part-stack.last() += tmp
}
if style-stack.len() != 0 {
let dbg-steps = steps
let dbg-txt = txt
panic("Unclosed '" + style-stack.last() + "' style in string '" + txt + "'")
}
return part-stack.join()
}
#let parse-color(value) = {
if value.starts-with("#") {
value = value.slice(1)
}
let m = value.match(regex("^(.+)([|/\\\-])(.+)$"))
if m != none {
let (col1, angle, col2) = m.captures
col1 = parse-color(col1)
col2 = parse-color(col2)
angle = (
"|": 0deg,
"/": 45deg,
"-": 90deg,
"\\": 135deg
).at(angle)
return gradient.linear(col1, col2, angle: angle)
}
if lower(value) in COLORS {
return COLORS.at(lower(value))
}
return rgb(value)
}
#let is-boundary-stmt(line) = {
return line.starts-with("@startuml") or line.starts-with("@enduml")
}
#let is-comment-stmt(line) = {
return line.starts-with("'")
}
#let is-ret-stmt(line) = {
return line.starts-with("return")
}
#let parse-ret-stmt(line) = {
let m = line.match(regex("^return(\s+(.*?))?$"))
let comment = m.captures.last()
return _ret(comment: comment)
}
#let is-par-stmt(line) = {
for shape in SHAPES {
if shape == "custom" {
continue
}
if line.starts-with(shape) {
return true
}
}
return false
}
#let parse-par-stmt(line) = {
let arg-stack = line.split(" ")
let arg-stack2 = ()
let in-string = false
for arg in arg-stack {
if in-string {
if arg.ends-with("\"") {
arg = arg.slice(0, arg.len()-1)
in-string = false
}
arg-stack2.last() += " " + arg
} else {
if arg.starts-with("\"") {
arg = arg.slice(1)
if arg.ends-with("\"") {
arg = arg.slice(0, arg.len()-1)
} else {
in-string = true
}
}
if not in-string and arg.trim().len() == 0 {
continue
}
arg-stack2.push(arg)
}
}
arg-stack = arg-stack2.rev()
let shape = arg-stack.pop()
let display-name = auto
let name = arg-stack.pop()
let color = auto
while arg-stack.len() != 0 {
let arg = arg-stack.pop()
if arg == "as" {
display-name = name
name = arg-stack.pop()
} else if arg.starts-with("#") {
color = parse-color(arg)
}
}
if display-name != auto {
display-name = parse-creole(display-name)
}
return _par(
name,
display-name: display-name,
shape: shape,
color: color
)
}
#let is-gap-stmt(line) = {
return line == "|||" or line.match(regex("\|\|\d+\|\|")) != none
}
#let parse-gap-stmt(line) = {
if line == "|||" {
return _gap()
}
let size = int(line.slice(2, line.len() - 2))
return _gap(size: size)
}
#let is-delay-stmt(line) = {
return line.starts-with("...")
}
#let parse-delay-stmt(line) = {
if line == "..." {
return _delay()
}
let m = line.match(regex("\.\.\.\s*(.*?)\s*\.\.\."))
let name = m.captures.first()
return _delay(name: name)
}
#let is-sep-stmt(line) = {
return line.starts-with("==") and line.ends-with("==")
}
#let parse-sep-stmt(line) = {
let name = line.slice(2, -2).trim()
return _sep(name)
}
#let is-evt-stmt(line) = {
return (
line.starts-with("activate") or
line.starts-with("deactivate") or
line.starts-with("create") or
line.starts-with("destroy")
)
}
#let parse-evt-stmt(line) = {
let (evt, par, col) = line.match(
regex(```(?x)
^
(?<evt>.+?)
\s+
(?<par>.+?)
(?<col>\s+.*)?
$```.text
)
).captures
if col != none {
col = parse-color(col.trim())
}
// TODO: lifeline style (i.e. use col)
let event = (
"activate": "enable",
"deactivate": "disable",
"create": "create",
"destroy": "destroy"
).at(evt)
return _evt(par, event)
}
#let is-grp-stmt(line) = {
return (
line.starts-with("group") or
line.starts-with("loop") or
line.starts-with("alt") or
line.starts-with("else")
)
}
#let parse-grp-stmt(line) = {
let words = line.split(" ")
let group-type = words.first()
let rest = words.slice(1).join(" ")
let is-group = group-type == "group"
let name = group-type
let desc = rest
if is-group {
let m = rest.match(
regex(
```(?x)
^
(.*?)
(\[.*\])?
$
```.text
)
)
name = m.captures.first()
desc = m.captures.last()
if desc != none {
desc = desc.trim(regex("[\[\]]*"))
}
}
return (
name: name,
desc: desc,
type: if is-group {
"default"
} else {
group-type
}
)
}
#let is-simple-note-stmt(line) = {
return line.starts-with(regex("/?\s*[hr]?note")) and line.contains(":")
}
#let parse-note-data(stmt) = {
let aligned = stmt.starts-with("/")
stmt = stmt.trim(regex("/?\s*"))
let data-parts = stmt.split(regex("\s+"))
let note-type = data-parts.at(0)
let shape = (
"note": "default",
"hnote": "hex",
"rnote": "rect"
).at(note-type)
let side = data-parts.at(1)
if side not in SIDES {
panic("Invalid note side '" + side + "'")
}
let color = auto
if data-parts.last().starts-with("#") {
color = parse-color(data-parts.pop())
}
let pos = none
if side == "left" or side == "right" {
if data-parts.len() >= 3 {
if data-parts.at(2) == "of" and data-parts.len() >= 4 {
pos = data-parts.at(3)
} else {
pos = data-parts.at(2)
}
}
} else if side == "over" {
pos = data-parts.slice(2)
.join(" ")
.split(",")
.map(p => p.trim())
if pos.len() == 1 {
pos = pos.first()
}
}
return (
side: side,
shape: shape,
color: color,
pos: pos,
aligned: aligned
)
}
#let parse-simple-note-stmt(line) = {
let (data, ..note) = line.split(":")
note = note.join(":")
note = parse-creole(note.trim())
let note-data = parse-note-data(data)
return _note(
note-data.side,
note,
color: note-data.color,
pos: note-data.pos,
shape: note-data.shape,
aligned: note-data.aligned
)
}
#let is-multiline-note-stmt(line) = {
return line.starts-with(regex("/?\s*[hr]?note")) and not line.contains(":")
}
#let parse-multiline-note-stmt(line) = {
let note-data = parse-note-data(line)
note-data.insert("lines", ())
return note-data
}
#let parse-seq-tips(arrow) = {
let m = arrow.trim().match(
regex(```(?x)
^
([ox])?
(<|<<|\\|\\\\|/|//)?
(-{1,2})
(>|>>|\\|\\\\|/|//)?
([ox])?
$
```.text
)
)
if m == none {
//panic(arrow)
return none
}
let flip = false
let (pre-start, start, mid, end, post-end) = m.captures
if start != none and end == none {
flip = true
}
if start != none {
start = (
"<": ">",
"\\": "/",
"\\\\": "//",
"/": "\\",
"//": "\\\\",
).at(start, default: start)
}
let start-tip = (pre-start, start).filter(t => t != none)
let end-tip = (post-end, end).filter(t => t != none)
if start-tip.len() == 1 {
start-tip = start-tip.first()
} else if start-tip.len() == 0 {
start-tip = ""
}
if end-tip.len() == 1 {
end-tip = end-tip.first()
} else if end-tip.len() == 0 {
end-tip = ""
}
if pre-start == "x" {
start-tip = "x"
}
if post-end == "x" {
end-tip = "x"
}
return (start-tip, mid == "--", end-tip, flip)
}
#let parse-seq-stmt(line) = {
let p1 = ""
let arrow = ""
let p2 = ""
let mods = ""
let comment = ""
let color = ""
let state = "idle"
let in-string = false
let steps = ()
let post-par = ""
let p1-as = ""
let p2-as = ""
for char in line.clusters() {
steps.push((state, char, in-string))
if steps.len() > 12 {
let a = steps
}
// Idle: go until first non whitespace character
// if first char is " -> in-string
if state == "idle" {
if char.match(regex("\s")) == none {
if char == "\"" {
in-string = true
} else {
p1 += char
}
if p1 in ("[", "?") {
state = "post-p1"
} else {
state = "p1"
}
}
// First participant: if in string, go until end of string,
// otherwise go until arrow character
} else if state == "p1" {
if in-string {
if char == "\"" {
in-string = false
state = "post-p1"
} else {
p1 += char
}
} else {
if char.match(regex(`-|<|>|\\|\\\\|/|//`.text)) != none {
arrow += char
state = "arrow"
} else if char.match(regex("\s")) != none {
state = "post-p1"
} else {
p1 += char
if p1 in ("[", "?") {
state = "post-p1"
}
}
}
} else if state == "post-p1" {
if char.match(regex(`o|x|-|<|>|\\|\\\\|/|//`.text)) != none {
arrow += char
state = "arrow"
} else if char.match(regex("\s")) == none {
post-par += char
if post-par.match(regex("as\s")) != none {
state = "p1-as"
post-par = ""
} else if post-par.match("^(a(s(\s?))?)?$") == none {
panic("Unexpected characters '" + post-par + "' after first participant")
}
}
} else if state == "p1-as" {
if char.match(regex("\s|o|x|-|<|>|\\|\\\\|/|//")) != none {
arrow += char
state = "arrow"
} else {
p1-as += char
}
// Arrow:
} else if state == "arrow" {
if char.match(regex(`o|x|-|<|>|\\|\\\\|/|//`.text)) != none {
arrow += char
} else {
if char == "\"" {
in-string = true
state = "p2"
} else if char == " " {
state = "pre-p2"
} else {
p2 += char
state = "p2"
}
}
} else if state == "pre-p2" {
if char.match(regex("\s")) == none {
if char == "\"" {
in-string = true
} else {
p2 += char
}
state = "p2"
}
// Second participant: if in string go until end of string
} else if state == "p2" {
if in-string {
if char == "\"" {
in-string = false
state = "post-p2"
} else {
p2 += char
}
} else {
if char.match(regex("\s")) != none {
state = "post-p2"
} else if char in "+-*!" {
state = "mods"
mods += char
} else if char == ":" {
state = "comment"
} else {
p2 += char
}
}
} else if state == "post-p2" {
if post-par.len() == 0 {
if char.match(regex("\s")) != none {
continue
}
if char in "+-*!" {
state = "mods"
mods += char
} else if char == ":" {
state = "comment"
} else {
post-par += char
}
} else {
post-par += char
if post-par.match(regex("^\s*as\s$")) != none {
state = "p2-as"
post-par = ""
} else if post-par.match(regex("^\s*(a(s(\s?))?)?$")) == none {
let a = steps
panic("Unexpected characters '" + post-par + "' after second participant")
}
}
} else if state == "p2-as" {
if char.match(regex("\s|\+|-|\*|!")) != none {
mods += char
state = "mods"
} else if char == ":" {
state = "comment"
} else {
p2-as += char
}
} else if state == "mods" {
if char.match(regex("\s|\+|-|\*|!")) != none {
mods += char
} else {
if char == ":" {
state = "comment"
} else if char.match(regex("\s")) != none {
state = "post-mods"
} else {
panic("Unexpected character '" + char + "' after mods")
}
}
} else if state == "post-mods" {
if char == ":" {
state = "comment"
}
} else if state == "comment" {
comment += char
}
}
p1 = parse-creole(p1)
p2 = parse-creole(p2)
comment = parse-creole(comment.trim())
p1-as = p1-as.trim()
p2-as = p2-as.trim()
let elmts = ()
if p1-as != "" {
elmts += _par(p1-as, display-name: p1)
p1 = p1-as
}
if p2-as != "" {
elmts += _par(p2-as, display-name: p2)
p2 = p2-as
}
/*let m = line.match(
regex(```(?x)
^
(?<p1>[a-zA-Z0-9_]+)
\s*
(?<arrow>[^o].*?)
\s*
(?<p2>[a-zA-Z0-9_]+)
\s*
(?<mods>[+\-*! ]{2,})?
\s*
(:\s*(?<comment>.*))?
```.text
)
)*/
/*
let m = line.match(
regex(```(?x)
^
(?<p1>[a-zA-Z0-9_\[?]+)
\s*
(?<arrow>[^o].*?)
\s*
(?<p2>[a-zA-Z0-9_\]?]+)
\s*
(?<mods>[+\-*! ]*?)
\s*
(?<color>\#.+)?
\s*
(?::\s*(?<comment>.*))?
$
```.text
)
)
if m == none {
panic(line)
}
let (p1, arrow, p2, mods, color, comment) = m.captures
*/
/*
let p1 = line.find(regex("^[a-zA-Z0-9_]+"))
line = line.slice(p1.len()).trim()
let i = line.position(regex("(?:[^o])[a-zA-Z0-9_ ]"))
if i == none {
return ()
}
let arrow = line.slice(0, i)
line = line.slice(i).trim()
let p2 = line.find(regex("^[a-zA-Z0-9_]+"))
line = line.slice(p2.len()).trim()
*/
//let comment = none
let enable-dst = false
let disable-src = false
let create-dst = false
let destroy-dst = false
/*if line.contains(":") {
(line, comment) = line.split(":")
comment = comment.trim()
line = line.trim()
}*/
if mods.contains("++") {
enable-dst = true
}
if mods.contains("--") {
disable-src = true
}
if mods.contains("**") {
create-dst = true
}
if mods.contains("!!") {
destroy-dst = true
}
// TODO start / end tips
let seq-tips = parse-seq-tips(arrow)
if seq-tips == none {
return ()
}
let (start-tip, dashed, end-tip, flip) = seq-tips
if flip and p1 == p2 {
(start-tip, end-tip) = (end-tip, start-tip)
}
//panic(start-tip, dashed, end-tip)
//comment += json.encode((start-tip, dashed, end-tip)).replace("\n", "")
elmts += _seq(
p1, p2,
comment: comment,
dashed: dashed,
start-tip: start-tip,
end-tip: end-tip,
enable-dst: enable-dst,
disable-src: disable-src,
create-dst: create-dst,
destroy-dst: destroy-dst,
flip: flip
)
return elmts
}
#let from-plantuml(code, width: auto) = {
let code = code.text
code = code.replace(regex("(?s)/'.*?'/"), "")
let elmts = ()
let lines = code.split("\n")
let group-stack = ()
let note-data = none
for line in lines {
if note-data != none {
let l = line.trim()
if l in ("end note", "endrnote", "endhnote") {
elmts += _note(
note-data.side,
note-data.lines.join("\n"),
color: note-data.color,
pos: note-data.pos,
shape: note-data.shape,
aligned: note-data.aligned
)
note-data = none
} else {
note-data.lines.push(line)
}
continue
}
line = line.trim()
if line.len() == 0 { continue }
if is-boundary-stmt(line) or is-comment-stmt(line) {
continue
} else if is-par-stmt(line) {
elmts += parse-par-stmt(line)
} else if is-gap-stmt(line) {
elmts += parse-gap-stmt(line)
} else if is-delay-stmt(line) {
elmts += parse-delay-stmt(line)
} else if is-sep-stmt(line) {
elmts += parse-sep-stmt(line)
} else if is-evt-stmt(line) {
elmts += parse-evt-stmt(line)
} else if is-ret-stmt(line) {
elmts += parse-ret-stmt(line)
} else if is-grp-stmt(line) {
let group-data = parse-grp-stmt(line)
group-data.insert("start-i", elmts.len())
group-stack.push(group-data)
} else if line == "end" {
if group-stack.len() == 0 {
continue
}
let data = group-stack.pop()
if data.type in ("alt", "else") {
let sections = ()
let section-elmts = ()
while true {
section-elmts = elmts.slice(data.start-i)
elmts = elmts.slice(0, data.start-i)
sections.push(section-elmts)
sections.push(data.desc)
if data.type == "alt" {
break
} else if data.type != "else" {
panic("Alt/else group mismatch")
}
data = group-stack.pop()
}
elmts += _alt(..sections.rev())
continue
}
let grp-elmts = elmts.slice(data.start-i)
elmts = elmts.slice(0, data.start-i)
elmts += _grp(
data.name,
grp-elmts,
desc: data.desc,
type: data.type
)
} else if is-simple-note-stmt(line) {
elmts += parse-simple-note-stmt(line)
} else if is-multiline-note-stmt(line) {
note-data = parse-multiline-note-stmt(line)
} else {
elmts += parse-seq-stmt(line)
}
}
return diagram(elmts, width: width)
}