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)
 | |
| } |