#import "@preview/cetz:0.2.2": canvas, draw #let class-counter = counter("uml-class") #let class-colors = ( rgb("#C02B22"), rgb("#D4721C"), rgb("D3BC2F"), rgb("60CB12"), rgb("#2ECC40"), rgb("#39CCA5"), rgb("#38B4DA"), rgb("#0074d9"), rgb("#450DC9"), rgb("#890DC9"), ) #let visibility(vis, symbol) = (type: "visibility", visibility: vis, symbol: symbol) #let public = visibility("public", "+") #let private = visibility("private", "-") #let protected = visibility("protected", "#") #let package = visibility("package", "~") #let class-family(family, prefix: none) = ( type: "class-family", family: family, prefix: prefix ) #let abstract = class-family("abstract") #let interface = class-family("interface", prefix: "interface") #let trait = class-family("trait") #let enum = class-family("enum") #let utility = class-family("utility", prefix: "utility") #let staticity(static) = (type: "staticity", static: static) #let static = staticity(true) #let non-static = staticity(false) #let class(name, ..args, grouped: none) = { let attributes = () let methods = () let visibility = none let family = none let args = args.pos() if args.len() != 0 { for arg in args { if type(arg) == dictionary { if arg.type == "visibility" { visibility = arg } else if arg.type == "class-family" { family = arg } } else if type(arg) == array { for elmt in arg { if type(elmt) == dictionary { if elmt.type == "visibility" { visibility = elmt } else if elmt.type == "attribute" { attributes.push(elmt) } else if elmt.type == "method" { methods.push(elmt) } } } } } } return (( type: "class", name: name, visibility: visibility, family: family, attributes: attributes, methods: methods, color: rgb(184, 84, 80), grouped: grouped ),) } #let attr(name, default: none, ..args) = { let visibility = none let staticity = none let val-type = none for arg in args.pos() { if type(arg) == dictionary { if arg.type == "visibility" { visibility = arg } else if arg.type == "type" { val-type = arg } else if arg.type == "staticity" { staticity = staticity } } else if type(arg) == str { val-type = arg } } return (( type: "attribute", name: name, visibility: visibility, staticity: staticity, val-type: val-type, default: default ),) } #let method(name, ..args) = { let visibility = none let staticity = none let parameters = none let return-type = none for arg in args.pos() { if type(arg) == dictionary { if "type" in arg { if arg.type == "visibility" { visibility = arg } else if arg.type == "staticity" { staticity = arg } } else { parameters = arg } } else if type(arg) == str { return-type = arg } else if type(arg) == array { parameters = arg } } return (( type: "method", name: name, visibility: visibility, staticity: staticity, parameters: parameters, return-type: return-type ),) } #let render-attr(attr, defaults, show-visibility: true) = { let visibility = if attr.visibility == none { defaults.attr-visibility } else { attr.visibility } let staticity = if attr.staticity == none { defaults.attr-staticity } else { attr.staticity } let name = attr.name if staticity.static { name = underline(name) } let txt = name if show-visibility { txt = visibility.symbol + txt } if attr.val-type != none { txt += ": " + attr.val-type } if attr.default != none { txt += " = " + str(attr.default) } return txt } #let render-method(method, defaults, show-visibility: true) = { let visibility = if method.visibility == none { defaults.method-visibility } else { method.visibility } let staticity = if method.staticity == none { defaults.method-staticity } else { method.staticity } let params = () if type(method.parameters) == array { params = method.parameters } else if type(method.parameters) == dictionary { params = method.parameters .pairs() .map(((n, t)) => n + ": " + t) } let name = method.name if staticity.static { name = underline(name) } let txt = name if show-visibility { txt = visibility.symbol + txt } txt += "(" + params.join(", ") + ")" if method.return-type != none { txt += ": " + method.return-type } return txt } #let get-attributes-by-visibility(class, defaults) = { let by-vis = (:) for attr in class.attributes { let visibility = if attr.visibility == none { defaults.attr-visibility } else { attr.visibility } if not visibility.visibility in by-vis { by-vis.insert(visibility.visibility, ()) } by-vis.at(visibility.visibility).push(attr) } return by-vis } #let get-methods-by-visibility(class, defaults) = { let by-vis = (:) for method in class.methods { let visibility = if method.visibility == none { defaults.method-visibility } else { method.visibility } if not visibility.visibility in by-vis { by-vis.insert(visibility.visibility, ()) } by-vis.at(visibility.visibility).push(method) } return by-vis } #let render-class-attributes(class, defaults) = { let grouped = if class.grouped == none { defaults.class-grouped } else { class.grouped } let cells = () if not grouped { for attr in class.attributes { cells.push(render-attr(attr, defaults, show-visibility: true)) } } else { let by-vis = get-attributes-by-visibility(class, defaults) for (vis, attrs) in by-vis { cells.push(table.cell(vis + ":")) cells += attrs.map(attr => { let txt = render-attr(attr, defaults, show-visibility: false) pad(left: 1em, txt) }) } } return cells } #let render-class-methods(class, defaults) = { let grouped = if class.grouped == none { defaults.class-grouped } else { class.grouped } let cells = () if not grouped { for method in class.methods { cells.push(render-method(method, defaults, show-visibility: true)) } } else { let by-vis = get-methods-by-visibility(class, defaults) for (vis, attrs) in by-vis { cells.push(table.cell(vis + ":")) cells += attrs.map(method => { let txt = render-method(method, defaults, show-visibility: false) pad(left: 1em, txt) }) } } return cells } #let render-class(class, defaults) = context { let family = if class.family == none { defaults.class-family } else { class.family } let cells = () cells += render-class-attributes(class, defaults) cells.push(table.hline()) cells += render-class-methods(class, defaults) let name = [*#class.name*] let family = class.family if family == abstract { name = emph(name) } if family != none and family.prefix != none { name = [*«#family.prefix»*\ #name] } let class-i = class-counter.get().first() let col = class-colors.at(calc.rem(class-i, class-colors.len())) set table.hline(stroke: col) table( inset: (top: 0.5em, bottom: 0.5em, left: 0.5em, right: 0.5em), stroke: (x, y) => { let s = (left: col, right: col) if y == 0 { s.insert("top", col) s.insert("bottom", col) } return s }, align: (x, y) => { if y == 0 { return center + horizon } return left + horizon }, fill: (x, y) => if y == 0 { col.lighten(75%) } else { none }, table.header(name), ..cells, table.hline() ) class-counter.step() } #let diagram(..args) = { class-counter.update(0) let args = args.pos() let defaults = ( class-family: none, class-grouped: false, attr-visibility: public, attr-staticity: non-static, method-visibility: public, method-staticity: non-static, ) let elements = () if args.len() != 0 { for arg in args { if type(arg) == dictionary { defaults += arg } else if type(arg) == array { elements = arg } } } let cells = () for element in elements { if element.type == "class" { cells.push(render-class(element, defaults)) } } box( stroke: black, inset: 1em, grid( columns: 2, column-gutter: 1em, align: center + horizon, row-gutter: 1em, ..cells ) ) } #let render-attr2(pos, id, attr, defaults, show-visibility: true) = { let visibility = if attr.visibility == none { defaults.attr-visibility } else { attr.visibility } let staticity = if attr.staticity == none { defaults.attr-staticity } else { attr.staticity } let name = attr.name if staticity.static { name = underline(name) } let txt = name if show-visibility { txt = visibility.symbol + txt } if attr.val-type != none { txt += ": " + attr.val-type } if attr.default != none { txt += " = " + str(attr.default) } draw.content(pos, txt, name: id) } #let render-method2(method, defaults, show-visibility: true) = { let visibility = if method.visibility == none { defaults.method-visibility } else { method.visibility } let staticity = if method.staticity == none { defaults.method-staticity } else { method.staticity } let params = () if type(method.parameters) == array { params = method.parameters } else if type(method.parameters) == dictionary { params = method.parameters .pairs() .map(((n, t)) => n + ": " + t) } let name = method.name if staticity.static { name = underline(name) } let txt = name if show-visibility { txt = visibility.symbol + txt } txt += "(" + params.join(", ") + ")" if method.return-type != none { txt += ": " + method.return-type } draw.content((), txt) } #let render-class-attributes2(class, defaults) = { let grouped = if class.grouped == none { defaults.class-grouped } else { class.grouped } let cells = () let prev = () if not grouped { for (i, attr) in class.attributes.enumerate() { let id = "class-"+class.name+"-attr-"+str(i) let txt = render-attr(attr, defaults, show-visibility: true) draw.content((rel: (0, -0.4em), to: prev), name: id, anchor: "north-west", txt) prev = id + ".south-west" } } else { let by-vis = get-attributes-by-visibility(class, defaults) for (vis, attrs) in by-vis { cells.push(table.cell(vis + ":")) let vis-id = "class-"+class.name+"-attrs-"+vis draw.content((rel: (0, -0.4em), to: prev), name: vis-id, anchor: "north-west", vis + ":") prev = vis-id + ".south-west" for (i, attr) in attrs.enumerate() { let txt = render-attr(attr, defaults, show-visibility: false) txt = pad(left: 1em, txt) let id = vis-id + "-" + str(i) draw.content((rel: (0, -0.4em), to: prev), name: id, anchor: "north-west", txt) prev = id + ".south-west" } } } } #let render-class-methods2(class, defaults) = { } #let render-class2(class, defaults) = { let attr-group = render-class-attributes2(class, defaults) let method-group = render-class-methods2(class, defaults) let family = if class.family == none { defaults.class-family } else { class.family } let name = [*#class.name*] let family = class.family if family == abstract { name = emph(name) } if family != none and family.prefix != none { name = [*«#family.prefix»*\ #name] } let class-i = class-counter.get().first() let col = class-colors.at(calc.rem(class-i, class-colors.len())) draw.group(name: "class-"+class.name, { draw.group(name: "class-"+class.name+"-name", padding: 5pt, { draw.content((), name) }) draw.on-layer(-1, { draw.rect("class-"+class.name+"-name.north-west", "class-"+class.name+"-name.south-east", fill: col.lighten(75%), stroke: col, layer: -1) }) draw.group(name: "class-"+class.name+"-attrs", padding: 0.2em, { attr-group }) method-group }) draw.rect("class-"+class.name+".north-west", "class-"+class.name+".south-east", stroke: col) } #let render-class3(class, defaults) = { let id = "class-" + class.name return draw.content((rel: (3, 0), to: ()), render-class(class, defaults), name: id) } #let diagram2(..args) = context { class-counter.update(0) let args = args.pos() let defaults = ( class-family: none, class-grouped: false, attr-visibility: public, attr-staticity: non-static, method-visibility: public, method-staticity: non-static, ) let elements = () if args.len() != 0 { for arg in args { if type(arg) == dictionary { defaults += arg } else if type(arg) == array { elements = arg } } } let cells = () canvas({ for element in elements { if element.type == "class" { render-class3(element, defaults) } } draw.line("class-Test", "class-Essai", mark: (end: "diamond", scale: 2)) }) }