Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions codex-rs/mermaid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ production dependency is needed.

| Family | Supported syntax |
| --- | --- |
| `flowchart`, `graph` | TD/TB, BT, LR, RL; rectangle `[label]`, decision `{label}`, and stadium `([label])` nodes; directed and labeled `-->` edges; chains, branches, merges, loops |
| `flowchart`, `graph` | TD/TB (default), BT, LR, RL; rectangle `[label]`, decision `{label}`, and stadium `([label])` nodes; solid `-->`/`---`/`<-->` and dashed `-.->`/`-.-`/`<-.->` edges; pipe labels and spaced directed infix labels (`-- text -->`, `-. text .->`); `&` fan-out/fan-in, chains, branches, merges, loops |
| `sequenceDiagram` | Implicit participants, `participant`/`actor`, aliases, `->`, `->>`, `-->`, `-->>`, `-x`, `--x`, self-messages, `Note over A[,B]`, nested `loop`/`alt`/`opt`/`critical`/`break`, one labeled `else` per `alt` |
| `stateDiagram-v2`, `stateDiagram` | Flat states, `state "label" as ID`, descriptions, directed transitions with optional labels, initial/final `[*]`, direction declarations |
| `classDiagram` | `class ID`, multiline member bodies, `ID : member`, solid/dashed links, association, inheritance, composition, aggregation, dependency, realization, quoted endpoint cardinalities, relationship labels, direction declarations |
Expand Down Expand Up @@ -59,8 +59,8 @@ named participants. States use separate `● initial` and `◎ final` nodes.

## Limits and evaluation

All inputs are limited to 16 KiB. Graphs allow 16 nodes, 24 edges, and 16 members per
node. Sequences allow 8 participants, 64 events (including fragment boundaries),
All inputs are limited to 16 KiB. Graphs allow 16 nodes, 24 edges (after expanding
`&` groups), and 16 members per node. Each `&` group allows at most 24 node references. Sequences allow 8 participants, 64 events (including fragment boundaries),
and 4 fragment levels. Source labels and identifiers are limited to 40 display
cells/ASCII bytes respectively. Rendered canvases are capped at 65,536 cells,
independent of the caller's maximum width. The library performs no I/O.
Expand Down
29 changes: 28 additions & 1 deletion codex-rs/mermaid/src/families_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ fn unicode_labels_and_later_declarations() {
}

