#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) ^ (?.+?) \s+ (?.+?) (?\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) ^ (?[a-zA-Z0-9_]+) \s* (?[^o].*?) \s* (?[a-zA-Z0-9_]+) \s* (?[+\-*! ]{2,})? \s* (:\s*(?.*))? ```.text ) )*/ /* let m = line.match( regex(```(?x) ^ (?[a-zA-Z0-9_\[?]+) \s* (?[^o].*?) \s* (?[a-zA-Z0-9_\]?]+) \s* (?[+\-*! ]*?) \s* (?\#.+)? \s* (?::\s*(?.*))? $ ```.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) }