forked from HEL/chronos
1036 lines
23 KiB
Typst
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)
|
|
} |