#[test]
fn class_relationship_endpoints() {
fn graph_relationship_endpoints() {
for (operator, source_tip, target_tip, dashed) in [
("<|--", '◁', '─', false),
("*--", '◆', '─', false),
Expand Down Expand Up @@ -192,6 +192,33 @@ fn class_relationship_endpoints() {
assert!(ports[1].contains("(many)"));
assert_eq!(output.contains('┆'), dashed);
}
for (operator, source_tip, target_tip, dashed) in [
("---", '─', '─', false),
("-.->", '─', '◄', true),
("-.-", '─', '─', true),
("<-->", '◄', '◄', false),
("<-.->", '◄', '◄', true),
] {
let output = render(
&format!("flowchart; A{operator}|uses|B"),
/*max_width*/ 100,
)
.unwrap();
let ports = output
.lines()
.filter(|line| line.contains('├'))
.collect::<Vec<_>>();
assert!(
ports[0].contains(&format!("├{source_tip}")),
"{operator}: {output}"
);
assert!(
ports[1].contains(&format!("├{target_tip}")),
"{operator}: {output}"
);
assert!(ports[0].contains("uses"));
assert_eq!(output.contains('┆'), dashed);
}
for source in [
"classDiagram\nclass A; A --> B : {ok}",
"classDiagram\nclass A; class B {\n+id\n}",
Expand Down
95 changes: 78 additions & 17 deletions codex-rs/mermaid/src/parse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Strict parser for a small flowchart grammar; every non-comment byte must be consumed.
//! Node groups expand into explicit edges within the graph edge cap.

use super::Direction;
use super::Edge;
Expand All @@ -13,35 +14,83 @@ use unicode_width::UnicodeWidthStr;

pub(super) fn parse(header: &str, body: &[&str]) -> Result<Graph, RenderError> {
let tokens = header.split_whitespace().collect::<Vec<_>>();
let ["flowchart" | "graph", direction] = tokens.as_slice() else {
return Err(RenderError::Unsupported);
let direction = match tokens.as_slice() {
["flowchart" | "graph"] => Direction::Down,
["flowchart" | "graph", direction] => Direction::parse(direction)?,
_ => return Err(RenderError::Unsupported),
};
let mut graph = Graph {
direction: Direction::parse(direction)?,
direction,
..Graph::default()
};
for statement in body {
let mut rest = *statement;
let mut from = node(&mut rest, &mut graph)?;
let mut from = nodes(&mut rest, &mut graph)?;
while !rest.trim_start().is_empty() {
rest = rest
.trim_start()
.strip_prefix("-->")
.ok_or(RenderError::Unsupported)?;
rest = rest.trim_start();
let label = if let Some(after) = rest.strip_prefix('|') {
let (label, remaining) = delimited_label(after, "|")?;
let label = flowchart_label(label)?;
rest = remaining;
label.to_owned()
let mut label = String::new();
let (source_tip, target_tip, dashed) = if let Some((after, source, target, dashed)) = [
("<-.->", '◄', '◄', true),
("<-->", '◄', '◄', false),
("-.->", '─', '◄', true),
("-.-", '─', '─', true),
("-->", '─', '◄', false),
("---", '─', '─', false),
]
.into_iter()
.find_map(|(token, source, target, dashed)| {
rest.strip_prefix(token)
.map(|after| (after, source, target, dashed))
}) {
// Circle/cross tips are unsupported; without a space they are not node IDs.
if target == '─' && after.starts_with(['o', 'x']) {
return Err(RenderError::Unsupported);
}
rest = after.trim_start();
if let Some(after) = rest.strip_prefix('|') {
let (text, remaining) = delimited_label(after, "|")?;
label = flowchart_label(text)?.to_owned();
rest = remaining;
}
(source, target, dashed)
} else {
String::new()
let (after, stem, dashed) = if let Some(after) = rest.strip_prefix("--") {
(after, "--", false)
} else if let Some(after) = rest.strip_prefix("-.") {
(after, ".-", true)
} else {
return Err(RenderError::Unsupported);
};
// Spaced labels keep endpoint markers out of the text. Stop at the first
// closing stem instead of swallowing an unsupported edge and its target.
let (text, remaining) = after.split_once(stem).ok_or(RenderError::Unsupported)?;
if !text.starts_with(char::is_whitespace) || !text.ends_with(char::is_whitespace) {
return Err(RenderError::Unsupported);
}
rest = remaining
.strip_prefix('>')
.ok_or(RenderError::Unsupported)?;
label = flowchart_label(text.trim())?.to_owned();
('─', '◄', dashed)
};
let to = node(&mut rest, &mut graph)?;
if graph.edges.len() == MAX_EDGES {
let to = nodes(&mut rest, &mut graph)?;
// Check the Cartesian expansion before allocating edges, including repeated IDs.
if from.len() * to.len() > MAX_EDGES - graph.edges.len() {
return Err(RenderError::Limit);
}
graph.edges.push(Edge::directed(from, to, label));
for &from in &from {
for &to in &to {
graph.edges.push(Edge {
from,
to,
label: label.clone(),
target_label: String::new(),
source_tip,
target_tip,
dashed,
});
}
}
from = to;
}
}
Expand All @@ -51,6 +100,18 @@ pub(super) fn parse(header: &str, body: &[&str]) -> Result<Graph, RenderError> {
Ok(graph)
}

fn nodes(rest: &mut &str, graph: &mut Graph) -> Result<Vec<usize>, RenderError> {
let mut nodes = vec![node(rest, graph)?];
while let Some(after) = rest.trim_start().strip_prefix('&') {
if nodes.len() == MAX_EDGES {
return Err(RenderError::Limit);
}
*rest = after;
nodes.push(node(rest, graph)?);
}
Ok(nodes)
}

fn node(rest: &mut &str, graph: &mut Graph) -> Result<usize, RenderError> {
let id = identifier(rest)?;
// Reserved constructs must not be interpreted as ordinary node declarations.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,106 @@
---
source: mermaid/src/tests.rs
assertion_line: 35
expression: "cases.join(\"\\n\\n\")"
---
TD
╭─────────╮
│ 请求 │
│ ├─────┐
│ ├◄────╪───┐
╰─────────╯ │ │
│ │
│ │
┌─────────┐ │ │
│ Work │ │ │
│ ├◄────┘ │
│ ├───────┐ │
└─────────┘ │ │
│ │
│ │
┌─────────┐ │ │
│ ◇ Done? │ │ │
│ ├◄──────┘ │
│ ├─────────┘
│ ├──go───┐
│ ├──go───╪─┐
│ ├◄──────╪─╪───┐
│ ├───────╪─╪───╪─┐
│ ├◄┄┄┄┄┄┄╪┄╪┄┄┄╪┄╪┄┄┄┐
╰─────────╯ │ │ │ │ ┆
│ │ │ │ ┆
│ │ │ │ ┆
┌─────────┐ │ │ │ │ ┆
│ Work │ │ │ │ │ ┆
│ ├◄──────┘ │ │ │ ┆
│ ├─┄no┄┄┄┄┄╪┄┐ │ │ ┆
│ ├─────────╪─╪─╪─┘ ┆
│ ├─┄┄┄┄┄┄┄┄╪┄╪┄╪┄┄┄┐ ┆
│ ├◄┄┄┄┄┄┄┄┄╪┄╪┄╪┄┄┄╪┄┘
└─────────┘ │ ┆ │ ┆
│ ┆ │ ┆
│ ┆ │ ┆
┌─────────┐ │ ┆ │ ┆
│ ◇ Done? │ │ ┆ │ ┆
│ ├◄────────┘ ┆ │ ┆
│ ├◄┄┄┄┄┄┄┄┄┄┄┘ │ ┆
│ ├◄────────────┘ ┆
│ ├─┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
└─────────┘

BT
┌─────────┐
│ ◇ Done? │
│ ├◄──────┐
│ ├───────╪─┐
└─────────┘ │ │
│ │
│ │
┌─────────┐ │ │
│ Work │ │ │
│ ├◄────┐ │ │
│ ├─────╪─┘ │
└─────────┘ │ │
│ │
│ │
╭─────────╮ │ │
│ 请求 │ │ │
│ ├─────┘ │
│ ├◄────────┘
│ ├◄────────┐
│ ├◄┄┄┄┄┄┄┄┄╪┄┐
│ ├◄────────╪─╪─┐
│ ├─┄┄┄┄┄┄┄┄╪┄╪┄╪┄┄┄┐
└─────────┘ │ ┆ │ ┆
│ ┆ │ ┆
│ ┆ │ ┆
┌─────────┐ │ ┆ │ ┆
│ Work │ │ ┆ │ ┆
│ ├◄──────┐ │ ┆ │ ┆
│ ├─┄no┄┄┄╪┄╪┄┘ │ ┆
│ ├───────╪─╪───╪─┐ ┆
│ ├─┄┄┄┄┄┄╪┄╪┄┄┄╪┄╪┄┘
│ ├◄┄┄┄┄┄┄╪┄╪┄┄┄╪┄╪┄┄┄┐
└─────────┘ │ │ │ │ ┆
│ │ │ │ ┆
│ │ │ │ ┆
╭─────────╮ │ │ │ │ ┆
│ 请求 │ │ │ │ │ ┆
│ ├──go───┘ │ │ │ ┆
│ ├──go─────┘ │ │ ┆
│ ├◄────────────┘ │ ┆
│ ├───────────────┘ ┆
│ ├◄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
╰─────────╯

LR
╭──────╮ ┌──────┐ ┌─────────┐
│ 请求 │ │ Work │ │ ◇ Done? │
╰┬─┬───╯ └┬─┬───┘ └┬─┬──────┘
│ ▲ ▲ │ ▲ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
└─╪───────┘ │ │ │
│ │ │ │
│ └───────┘ │
│ │
└───────────────────┘
╭────────────────────╮ ┌────────────────────┐ ┌────────────────┐
│ 请求 │ │ Work │ │ ◇ Done? │
╰┬───┬───┬───┬───┬───╯ └┬───┬───┬───┬───┬───┘ └┬───┬───┬───┬───┘
│ │ ▲ │ ▲ ▲ │ │ │ ▲ ▲ ▲ ▲ │
│go │go │ │ ┆ │ ┆no │ ┆ ┆ │ ┆ │ ┆
│ │ │ │ ┆ │ ┆ │ ┆ ┆ │ ┆ │ ┆
│ │ │ │ ┆ │ ┆ │ ┆ ┆ │ ┆ │ ┆
└───╪───╪───╪───╪───────┘ ┆ │ ┆ ┆ │ ┆ │ ┆
│ │ │ ┆ ┆ │ ┆ ┆ │ ┆ │ ┆
└───╪───╪───╪───────────╪───╪───╪───╪───────┘ ┆ │ ┆
│ │ ┆ ┆ │ ┆ ┆ ┆ │ ┆
│ │ ┆ └┄┄┄╪┄┄┄╪┄┄┄╪┄┄┄┄┄┄┄┄┄┄┄┘ │ ┆
│ │ ┆ │ ┆ ┆ │ ┆
└───╪───╪───────────────╪───╪───╪───────────────┘ ┆
│ ┆ │ ┆ ┆ ┆
└───╪───────────────┘ ┆ ┆ ┆
┆ ┆ ┆ ┆
┆ └┄┄┄╪┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
┆ ┆
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘

RL
┌─────────┐ ┌──────┐ ╭──────╮
│ ◇ Done? │ │ Work │ │ 请求 │
└┬─┬──────┘ └┬─┬───┘ ╰┬─┬───╯
▲ │ ▲ │ │ ▲
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ └─╪───────┘ │
│ │ │ │
└─╪────────────┘ │
│ │
└──────────────────────┘
┌────────────────┐ ┌────────────────────┐ ╭────────────────────╮
│ ◇ Done? │ │ Work │ │ 请求 │
└┬───┬───┬───┬───┘ └┬───┬───┬───┬───┬───┘ ╰┬───┬───┬───┬───┬───╯
▲ ▲ ▲ │ ▲ │ │ │ ▲ │ │ ▲ │ ▲
│ ┆ │ ┆ │ ┆no │ ┆ ┆ │go │go │ │ ┆
│ ┆ │ ┆ │ ┆ │ ┆ ┆ │ │ │ │ ┆
│ ┆ │ ┆ │ ┆ │ ┆ ┆ │ │ │ │ ┆
│ ┆ │ ┆ └───╪───╪───╪───╪───────┘ │ │ │ ┆
│ ┆ │ ┆ ┆ │ ┆ ┆ │ │ │ ┆
└───╪───╪───╪───────────╪───╪───╪───╪───────────┘ │ │ ┆
┆ │ ┆ ┆ │ ┆ ┆ │ │ ┆
└┄┄┄╪┄┄┄╪┄┄┄┄┄┄┄┄┄┄┄┘ │ ┆ ┆ │ │ ┆
│ ┆ │ ┆ ┆ │ │ ┆
└───╪───────────────╪───╪───╪───────────────┘ │ ┆
┆ │ ┆ ┆ │ ┆
┆ └───╪───╪───────────────────┘ ┆
┆ ┆ ┆ ┆
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ ┆ ┆
┆ ┆
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
Loading
Loading