Files
codex/codex-rs/mermaid/src/state.rs
T
Eric Traut 6e1ab4d294 Fix Mermaid shape, relationship, and state description parsing (#48489)
## What changed

- Reject unsupported flowchart shapes and Markdown strings so the terminal renderer preserves the source with an unsupported-feature notice instead of misrendering it. Keep ordinary quoted labels containing shape-like punctuation valid.
- Preserve class relationship targets beginning with `o`, such as `A --orange`, instead of consuming the first letter as an aggregation marker.
- Accumulate state aliases and descriptions in source order: use the first as the title and subsequent entries as body rows, preserving state identity and limiting body rows to 16.

## Testing

Add parser regressions for these cases and description limits, update class and state snapshots, and extend TUI snapshots to verify source preservation for unsupported flowchart syntax.

GitOrigin-RevId: c44d954312c77229a3752a0d9ada14e34a43a376
2026-09-26 17:26:07 +00:00

120 lines
4.0 KiB
Rust

//! Flat state machines, with distinct initial/final pseudostates and labeled transitions.
//! Aliases and descriptions accumulate in source order: first the title, then body rows.
use super::Direction;
use super::Edge;
use super::Graph;
use super::MAX_EDGES;
use super::RenderError;
use super::parse::check_label;
use super::parse::identifier;
pub(super) fn parse(body: &[&str]) -> Result<Graph, RenderError> {
let mut graph = Graph::default();
let mut direction = false;
for &line in body {
if matches!(line, "state" | "direction" | "note" | "end" | "hide") {
return Err(RenderError::Unsupported);
}
if let Some(value) = line.strip_prefix("direction ") {
if direction {
return Err(RenderError::Unsupported);
}
graph.direction = Direction::parse(value.trim())?;
direction = true;
continue;
}
let mut rest = line;
if let Some(after) = rest.strip_prefix("state \"") {
let (label, after) = after.split_once('"').ok_or(RenderError::Unsupported)?;
check_label(label)?;
rest = after
.trim_start()
.strip_prefix("as ")
.ok_or(RenderError::Unsupported)?;
let index = graph.node(identifier(&mut rest)?)?;
if !rest.trim().is_empty() {
return Err(RenderError::Unsupported);
}
add_description(&mut graph.nodes[index], label)?;
continue;
}
let from = if let Some(after) = rest.strip_prefix("[*]") {
rest = after;
let index = graph.node("[initial]")?;
graph.nodes[index].label = "● initial".to_owned();
index
} else {
let id = identifier(&mut rest)?;
if (id.eq_ignore_ascii_case("accTitle") || id.eq_ignore_ascii_case("accDescr"))
&& rest.trim_start().starts_with(':')
{
return Err(RenderError::Unsupported);
}
graph.node(id)?
};
rest = rest.trim();
if rest.is_empty() && graph.nodes[from].id != "[initial]" {
continue;
}
if let Some(description) = rest
.strip_prefix(':')
.filter(|text| !text.starts_with("::"))
{
if graph.nodes[from].id == "[initial]" {
return Err(RenderError::Unsupported);
}
let description = description.trim();
check_label(description)?;
add_description(&mut graph.nodes[from], description)?;
continue;
}
rest = rest
.strip_prefix("-->")
.ok_or(RenderError::Unsupported)?
.trim_start();
let to = if let Some(after) = rest.strip_prefix("[*]") {
rest = after;
let index = graph.node("[final]")?;
graph.nodes[index].label = "◎ final".to_owned();
index
} else {
graph.node(identifier(&mut rest)?)?
};
let label = if let Some(label) = rest
.trim()
.strip_prefix(':')
.filter(|text| !text.starts_with("::"))
{
let label = label.trim();
check_label(label)?;
label.to_owned()
} else if rest.trim().is_empty() {
String::new()
} else {
return Err(RenderError::Unsupported);
};
if graph.edges.len() == MAX_EDGES {
return Err(RenderError::Limit);
}
graph.edges.push(Edge::directed(from, to, label));
}
if graph.nodes.is_empty() {
return Err(RenderError::Unsupported);
}
Ok(graph)
}
fn add_description(node: &mut super::Node, description: &str) -> Result<(), RenderError> {
if !node.declared {
node.label = description.to_owned();
node.declared = true;
} else {
if node.members.len() == 16 {
return Err(RenderError::Limit);
}
node.members.push(description.to_owned());
}
Ok(())
}