Skip to main content

rust_igraph/algorithms/io/
dot.rs

1//! DOT (Graphviz) reader / writer (ALGO-IO-006, ALGO-IO-013).
2//!
3//! Reads and writes graphs in the DOT language used by the Graphviz suite
4//! of tools. The reader handles the structural subset: `graph`/`digraph`
5//! keyword, node declarations, and edge statements (`--` / `->`).
6//! Attribute blocks (`[key=value, ...]`) on nodes and edges are parsed and
7//! stored via the [`Graph`] attribute system. Graph-level attribute
8//! statements (`graph [key=value]`) are stored as graph attributes.
9//!
10//! ```text
11//! graph {
12//!   graph [label="My Graph"];
13//!   0 [color="red"];
14//!   0 -- 1 [weight=1.5];
15//!   1 -- 2;
16//! }
17//! ```
18//!
19//! For directed graphs:
20//! ```text
21//! digraph {
22//!   0 -> 1 [weight=2.0];
23//!   1 -> 2;
24//! }
25//! ```
26//!
27//! Counterparts of `igraph_read_graph_dot` (new) and `igraph_write_graph_dot`.
28
29use std::collections::HashMap;
30use std::io::{BufRead, BufReader, Read, Write};
31
32use crate::core::attributes::AttributeValue;
33use crate::core::{Graph, IgraphError, IgraphResult};
34
35/// Result of parsing a DOT file.
36pub struct DotGraph {
37    /// The parsed graph.
38    pub graph: Graph,
39    /// Node labels in vertex-index order. Numeric-only labels are preserved
40    /// as strings (e.g. `"0"`, `"1"`).
41    pub labels: Vec<String>,
42}
43
44/// Read a graph from DOT (Graphviz) format.
45///
46/// Parses `graph { ... }` (undirected) or `digraph { ... }` (directed)
47/// blocks. Recognizes edge statements using `--` or `->` and node
48/// declarations. Attribute blocks (`[key=value, ...]`) on nodes and edges
49/// are parsed and stored via the [`Graph`] attribute system. Subgraphs are
50/// treated as flat (nodes are merged into the top-level graph).
51///
52/// Node identifiers may be bare identifiers, numbers, or double-quoted
53/// strings. They are assigned internal vertex indices in first-appearance
54/// order and returned in [`DotGraph::labels`].
55///
56/// # Errors
57///
58/// Returns an error if the input lacks a `graph`/`digraph` keyword or
59/// contains `--` edges in a `digraph` (or `->` in a `graph`).
60///
61/// # Examples
62///
63/// ```
64/// use rust_igraph::{read_dot, AttributeValue};
65///
66/// let dot = r#"graph {
67///   Alice [color="red"];
68///   Alice -- Bob [weight=1.5];
69///   Bob -- Carol;
70/// }"#;
71/// let result = read_dot(dot.as_bytes()).unwrap();
72/// assert_eq!(result.graph.vcount(), 3);
73/// assert_eq!(result.graph.ecount(), 2);
74/// assert!(!result.graph.is_directed());
75/// assert_eq!(result.labels, vec!["Alice", "Bob", "Carol"]);
76/// assert_eq!(
77///     result.graph.vertex_attribute("color", 0).and_then(|v| v.as_str()),
78///     Some("red"),
79/// );
80/// assert_eq!(
81///     result.graph.edge_attribute("weight", 0).and_then(|v| v.as_f64()),
82///     Some(1.5),
83/// );
84/// ```
85#[allow(clippy::too_many_lines)]
86pub fn read_dot<R: Read>(input: R) -> IgraphResult<DotGraph> {
87    let reader = BufReader::new(input);
88    let mut lines_buf: Vec<String> = Vec::new();
89    for line in reader.lines() {
90        lines_buf.push(line?);
91    }
92    let content = lines_buf.join("\n");
93
94    let content = strip_comments(&content);
95    let directed = detect_directed(&content)?;
96
97    let body_start = content.find('{').ok_or_else(|| IgraphError::Parse {
98        line: 0,
99        message: "no opening '{' found in DOT input".to_owned(),
100    })? + 1;
101    let body_end = content.rfind('}').ok_or_else(|| IgraphError::Parse {
102        line: 0,
103        message: "no closing '}' found in DOT input".to_owned(),
104    })?;
105    let body = &content[body_start..body_end];
106
107    let mut node_ids: Vec<String> = Vec::new();
108    let mut node_map: HashMap<String, u32> = HashMap::new();
109    let mut edges: Vec<(u32, u32)> = Vec::new();
110    let mut node_attr_list: Vec<Vec<(String, AttributeValue)>> = Vec::new();
111    let mut edge_attr_list: Vec<Vec<(String, AttributeValue)>> = Vec::new();
112    let mut graph_attrs: Vec<(String, AttributeValue)> = Vec::new();
113
114    let edge_op = if directed { "->" } else { "--" };
115
116    let ensure_node = |name: &str,
117                       ids: &mut Vec<String>,
118                       map: &mut HashMap<String, u32>,
119                       attrs: &mut Vec<Vec<(String, AttributeValue)>>|
120     -> IgraphResult<u32> {
121        if let Some(&idx) = map.get(name) {
122            return Ok(idx);
123        }
124        let idx = u32::try_from(ids.len())
125            .map_err(|_| IgraphError::InvalidArgument("too many nodes for u32".to_owned()))?;
126        map.insert(name.to_owned(), idx);
127        ids.push(name.to_owned());
128        attrs.push(Vec::new());
129        Ok(idx)
130    };
131
132    for raw_stmt in body.split(';') {
133        let stmt = raw_stmt.trim();
134        if stmt.is_empty() {
135            continue;
136        }
137
138        let lower = stmt.to_ascii_lowercase();
139
140        // Graph-level attribute statement: graph [key=value]
141        if lower.starts_with("graph ") && !stmt.contains(edge_op) {
142            if let Some(attrs) = extract_attr_block(stmt) {
143                let parsed = parse_attr_pairs(&attrs);
144                graph_attrs.extend(parsed);
145            }
146            continue;
147        }
148
149        // Skip global node/edge/subgraph attribute statements
150        if (lower.starts_with("node ")
151            || lower.starts_with("edge ")
152            || lower.starts_with("subgraph "))
153            && !stmt.contains(edge_op)
154        {
155            continue;
156        }
157        if lower.starts_with('{') || lower.starts_with('}') {
158            continue;
159        }
160
161        // Edge statement
162        if stmt.contains(edge_op) {
163            let (structural, attrs) = split_attr_block(stmt);
164            let edge_attrs_parsed = attrs.map(|a| parse_attr_pairs(&a)).unwrap_or_default();
165            let parts = split_edge_statement(&structural, edge_op);
166            if parts.len() >= 2 {
167                for pair in parts.windows(2) {
168                    let src_name = clean_node_id(pair[0]);
169                    let tgt_name = clean_node_id(pair[1]);
170                    if src_name.is_empty() || tgt_name.is_empty() {
171                        continue;
172                    }
173                    let src =
174                        ensure_node(&src_name, &mut node_ids, &mut node_map, &mut node_attr_list)?;
175                    let tgt =
176                        ensure_node(&tgt_name, &mut node_ids, &mut node_map, &mut node_attr_list)?;
177                    edges.push((src, tgt));
178                    edge_attr_list.push(edge_attrs_parsed.clone());
179                }
180                continue;
181            }
182        }
183
184        // Node declaration (possibly with attributes)
185        let (structural, attrs) = split_attr_block(stmt);
186        let node_name = clean_node_id(&structural);
187        if !node_name.is_empty() {
188            let idx = ensure_node(
189                &node_name,
190                &mut node_ids,
191                &mut node_map,
192                &mut node_attr_list,
193            )?;
194            if let Some(a) = attrs {
195                let parsed = parse_attr_pairs(&a);
196                node_attr_list[idx as usize].extend(parsed);
197            }
198        }
199    }
200
201    // Build graph
202    let n = u32::try_from(node_ids.len())
203        .map_err(|_| IgraphError::InvalidArgument("too many nodes for u32".to_owned()))?;
204    let mut graph = Graph::new(n, directed)?;
205    for (src, tgt) in &edges {
206        graph.add_edge(*src, *tgt)?;
207    }
208
209    // Apply vertex attributes
210    apply_vertex_attrs(&mut graph, &node_attr_list)?;
211
212    // Apply edge attributes
213    apply_edge_attrs(&mut graph, &edge_attr_list)?;
214
215    // Apply graph attributes
216    for (key, val) in graph_attrs {
217        graph.set_graph_attribute(key, val);
218    }
219
220    Ok(DotGraph {
221        graph,
222        labels: node_ids,
223    })
224}
225
226/// Apply parsed vertex attributes to the graph.
227fn apply_vertex_attrs(
228    graph: &mut Graph,
229    node_attr_list: &[Vec<(String, AttributeValue)>],
230) -> IgraphResult<()> {
231    let mut attr_names: HashMap<String, Vec<AttributeValue>> = HashMap::new();
232    let n = graph.vcount() as usize;
233
234    for (vid, attrs) in node_attr_list.iter().enumerate() {
235        for (key, val) in attrs {
236            let col = attr_names.entry(key.clone()).or_insert_with(|| {
237                let default = val.default_for_same_type();
238                vec![default; n]
239            });
240            if vid < col.len() {
241                col[vid] = val.clone();
242            }
243        }
244    }
245
246    for (name, values) in attr_names {
247        graph.set_vertex_attribute_all(name, values)?;
248    }
249    Ok(())
250}
251
252/// Apply parsed edge attributes to the graph.
253fn apply_edge_attrs(
254    graph: &mut Graph,
255    edge_attr_list: &[Vec<(String, AttributeValue)>],
256) -> IgraphResult<()> {
257    let mut attr_names: HashMap<String, Vec<AttributeValue>> = HashMap::new();
258    let m = graph.ecount();
259
260    for (eid, attrs) in edge_attr_list.iter().enumerate() {
261        for (key, val) in attrs {
262            let col = attr_names.entry(key.clone()).or_insert_with(|| {
263                let default = val.default_for_same_type();
264                vec![default; m]
265            });
266            if eid < col.len() {
267                col[eid] = val.clone();
268            }
269        }
270    }
271
272    for (name, values) in attr_names {
273        graph.set_edge_attribute_all(name, values)?;
274    }
275    Ok(())
276}
277
278/// Extract the content inside `[...]` from a statement string, if present.
279fn extract_attr_block(stmt: &str) -> Option<String> {
280    let open = stmt.find('[')?;
281    let close = stmt.rfind(']')?;
282    if close > open {
283        Some(stmt[open + 1..close].to_owned())
284    } else {
285        None
286    }
287}
288
289/// Split a statement into (structural part, optional attr block content).
290fn split_attr_block(stmt: &str) -> (String, Option<String>) {
291    if let Some(open) = stmt.find('[') {
292        let structural = stmt[..open].trim().to_owned();
293        let close = stmt.rfind(']').unwrap_or(stmt.len());
294        let attrs = stmt[open + 1..close].to_owned();
295        (structural, Some(attrs))
296    } else {
297        (stmt.to_owned(), None)
298    }
299}
300
301/// Parse `key=value, key=value, ...` pairs from attribute block content.
302fn parse_attr_pairs(content: &str) -> Vec<(String, AttributeValue)> {
303    let mut result = Vec::new();
304    let mut remaining = content.trim();
305
306    while !remaining.is_empty() {
307        // Find key
308        let Some(eq_pos) = remaining.find('=') else {
309            break;
310        };
311        let key = remaining[..eq_pos].trim();
312        remaining = remaining[eq_pos + 1..].trim();
313
314        // Parse value
315        let (value_str, rest) = if remaining.starts_with('"') {
316            parse_quoted_value(remaining)
317        } else {
318            parse_unquoted_value(remaining)
319        };
320
321        if !key.is_empty() {
322            result.push((key.to_owned(), dot_value_to_attribute(&value_str)));
323        }
324
325        remaining = rest.trim();
326        // Skip comma or semicolon separator
327        if remaining.starts_with(',') || remaining.starts_with(';') {
328            remaining = remaining[1..].trim();
329        }
330    }
331
332    result
333}
334
335/// Parse a double-quoted value, handling escapes.
336fn parse_quoted_value(s: &str) -> (String, &str) {
337    let bytes = s.as_bytes();
338    let mut i = 1; // skip opening quote
339    let mut value = String::new();
340    while i < bytes.len() {
341        if bytes[i] == b'\\' && i + 1 < bytes.len() {
342            match bytes[i + 1] {
343                b'"' => value.push('"'),
344                b'\\' => value.push('\\'),
345                b'n' => value.push('\n'),
346                other => {
347                    value.push('\\');
348                    value.push(other as char);
349                }
350            }
351            i += 2;
352        } else if bytes[i] == b'"' {
353            return (value, &s[i + 1..]);
354        } else {
355            value.push(bytes[i] as char);
356            i += 1;
357        }
358    }
359    (value, "")
360}
361
362/// Parse an unquoted value (terminated by comma, semicolon, or end).
363fn parse_unquoted_value(s: &str) -> (String, &str) {
364    let end = s.find([',', ';', ']']).unwrap_or(s.len());
365    let value = s[..end].trim().to_owned();
366    (value, &s[end..])
367}
368
369/// Convert a DOT attribute value string to an `AttributeValue`.
370fn dot_value_to_attribute(s: &str) -> AttributeValue {
371    // Try boolean
372    match s.to_ascii_lowercase().as_str() {
373        "true" => return AttributeValue::Boolean(true),
374        "false" => return AttributeValue::Boolean(false),
375        _ => {}
376    }
377    // Try numeric
378    if let Ok(v) = s.parse::<f64>() {
379        return AttributeValue::Numeric(v);
380    }
381    AttributeValue::String(s.to_owned())
382}
383
384/// Strip C-style (`/* ... */`) and C++-style (`// ...`) comments.
385fn strip_comments(s: &str) -> String {
386    let mut out = String::with_capacity(s.len());
387    let bytes = s.as_bytes();
388    let len = bytes.len();
389    let mut i = 0;
390    while i < len {
391        if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
392            i += 2;
393            while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
394                i += 1;
395            }
396            i += 2;
397        } else if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'/' {
398            while i < len && bytes[i] != b'\n' {
399                i += 1;
400            }
401        } else if bytes[i] == b'"' {
402            out.push('"');
403            i += 1;
404            while i < len && bytes[i] != b'"' {
405                if bytes[i] == b'\\' && i + 1 < len {
406                    out.push(bytes[i] as char);
407                    i += 1;
408                }
409                out.push(bytes[i] as char);
410                i += 1;
411            }
412            if i < len {
413                out.push('"');
414                i += 1;
415            }
416        } else {
417            out.push(bytes[i] as char);
418            i += 1;
419        }
420    }
421    out
422}
423
424/// Detect whether the DOT file is `digraph` (directed) or `graph` (undirected).
425fn detect_directed(content: &str) -> IgraphResult<bool> {
426    let lower = content.to_ascii_lowercase();
427    let di_pos = lower.find("digraph");
428    let g_pos = lower.find("graph");
429    match (di_pos, g_pos) {
430        (Some(dp), Some(gp)) => {
431            if dp <= gp {
432                Ok(true)
433            } else {
434                Ok(false)
435            }
436        }
437        (Some(_), None) => Ok(true),
438        (None, Some(_)) => Ok(false),
439        (None, None) => Err(IgraphError::Parse {
440            line: 0,
441            message: "no 'graph' or 'digraph' keyword found".to_owned(),
442        }),
443    }
444}
445
446/// Split an edge statement by the edge operator, handling chains like
447/// `a -> b -> c`.
448fn split_edge_statement<'a>(stmt: &'a str, op: &str) -> Vec<&'a str> {
449    stmt.split(op).map(str::trim).collect()
450}
451
452/// Clean up a node identifier: trim whitespace and remove surrounding
453/// double quotes.
454fn clean_node_id(raw: &str) -> String {
455    let mut s = raw.trim();
456    // Strip trailing braces (subgraph artifacts)
457    if let Some(brace) = s.find('{') {
458        s = s[..brace].trim();
459    }
460    if let Some(brace) = s.find('}') {
461        s = s[..brace].trim();
462    }
463    // Remove surrounding quotes
464    if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
465        let inner = &s[1..s.len() - 1];
466        return inner.replace("\\\"", "\"").replace("\\\\", "\\");
467    }
468    s.to_owned()
469}
470
471// ─── Writer ──────────────────────────────────────────────────────────
472
473/// Write a graph in DOT (Graphviz) format.
474///
475/// If `labels` is provided, uses them as vertex labels; otherwise uses
476/// numeric ids. Isolated vertices are listed explicitly. Vertex and edge
477/// attributes stored on the [`Graph`] are emitted as DOT attribute blocks
478/// `[key=value, ...]`.
479///
480/// # Examples
481///
482/// ```
483/// use rust_igraph::{Graph, write_dot, AttributeValue};
484///
485/// let mut g = Graph::with_vertices(3);
486/// g.add_edge(0, 1).unwrap();
487/// g.add_edge(1, 2).unwrap();
488/// g.set_vertex_attribute("color", 0, AttributeValue::String("red".into())).unwrap();
489/// g.set_edge_attribute("weight", 0, AttributeValue::Numeric(1.5)).unwrap();
490///
491/// let mut buf = Vec::new();
492/// write_dot(&g, None, &mut buf).unwrap();
493/// let s = String::from_utf8(buf).unwrap();
494/// assert!(s.contains("graph {"));
495/// assert!(s.contains("[color=\"red\"]"));
496/// assert!(s.contains("[weight=1.5]"));
497/// ```
498#[allow(clippy::too_many_lines)]
499pub fn write_dot<W: Write>(
500    graph: &Graph,
501    labels: Option<&[String]>,
502    writer: &mut W,
503) -> IgraphResult<()> {
504    if let Some(l) = labels {
505        if l.len() != graph.vcount() as usize {
506            return Err(IgraphError::InvalidArgument(format!(
507                "labels length {} does not match vcount {}",
508                l.len(),
509                graph.vcount()
510            )));
511        }
512    }
513
514    let edge_op = if graph.is_directed() { "->" } else { "--" };
515    let graph_type = if graph.is_directed() {
516        "digraph"
517    } else {
518        "graph"
519    };
520
521    writeln!(writer, "{graph_type} {{")?;
522
523    // Write graph-level attributes
524    write_graph_attrs(graph, writer)?;
525
526    // Collect vertex/edge attribute names
527    let v_attr_names = graph.vertex_attribute_names();
528    let e_attr_names = graph.edge_attribute_names();
529
530    // Track which vertices appear in edges
531    let mut has_edge = vec![false; graph.vcount() as usize];
532
533    for eid in 0..graph.ecount() {
534        let eid_u32 = u32::try_from(eid)
535            .map_err(|_| IgraphError::InvalidArgument("edge id overflow".to_owned()))?;
536        let (src, tgt) = graph.edge(eid_u32)?;
537        has_edge[src as usize] = true;
538        has_edge[tgt as usize] = true;
539
540        let src_label = vertex_label(src, labels);
541        let tgt_label = vertex_label(tgt, labels);
542
543        write!(writer, "  {src_label} {edge_op} {tgt_label}")?;
544        write_edge_attr_block(graph, eid_u32, &e_attr_names, writer)?;
545        writeln!(writer, ";")?;
546    }
547
548    // Emit all vertices (with attributes if any, or isolated ones without)
549    for v in 0..graph.vcount() {
550        let has_attrs = v_attr_names.iter().any(|name| {
551            graph
552                .vertex_attribute(name, v)
553                .is_some_and(|val| !is_default_attr(val))
554        });
555
556        if has_attrs || !has_edge[v as usize] {
557            let lbl = vertex_label(v, labels);
558            write!(writer, "  {lbl}")?;
559            write_vertex_attr_block(graph, v, &v_attr_names, writer)?;
560            writeln!(writer, ";")?;
561        }
562    }
563
564    writeln!(writer, "}}")?;
565
566    Ok(())
567}
568
569/// Write graph-level attributes as `graph [key=value, ...]`.
570fn write_graph_attrs<W: Write>(graph: &Graph, writer: &mut W) -> IgraphResult<()> {
571    let names = graph.graph_attribute_names();
572    if names.is_empty() {
573        return Ok(());
574    }
575
576    write!(writer, "  graph [")?;
577    let mut first = true;
578    for name in &names {
579        if let Some(val) = graph.graph_attribute(name) {
580            if !first {
581                write!(writer, ", ")?;
582            }
583            write_attr_pair(name, val, writer)?;
584            first = false;
585        }
586    }
587    writeln!(writer, "];")?;
588    Ok(())
589}
590
591/// Write a vertex attribute block `[key=value, ...]` for vertex `v`.
592fn write_vertex_attr_block<W: Write>(
593    graph: &Graph,
594    v: u32,
595    attr_names: &[&str],
596    writer: &mut W,
597) -> IgraphResult<()> {
598    let mut pairs: Vec<(&str, &AttributeValue)> = Vec::new();
599    for name in attr_names {
600        if let Some(val) = graph.vertex_attribute(name, v) {
601            if !is_default_attr(val) {
602                pairs.push((name, val));
603            }
604        }
605    }
606
607    if pairs.is_empty() {
608        return Ok(());
609    }
610
611    write!(writer, " [")?;
612    for (i, (name, val)) in pairs.iter().enumerate() {
613        if i > 0 {
614            write!(writer, ", ")?;
615        }
616        write_attr_pair(name, val, writer)?;
617    }
618    write!(writer, "]")?;
619    Ok(())
620}
621
622/// Write an edge attribute block `[key=value, ...]` for edge `eid`.
623fn write_edge_attr_block<W: Write>(
624    graph: &Graph,
625    eid: u32,
626    attr_names: &[&str],
627    writer: &mut W,
628) -> IgraphResult<()> {
629    let mut pairs: Vec<(&str, &AttributeValue)> = Vec::new();
630    for name in attr_names {
631        if let Some(val) = graph.edge_attribute(name, eid) {
632            if !is_default_attr(val) {
633                pairs.push((name, val));
634            }
635        }
636    }
637
638    if pairs.is_empty() {
639        return Ok(());
640    }
641
642    write!(writer, " [")?;
643    for (i, (name, val)) in pairs.iter().enumerate() {
644        if i > 0 {
645            write!(writer, ", ")?;
646        }
647        write_attr_pair(name, val, writer)?;
648    }
649    write!(writer, "]")?;
650    Ok(())
651}
652
653/// Check if an attribute value is a "default" (NaN, empty string, false)
654/// that should be omitted from output.
655fn is_default_attr(val: &AttributeValue) -> bool {
656    match val {
657        AttributeValue::Numeric(v) => v.is_nan(),
658        AttributeValue::String(s) => s.is_empty(),
659        AttributeValue::Boolean(_) => false,
660    }
661}
662
663/// Write a single `key=value` pair in DOT format.
664fn write_attr_pair<W: Write>(name: &str, val: &AttributeValue, writer: &mut W) -> IgraphResult<()> {
665    match val {
666        AttributeValue::Numeric(v) => {
667            write!(writer, "{name}={v}")?;
668        }
669        AttributeValue::Boolean(b) => {
670            write!(writer, "{name}={b}")?;
671        }
672        AttributeValue::String(s) => {
673            let escaped = dot_escape_value(s);
674            write!(writer, "{name}={escaped}")?;
675        }
676    }
677    Ok(())
678}
679
680fn vertex_label(v: u32, labels: Option<&[String]>) -> String {
681    match labels {
682        Some(l) => dot_escape_id(&l[v as usize]),
683        None => v.to_string(),
684    }
685}
686
687/// Escape a string for use as a DOT node identifier.
688fn dot_escape_id(s: &str) -> String {
689    let is_simple = !s.is_empty()
690        && !s.as_bytes()[0].is_ascii_digit()
691        && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
692
693    let reserved = matches!(
694        s.to_ascii_lowercase().as_str(),
695        "graph" | "digraph" | "node" | "edge" | "strict" | "subgraph"
696    );
697
698    if is_simple && !reserved {
699        s.to_owned()
700    } else {
701        dot_quote(s)
702    }
703}
704
705/// Escape a string for use as a DOT attribute value.
706fn dot_escape_value(s: &str) -> String {
707    dot_quote(s)
708}
709
710/// Wrap a string in double quotes with DOT escaping.
711fn dot_quote(s: &str) -> String {
712    let mut out = String::with_capacity(s.len() + 2);
713    out.push('"');
714    for c in s.chars() {
715        match c {
716            '"' => out.push_str("\\\""),
717            '\\' => out.push_str("\\\\"),
718            '\n' => out.push_str("\\n"),
719            _ => out.push(c),
720        }
721    }
722    out.push('"');
723    out
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729
730    #[test]
731    fn test_undirected_basic() {
732        let mut g = Graph::with_vertices(3);
733        g.add_edge(0, 1).unwrap();
734        g.add_edge(1, 2).unwrap();
735
736        let mut buf = Vec::new();
737        write_dot(&g, None, &mut buf).unwrap();
738        let s = String::from_utf8(buf).unwrap();
739        assert!(s.starts_with("graph {\n"));
740        assert!(s.contains("0 -- 1;"));
741        assert!(s.contains("1 -- 2;"));
742        assert!(s.ends_with("}\n"));
743    }
744
745    #[test]
746    fn test_directed_basic() {
747        let mut g = Graph::new(3, true).unwrap();
748        g.add_edge(0, 1).unwrap();
749        g.add_edge(1, 2).unwrap();
750
751        let mut buf = Vec::new();
752        write_dot(&g, None, &mut buf).unwrap();
753        let s = String::from_utf8(buf).unwrap();
754        assert!(s.starts_with("digraph {\n"));
755        assert!(s.contains("0 -> 1;"));
756        assert!(s.contains("1 -> 2;"));
757    }
758
759    #[test]
760    fn test_with_labels() {
761        let mut g = Graph::with_vertices(3);
762        g.add_edge(0, 1).unwrap();
763
764        let labels = vec!["Alice".to_string(), "Bob".to_string(), "Carol".to_string()];
765        let mut buf = Vec::new();
766        write_dot(&g, Some(&labels), &mut buf).unwrap();
767        let s = String::from_utf8(buf).unwrap();
768        assert!(s.contains("Alice -- Bob;"));
769        assert!(s.contains("Carol;"));
770    }
771
772    #[test]
773    fn test_isolated_vertices() {
774        let g = Graph::with_vertices(3);
775
776        let mut buf = Vec::new();
777        write_dot(&g, None, &mut buf).unwrap();
778        let s = String::from_utf8(buf).unwrap();
779        assert!(s.contains("  0;\n"));
780        assert!(s.contains("  1;\n"));
781        assert!(s.contains("  2;\n"));
782    }
783
784    #[test]
785    fn test_empty_graph() {
786        let g = Graph::with_vertices(0);
787
788        let mut buf = Vec::new();
789        write_dot(&g, None, &mut buf).unwrap();
790        let s = String::from_utf8(buf).unwrap();
791        assert_eq!(s, "graph {\n}\n");
792    }
793
794    #[test]
795    fn test_reserved_word_label_escaped() {
796        let mut g = Graph::with_vertices(2);
797        g.add_edge(0, 1).unwrap();
798
799        let labels = vec!["graph".to_string(), "node".to_string()];
800        let mut buf = Vec::new();
801        write_dot(&g, Some(&labels), &mut buf).unwrap();
802        let s = String::from_utf8(buf).unwrap();
803        assert!(s.contains("\"graph\" -- \"node\";"));
804    }
805
806    #[test]
807    fn test_label_with_spaces_escaped() {
808        let mut g = Graph::with_vertices(2);
809        g.add_edge(0, 1).unwrap();
810
811        let labels = vec!["hello world".to_string(), "foo bar".to_string()];
812        let mut buf = Vec::new();
813        write_dot(&g, Some(&labels), &mut buf).unwrap();
814        let s = String::from_utf8(buf).unwrap();
815        assert!(s.contains("\"hello world\" -- \"foo bar\";"));
816    }
817
818    #[test]
819    fn test_label_with_quotes_escaped() {
820        let mut g = Graph::with_vertices(2);
821        g.add_edge(0, 1).unwrap();
822
823        let labels = vec!["say \"hi\"".to_string(), "ok".to_string()];
824        let mut buf = Vec::new();
825        write_dot(&g, Some(&labels), &mut buf).unwrap();
826        let s = String::from_utf8(buf).unwrap();
827        assert!(s.contains("\"say \\\"hi\\\"\" -- ok;"));
828    }
829
830    #[test]
831    fn test_label_starting_with_digit() {
832        let mut g = Graph::with_vertices(2);
833        g.add_edge(0, 1).unwrap();
834
835        let labels = vec!["123abc".to_string(), "valid_name".to_string()];
836        let mut buf = Vec::new();
837        write_dot(&g, Some(&labels), &mut buf).unwrap();
838        let s = String::from_utf8(buf).unwrap();
839        assert!(s.contains("\"123abc\" -- valid_name;"));
840    }
841
842    #[test]
843    fn test_self_loop() {
844        let mut g = Graph::with_vertices(1);
845        g.add_edge(0, 0).unwrap();
846
847        let mut buf = Vec::new();
848        write_dot(&g, None, &mut buf).unwrap();
849        let s = String::from_utf8(buf).unwrap();
850        assert!(s.contains("0 -- 0;"));
851    }
852
853    #[test]
854    fn test_labels_mismatch_error() {
855        let g = Graph::with_vertices(3);
856        let labels = vec!["A".to_string()];
857        let mut buf = Vec::new();
858        assert!(write_dot(&g, Some(&labels), &mut buf).is_err());
859    }
860
861    // --- write_dot attribute tests ---
862
863    #[test]
864    fn write_vertex_attributes() {
865        let mut g = Graph::with_vertices(2);
866        g.add_edge(0, 1).unwrap();
867        g.set_vertex_attribute("color", 0, AttributeValue::String("red".into()))
868            .unwrap();
869        g.set_vertex_attribute("color", 1, AttributeValue::String("blue".into()))
870            .unwrap();
871
872        let mut buf = Vec::new();
873        write_dot(&g, None, &mut buf).unwrap();
874        let s = String::from_utf8(buf).unwrap();
875        assert!(s.contains("[color=\"red\"]"));
876        assert!(s.contains("[color=\"blue\"]"));
877    }
878
879    #[test]
880    fn write_edge_attributes() {
881        let mut g = Graph::with_vertices(2);
882        g.add_edge(0, 1).unwrap();
883        g.set_edge_attribute("weight", 0, AttributeValue::Numeric(2.5))
884            .unwrap();
885
886        let mut buf = Vec::new();
887        write_dot(&g, None, &mut buf).unwrap();
888        let s = String::from_utf8(buf).unwrap();
889        assert!(s.contains("0 -- 1 [weight=2.5];"));
890    }
891
892    #[test]
893    fn write_graph_attributes() {
894        let mut g = Graph::with_vertices(1);
895        g.set_graph_attribute("label", AttributeValue::String("test graph".into()));
896
897        let mut buf = Vec::new();
898        write_dot(&g, None, &mut buf).unwrap();
899        let s = String::from_utf8(buf).unwrap();
900        assert!(s.contains("graph [label=\"test graph\"];"));
901    }
902
903    #[test]
904    fn write_boolean_attribute() {
905        let mut g = Graph::with_vertices(2);
906        g.add_edge(0, 1).unwrap();
907        g.set_vertex_attribute("visited", 0, AttributeValue::Boolean(true))
908            .unwrap();
909
910        let mut buf = Vec::new();
911        write_dot(&g, None, &mut buf).unwrap();
912        let s = String::from_utf8(buf).unwrap();
913        assert!(s.contains("[visited=true]"));
914    }
915
916    #[test]
917    fn write_multiple_attributes() {
918        let mut g = Graph::with_vertices(2);
919        g.add_edge(0, 1).unwrap();
920        g.set_edge_attribute("weight", 0, AttributeValue::Numeric(1.5))
921            .unwrap();
922        g.set_edge_attribute("label", 0, AttributeValue::String("edge0".into()))
923            .unwrap();
924
925        let mut buf = Vec::new();
926        write_dot(&g, None, &mut buf).unwrap();
927        let s = String::from_utf8(buf).unwrap();
928        // Both attributes should be present in the block
929        assert!(s.contains("weight=1.5"));
930        assert!(s.contains("label=\"edge0\""));
931    }
932
933    // --- read_dot tests ---
934
935    #[test]
936    fn read_undirected_basic() {
937        let dot = "graph {\n  0 -- 1;\n  1 -- 2;\n}";
938        let result = read_dot(dot.as_bytes()).unwrap();
939        assert_eq!(result.graph.vcount(), 3);
940        assert_eq!(result.graph.ecount(), 2);
941        assert!(!result.graph.is_directed());
942    }
943
944    #[test]
945    fn read_directed_basic() {
946        let dot = "digraph {\n  a -> b;\n  b -> c;\n}";
947        let result = read_dot(dot.as_bytes()).unwrap();
948        assert_eq!(result.graph.vcount(), 3);
949        assert_eq!(result.graph.ecount(), 2);
950        assert!(result.graph.is_directed());
951        assert_eq!(result.labels, vec!["a", "b", "c"]);
952    }
953
954    #[test]
955    fn read_with_labels() {
956        let dot = "graph {\n  Alice -- Bob;\n  Bob -- Carol;\n}";
957        let result = read_dot(dot.as_bytes()).unwrap();
958        assert_eq!(result.labels, vec!["Alice", "Bob", "Carol"]);
959        assert_eq!(result.graph.ecount(), 2);
960    }
961
962    #[test]
963    fn read_quoted_labels() {
964        let dot = r#"graph { "hello world" -- "foo bar"; }"#;
965        let result = read_dot(dot.as_bytes()).unwrap();
966        assert_eq!(result.labels, vec!["hello world", "foo bar"]);
967        assert_eq!(result.graph.ecount(), 1);
968    }
969
970    #[test]
971    fn read_with_attributes_parsed() {
972        let dot = r#"graph {
973  a [color="red", shape=circle];
974  b [color="blue"];
975  a -- b [weight=1.5];
976}"#;
977        let result = read_dot(dot.as_bytes()).unwrap();
978        assert_eq!(result.graph.vcount(), 2);
979        assert_eq!(result.graph.ecount(), 1);
980        assert_eq!(
981            result
982                .graph
983                .vertex_attribute("color", 0)
984                .and_then(AttributeValue::as_str),
985            Some("red"),
986        );
987        assert_eq!(
988            result
989                .graph
990                .vertex_attribute("color", 1)
991                .and_then(AttributeValue::as_str),
992            Some("blue"),
993        );
994        assert_eq!(
995            result
996                .graph
997                .vertex_attribute("shape", 0)
998                .and_then(AttributeValue::as_str),
999            Some("circle"),
1000        );
1001        assert_eq!(
1002            result
1003                .graph
1004                .edge_attribute("weight", 0)
1005                .and_then(AttributeValue::as_f64),
1006            Some(1.5),
1007        );
1008    }
1009
1010    #[test]
1011    fn read_graph_level_attributes() {
1012        let dot = r#"graph {
1013  graph [label="My Graph", rankdir="LR"];
1014  a -- b;
1015}"#;
1016        let result = read_dot(dot.as_bytes()).unwrap();
1017        assert_eq!(
1018            result
1019                .graph
1020                .graph_attribute("label")
1021                .and_then(AttributeValue::as_str),
1022            Some("My Graph"),
1023        );
1024        assert_eq!(
1025            result
1026                .graph
1027                .graph_attribute("rankdir")
1028                .and_then(AttributeValue::as_str),
1029            Some("LR"),
1030        );
1031    }
1032
1033    #[test]
1034    fn read_boolean_attribute() {
1035        let dot = r"graph {
1036  a [visited=true];
1037  b [visited=false];
1038  a -- b;
1039}";
1040        let result = read_dot(dot.as_bytes()).unwrap();
1041        assert_eq!(
1042            result
1043                .graph
1044                .vertex_attribute("visited", 0)
1045                .and_then(AttributeValue::as_bool),
1046            Some(true),
1047        );
1048        assert_eq!(
1049            result
1050                .graph
1051                .vertex_attribute("visited", 1)
1052                .and_then(AttributeValue::as_bool),
1053            Some(false),
1054        );
1055    }
1056
1057    #[test]
1058    fn read_edge_chain() {
1059        let dot = "digraph { a -> b -> c -> d; }";
1060        let result = read_dot(dot.as_bytes()).unwrap();
1061        assert_eq!(result.graph.vcount(), 4);
1062        assert_eq!(result.graph.ecount(), 3);
1063    }
1064
1065    #[test]
1066    fn read_isolated_nodes() {
1067        let dot = "graph {\n  x;\n  y;\n  z;\n  x -- y;\n}";
1068        let result = read_dot(dot.as_bytes()).unwrap();
1069        assert_eq!(result.graph.vcount(), 3);
1070        assert_eq!(result.graph.ecount(), 1);
1071    }
1072
1073    #[test]
1074    fn read_with_comments() {
1075        let dot = "// comment\ngraph {\n  /* block comment */\n  a -- b;\n}";
1076        let result = read_dot(dot.as_bytes()).unwrap();
1077        assert_eq!(result.graph.vcount(), 2);
1078        assert_eq!(result.graph.ecount(), 1);
1079    }
1080
1081    #[test]
1082    fn read_empty_graph() {
1083        let dot = "graph { }";
1084        let result = read_dot(dot.as_bytes()).unwrap();
1085        assert_eq!(result.graph.vcount(), 0);
1086        assert_eq!(result.graph.ecount(), 0);
1087    }
1088
1089    #[test]
1090    fn read_no_keyword_is_error() {
1091        let dot = "{ a -- b; }";
1092        assert!(read_dot(dot.as_bytes()).is_err());
1093    }
1094
1095    #[test]
1096    fn read_self_loop() {
1097        let dot = "graph { a -- a; }";
1098        let result = read_dot(dot.as_bytes()).unwrap();
1099        assert_eq!(result.graph.vcount(), 1);
1100        assert_eq!(result.graph.ecount(), 1);
1101    }
1102
1103    #[test]
1104    fn roundtrip_dot() {
1105        let mut g = Graph::new(3, true).unwrap();
1106        g.add_edge(0, 1).unwrap();
1107        g.add_edge(1, 2).unwrap();
1108        g.add_edge(2, 0).unwrap();
1109
1110        let mut buf = Vec::new();
1111        write_dot(&g, None, &mut buf).unwrap();
1112
1113        let result = read_dot(buf.as_slice()).unwrap();
1114        assert_eq!(result.graph.vcount(), 3);
1115        assert_eq!(result.graph.ecount(), 3);
1116        assert!(result.graph.is_directed());
1117    }
1118
1119    #[test]
1120    fn roundtrip_with_attributes() {
1121        let mut g = Graph::new(3, true).unwrap();
1122        g.add_edge(0, 1).unwrap();
1123        g.add_edge(1, 2).unwrap();
1124
1125        g.set_vertex_attribute("color", 0, AttributeValue::String("red".into()))
1126            .unwrap();
1127        g.set_vertex_attribute("color", 1, AttributeValue::String("blue".into()))
1128            .unwrap();
1129        g.set_edge_attribute("weight", 0, AttributeValue::Numeric(1.5))
1130            .unwrap();
1131        g.set_graph_attribute("label", AttributeValue::String("test".into()));
1132
1133        let mut buf = Vec::new();
1134        write_dot(&g, None, &mut buf).unwrap();
1135
1136        let result = read_dot(buf.as_slice()).unwrap();
1137        assert_eq!(result.graph.vcount(), 3);
1138        assert_eq!(result.graph.ecount(), 2);
1139        assert!(result.graph.is_directed());
1140        assert_eq!(
1141            result
1142                .graph
1143                .vertex_attribute("color", 0)
1144                .and_then(AttributeValue::as_str),
1145            Some("red"),
1146        );
1147        assert_eq!(
1148            result
1149                .graph
1150                .vertex_attribute("color", 1)
1151                .and_then(AttributeValue::as_str),
1152            Some("blue"),
1153        );
1154        assert_eq!(
1155            result
1156                .graph
1157                .edge_attribute("weight", 0)
1158                .and_then(AttributeValue::as_f64),
1159            Some(1.5),
1160        );
1161        assert_eq!(
1162            result
1163                .graph
1164                .graph_attribute("label")
1165                .and_then(AttributeValue::as_str),
1166            Some("test"),
1167        );
1168    }
1169
1170    #[test]
1171    fn read_strict_keyword() {
1172        let dot = "strict graph {\n  a -- b;\n  a -- b;\n}";
1173        let result = read_dot(dot.as_bytes()).unwrap();
1174        assert!(!result.graph.is_directed());
1175        assert_eq!(result.graph.vcount(), 2);
1176    }
1177
1178    #[test]
1179    fn read_node_attrs_ignored() {
1180        // "node [shape=circle]" is a global default statement — skip it
1181        let dot = r"graph {
1182  node [shape=circle];
1183  edge [color=red];
1184  a -- b;
1185}";
1186        let result = read_dot(dot.as_bytes()).unwrap();
1187        assert_eq!(result.graph.vcount(), 2);
1188        assert_eq!(result.graph.ecount(), 1);
1189        // Global node/edge defaults are NOT stored as vertex/edge attributes
1190        assert!(!result.graph.has_vertex_attribute("shape"));
1191    }
1192
1193    #[test]
1194    fn parse_attr_pairs_basic() {
1195        let pairs = parse_attr_pairs(r#"weight=1.5, color="red", active=true"#);
1196        assert_eq!(pairs.len(), 3);
1197        assert_eq!(pairs[0], ("weight".into(), AttributeValue::Numeric(1.5)));
1198        assert_eq!(
1199            pairs[1],
1200            ("color".into(), AttributeValue::String("red".into()))
1201        );
1202        assert_eq!(pairs[2], ("active".into(), AttributeValue::Boolean(true)));
1203    }
1204}