limu 0.2.1

A diagram layout engine. It reads Mermaid and leaves the drawing to your program.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! A look that is theme_only draws a diagram that sets colors in its text as it draws the
//! same diagram without them.

use limu::layout::Options;
use limu::scene::style::Look;
use limu::scene::svg;

/// A diagram that sets colors in its text, the same diagram without the lines that set them,
/// and the classes its author named, which the SVG writer writes as the classes of elements.
struct Case {
    name: &'static str,
    src: &'static str,
    bare: &'static str,
    classes: &'static [&'static str],
}

const CASES: &[Case] = &[
    // classDef fills, a style line on a group that edges run inside and across, which lays
    // casings under them, and a style line on a node.
    Case {
        name: "flowchart",
        src: r##"flowchart LR
  subgraph S[Group]
    A --> B
  end
  B --> C
  D --> A
  classDef hot fill:#c00,stroke:#600,stroke-width:3px,color:#fff
  class C hot
  style S fill:#123456,stroke:#f80,stroke-dasharray:4 2
  style D fill:#ff0,stroke-linecap:round"##,
        bare: r##"flowchart LR
  subgraph S[Group]
    A --> B
  end
  B --> C
  D --> A"##,
        classes: &["hot"],
    },
    Case {
        name: "state",
        src: r##"stateDiagram-v2
  [*] --> Moving
  state Moving {
    Slow --> Fast
  }
  Fast --> Still
  classDef bad fill:#300,color:#fcc
  class Still bad
  style Moving fill:#222"##,
        bare: r##"stateDiagram-v2
  [*] --> Moving
  state Moving {
    Slow --> Fast
  }
  Fast --> Still"##,
        classes: &["bad"],
    },
    Case {
        name: "er",
        src: r##"erDiagram
  subgraph G
    A ||--o{ B : has
  end
  B ||--|| C : is
  C {
    int id
    string name
  }
  style G fill:#004
  style C fill:#fe0,stroke:#000"##,
        bare: r##"erDiagram
  subgraph G
    A ||--o{ B : has
  end
  B ||--|| C : is
  C {
    int id
    string name
  }"##,
        classes: &[],
    },
    Case {
        name: "block",
        src: r##"block-beta
  columns 3
  block:G
    a b
  end
  c
  a --> c
  style G fill:#333
  style c fill:#9cf"##,
        bare: r##"block-beta
  columns 3
  block:G
    a b
  end
  c
  a --> c"##,
        classes: &[],
    },
    Case {
        name: "agentflow",
        src: r##"agentflow-beta TB
  flow reviewer["Review Agent"]
    changes["Gather changes"]@{ shape: input }
    ok["Clean?"]@{ shape: decision }
    changes --> ok
  end
  done["Done"]
  ok --> done
  style reviewer fill:#003
  style done fill:#fd0"##,
        bare: r##"agentflow-beta TB
  flow reviewer["Review Agent"]
    changes["Gather changes"]@{ shape: input }
    ok["Clean?"]@{ shape: decision }
    changes --> ok
  end
  done["Done"]
  ok --> done"##,
        classes: &[],
    },
    Case {
        name: "usecase",
        src: r##"usecase-beta
  direction LR
  classDef system fill:#050
  classDef order fill:#fcf
  actor Customer("Customer")
  systemBoundary Shop["Order system"]:::system
    Checkout("Place order"):::order
  end
  Customer --> Checkout"##,
        bare: r##"usecase-beta
  direction LR
  actor Customer("Customer")
  systemBoundary Shop["Order system"]
    Checkout("Place order")
  end
  Customer --> Checkout"##,
        classes: &["system", "order"],
    },
    Case {
        name: "sequence",
        src: r##"sequenceDiagram
  box rgb(20, 40, 80) Team
  participant A
  participant B
  end
  A->>B: Hello
  rect rgb(0, 0, 0)
  B->>A: Hi
  end"##,
        bare: r##"sequenceDiagram
  box Team
  participant A
  participant B
  end
  A->>B: Hello
  rect
  B->>A: Hi
  end"##,
        classes: &[],
    },
    Case {
        name: "pie",
        src: r##"---
config:
  themeVariables:
    pie1: "#101010"
    pie2: transparent
    pie3: currentColor
---
pie title Pets
  "Dogs": 386
  "Cats": 85
  "Rats": 15"##,
        bare: r##"pie title Pets
  "Dogs": 386
  "Cats": 85
  "Rats": 15"##,
        classes: &[],
    },
    Case {
        name: "treemap",
        src: r##"---
config:
  themeVariables:
    cScale0: "#202020"
    cScale1: "#f0e000"
---
treemap-beta
"Section":::frame
  "a": 10:::dark
  "b": 20
"Other"
  "c": 30
classDef frame fill:#FFF9C4,stroke-width:2px
classDef dark fill:navy"##,
        bare: r##"treemap-beta
"Section"
  "a": 10
  "b": 20
"Other"
  "c": 30"##,
        classes: &["frame", "dark"],
    },
    Case {
        name: "mindmap",
        src: r##"---
config:
  themeVariables:
    cScale1: "#400000"
    cScale2: "#00ff00"
---
mindmap
  root((Root))
    A
      A1
    B"##,
        bare: r##"mindmap
  root((Root))
    A
      A1
    B"##,
        classes: &[],
    },
    Case {
        name: "gitGraph",
        src: r##"---
config:
  themeVariables:
    git0: "#000080"
    git1: "#ffff00"
---
gitGraph
  commit
  branch develop
  commit
  checkout main
  merge develop"##,
        bare: r##"gitGraph
  commit
  branch develop
  commit
  checkout main
  merge develop"##,
        classes: &[],
    },
    Case {
        name: "timeline",
        src: r##"---
config:
  themeVariables:
    cScale0: "#300030"
    cScale1: "#e0ffe0"
---
timeline
  title History
  section Early
    2002 : LinkedIn
  section Later
    2004 : Facebook : Google"##,
        bare: r##"timeline
  title History
  section Early
    2002 : LinkedIn
  section Later
    2004 : Facebook : Google"##,
        classes: &[],
    },
    Case {
        name: "radar",
        src: r##"---
config:
  themeVariables:
    cScale0: "#880000"
    cScale1: "rgba(0, 0, 255, 0.5)"
---
radar-beta
  axis A, B, C
  curve c1{1, 2, 3}
  curve c2{3, 2, 1}"##,
        bare: r##"radar-beta
  axis A, B, C
  curve c1{1, 2, 3}
  curve c2{3, 2, 1}"##,
        classes: &[],
    },
    Case {
        name: "venn",
        src: r##"---
config:
  themeVariables:
    venn1: "#aa0000"
    venn2: "#0000aa"
---
venn-beta
  set A
  set B
  union A,B"##,
        bare: r##"venn-beta
  set A
  set B
  union A,B"##,
        classes: &[],
    },
    Case {
        name: "xychart",
        src: r##"---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#ff0000, #00aa00"
---
xychart-beta
  x-axis [jan, feb, mar]
  bar [50, 60, 70]
  line [40, 80, 30]"##,
        bare: r##"xychart-beta
  x-axis [jan, feb, mar]
  bar [50, 60, 70]
  line [40, 80, 30]"##,
        classes: &[],
    },
];

/// An SVG document with the classes of elements named left out of their class attributes.
fn without_classes(doc: &str, names: &[&str]) -> String {
    const ATTR: &str = " class=\"";
    let mut out = String::with_capacity(doc.len());
    let mut rest = doc;
    while let Some(at) = rest.find(ATTR) {
        out.push_str(&rest[..at]);
        let value = &rest[at + ATTR.len()..];
        let end = value.find('"').expect("a class attribute that ends");
        let kept: Vec<&str> = value[..end]
            .split_whitespace()
            .filter(|c| !names.contains(c))
            .collect();
        if !kept.is_empty() {
            out.push_str(ATTR);
            out.push_str(&kept.join(" "));
            out.push('"');
        }
        rest = &value[end + 1..];
    }
    out.push_str(rest);
    out
}

fn draw(src: &str, name: &str, look: Look) -> String {
    let sc = limu::layout(src, &Options::default()).unwrap_or_else(|e| panic!("{name}: {e}"));
    svg::render(
        &sc,
        &svg::Options {
            look,
            ..svg::Options::default()
        },
    )
}

// What the author's style lines, classes and palette set, the colors picked for what lies on
// the author's fills, and the casings, are all left out. A look that isn't theme_only draws
// the colors.
#[test]
fn theme_only_draws_as_without_the_authors_colors() {
    let only = Look {
        theme_only: true,
        ..Look::default()
    };
    for c in CASES {
        let got = without_classes(&draw(c.src, c.name, only.clone()), c.classes);
        let want = draw(c.bare, c.name, Look::default());
        assert!(
            got == want,
            "{} drawn theme_only:\n{got}\nwithout its colors:\n{want}",
            c.name
        );
        let drawn = without_classes(&draw(c.src, c.name, Look::default()), c.classes);
        assert!(drawn != got, "{} draws no color of its author's", c.name);
    }
}

// The flowchart above lays casings under the edges that cross its group's fill, which a
// theme_only look doesn't draw.
#[test]
fn theme_only_leaves_out_casings() {
    let sc = limu::layout(CASES[0].src, &Options::default()).expect("a flowchart");
    assert!(
        sc.items
            .iter()
            .any(|it| it.role.as_str().ends_with(".casing")),
        "no casing laid out"
    );
    let look = Look {
        theme_only: true,
        ..Look::default()
    };
    assert!(!look.draws("flowchart.casing") && look.draws("flowchart.edge"));
    assert!(Look::default().draws("flowchart.casing"));
}