Skip to main content

graphviz_pure_parser/
parser.rs

1//! Recursive-descent DOT parser matching Graphviz `lib/cgraph/grammar.y`,
2//! with full lexical scoping for `graph`, `node`, and `edge` defaults across
3//! nested subgraphs and clusters.
4
5use crate::lexer::{Lexer, ScanState, Token, TokenKind};
6type AttrMap2 = std::collections::BTreeMap<String, graphviz_pure_core::AttrSym>;
7
8use graphviz_pure_core::{
9    AttrMap, BoundingBox, EdgeId, Graph, GraphKind, GraphvizError, NodeId, Port, Result,
10    SubgraphData, SubgraphId,
11};
12
13const MAX_SUBGRAPH_DEPTH: usize = 256;
14
15#[derive(Debug, Clone)]
16enum SimpleItem {
17    Nodes(Vec<(NodeId, Port)>),
18    Subgraph(Vec<(NodeId, Port)>),
19}
20
21impl SimpleItem {
22    fn into_endpoints(self) -> Vec<(NodeId, Port)> {
23        match self {
24            Self::Nodes(v) | Self::Subgraph(v) => v,
25        }
26    }
27}
28
29/// Attributes declared before the graph body, as set by the command line flags
30/// `-Gname=value`, `-Nname=value` and `-Ename=value`.
31#[derive(Debug, Clone, Default, PartialEq, Eq)]
32pub struct GlobalAttrs {
33    pub graph: AttrMap,
34    pub node: AttrMap,
35    pub edge: AttrMap,
36}
37
38pub struct Parser {
39    tokens: Vec<Token>,
40    pos: usize,
41    graph: Graph,
42    scope_stack: Vec<SubgraphId>,
43    globals: GlobalAttrs,
44    /// Line of the root graph's closing `}`.
45    rbrace_line: usize,
46    /// Keys of the last attribute list in source order: cgraph declares them in this
47    /// order, which fixes their `sym->id`.
48    attr_order: Vec<String>,
49    /// cgraph's id counter for anonymous objects (`ctr` in `idmap`).
50    anon_ctr: u64,
51    /// Parent of each subgraph (`None`: the root graph).
52    sub_parent: Vec<Option<usize>>,
53}
54
55/// Which dictionary of a graph an attribute belongs to.
56#[derive(Clone, Copy, PartialEq, Eq)]
57enum DictKind {
58    Graph,
59    Node,
60    Edge,
61}
62
63impl Parser {
64    fn new(input: &str, globals: &GlobalAttrs) -> Result<Self> {
65        let mut lexer = Lexer::new(input);
66        let tokens = lexer.tokenize_all()?;
67        Ok(Self {
68            tokens,
69            pos: 0,
70            graph: Graph::new("", GraphKind::Undirected, false),
71            scope_stack: Vec::new(),
72            globals: globals.clone(),
73            rbrace_line: 0,
74            attr_order: Vec::new(),
75            anon_ctr: 1,
76            sub_parent: Vec::new(),
77        })
78    }
79
80    /// Parses the first graph in `input`.
81    pub fn parse_str(input: &str) -> Result<Graph> {
82        let mut parser = Self::new(input, &GlobalAttrs::default())?;
83        parser.parse_graph()?;
84        Ok(parser.graph)
85    }
86
87    /// Parses every graph in `input`, applying `globals` before each graph body.
88    pub fn parse_all(input: &str, globals: &GlobalAttrs) -> Result<Vec<Graph>> {
89        match Self::parse_all_partial(input, globals) {
90            (graphs, None) => Ok(graphs),
91            (_, Some(e)) => Err(e),
92        }
93    }
94
95    /// Like [`Parser::parse_all`], but keeps the graphs read before a syntax error: C's
96    /// `agread` returns them one at a time, so `dot` lays out and renders those first.
97    ///
98    /// cgraph's scanner reads its input a line at a time (`fgets`) and each `agread`
99    /// starts a new scanner, so whatever follows a graph's closing `}` on the same line
100    /// is dropped, and the next graph is read from the next line, its line numbers
101    /// counted from there.
102    pub fn parse_all_partial(
103        input: &str,
104        globals: &GlobalAttrs,
105    ) -> (Vec<Graph>, Option<GraphvizError>) {
106        let mut graphs = Vec::new();
107        let mut rest = input;
108        loop {
109            let mut parser = match Self::new(rest, globals) {
110                Ok(p) => p,
111                Err(e) => return (graphs, Some(e)),
112            };
113            if parser.peek().kind == TokenKind::Eof {
114                return (graphs, None);
115            }
116            if let Err(e) = parser.parse_graph() {
117                return (graphs, Some(e));
118            }
119            graphs.push(parser.graph);
120            // the byte after the `rbrace_line`-th newline
121            match rest
122                .match_indices('\n')
123                .nth(parser.rbrace_line.saturating_sub(1))
124            {
125                Some((i, _)) => rest = &rest[i + 1..],
126                None => return (graphs, None),
127            }
128        }
129    }
130
131    fn peek(&self) -> &Token {
132        self.tokens
133            .get(self.pos)
134            .unwrap_or_else(|| self.tokens.last().expect("tokens always ends with Eof"))
135    }
136
137    fn peek_n(&self, offset: usize) -> &Token {
138        self.tokens
139            .get(self.pos + offset)
140            .unwrap_or_else(|| self.tokens.last().expect("tokens always ends with Eof"))
141    }
142
143    fn advance(&mut self) -> Token {
144        let tok = self.peek().clone();
145        if self.pos + 1 < self.tokens.len() {
146            self.pos += 1;
147        }
148        tok
149    }
150
151    fn expect(&mut self, expected: &TokenKind) -> Result<Token> {
152        if &self.peek().kind == expected {
153            Ok(self.advance())
154        } else {
155            Err(syntax_error(self.peek()))
156        }
157    }
158
159    fn is_atom_token(kind: &TokenKind) -> bool {
160        matches!(
161            kind,
162            TokenKind::Ident(_) | TokenKind::Quoted(_) | TokenKind::Html(_)
163        )
164    }
165
166    /// `atom : T_atom | qatom` (grammar.y). The keywords `node`, `edge`,
167    /// `graph`, `digraph`, `subgraph` and `strict` are their own tokens in
168    /// scan.l (matched case-insensitively), not `T_atom`, so an unquoted
169    /// keyword is a syntax error wherever an atom is expected: attribute
170    /// names and values, node names, ports and compass points (issue #29).
171    fn parse_atom(&mut self) -> Result<String> {
172        let tok = self.peek().clone();
173        let mut is_html = false;
174        let mut val = match tok.kind {
175            TokenKind::Ident(s) | TokenKind::Quoted(s) => {
176                self.advance();
177                s
178            }
179            TokenKind::Html(s) => {
180                self.advance();
181                is_html = true;
182                s
183            }
184            _ => return Err(syntax_error(&tok)),
185        };
186
187        // Support string concatenation with `+` (`qatom '+' T_qatom` in grammar.y)
188        while self.peek().kind == TokenKind::Plus {
189            self.advance();
190            let next_tok = self.peek().clone();
191            match next_tok.kind {
192                TokenKind::Ident(s) | TokenKind::Quoted(s) | TokenKind::Html(s) => {
193                    self.advance();
194                    is_html = false;
195                    val.push_str(&s);
196                }
197                _ => return Err(syntax_error(&next_tok)),
198            }
199        }
200
201        if is_html {
202            self.graph.html_strings.insert(val.clone());
203        }
204        Ok(val)
205    }
206
207    /// Checks how many tokens an atom (including `+` concatenations) spans starting at `start_offset`,
208    /// so we can look ahead for `=` without mutating parser state.
209    fn atom_span_len(&self, start_offset: usize) -> Option<usize> {
210        if !Self::is_atom_token(&self.peek_n(start_offset).kind) {
211            return None;
212        }
213        let mut len = 1;
214        while self.peek_n(start_offset + len).kind == TokenKind::Plus {
215            if Self::is_atom_token(&self.peek_n(start_offset + len + 1).kind) {
216                len += 2;
217            } else {
218                break;
219            }
220        }
221        Some(len)
222    }
223
224    fn parse_graph(&mut self) -> Result<()> {
225        let strict = if self.peek().kind == TokenKind::Strict {
226            self.advance();
227            true
228        } else {
229            false
230        };
231
232        let tok = self.peek().clone();
233        let kind = match tok.kind {
234            TokenKind::Graph => {
235                self.advance();
236                GraphKind::Undirected
237            }
238            TokenKind::Digraph => {
239                self.advance();
240                GraphKind::Directed
241            }
242            _ => return Err(syntax_error(&tok)),
243        };
244
245        let name = if Self::is_atom_token(&self.peek().kind) {
246            self.parse_atom()?
247        } else {
248            String::new()
249        };
250
251        self.anon_ctr = if name.is_empty() { 3 } else { 1 };
252        self.graph = Graph::new(name, kind, strict);
253        self.graph.graph_attrs.extend(self.globals.graph.clone());
254        self.graph.node_defaults.extend(self.globals.node.clone());
255        self.graph.edge_defaults.extend(self.globals.edge.clone());
256        self.sub_parent.clear();
257        self.graph.dicts = vec![graphviz_pure_core::AttrDicts::default()];
258        // gvContext declares the node label on cgraph's ProtoGraph, which every root copies.
259        self.graph.dicts[0].node.insert(
260            "label".to_string(),
261            graphviz_pure_core::AttrSym {
262                defval: "\\N".to_string(),
263                print: false,
264                id: 0,
265            },
266        );
267        // Command-line defaults come from cgraph's ProtoGraph dictionaries.
268        for (kind, map) in [
269            (DictKind::Graph, self.globals.graph.clone()),
270            (DictKind::Node, self.globals.node.clone()),
271            (DictKind::Edge, self.globals.edge.clone()),
272        ] {
273            for (k, v) in map {
274                let d = self.dict_mut(0, kind);
275                let id = d.get(&k).map_or(d.len(), |s| s.id);
276                d.insert(
277                    k,
278                    graphviz_pure_core::AttrSym {
279                        defval: v,
280                        print: false,
281                        id,
282                    },
283                );
284            }
285        }
286
287        self.expect(&TokenKind::LBrace)?;
288        self.parse_stmt_list()?;
289        self.rbrace_line = self.expect(&TokenKind::RBrace)?.line;
290        // A graph attribute first declared in a subgraph is declared on the root with
291        // default "" (agattr), so agget(root, name) is "" rather than NULL.
292        let declared: Vec<(String, String)> = self.graph.dicts[0]
293            .graph
294            .iter()
295            .map(|(k, v)| (k.clone(), v.defval.clone()))
296            .collect();
297        for (k, v) in declared {
298            self.graph.graph_attrs.entry(k).or_insert(v);
299        }
300        // Consume optional trailing semicolons
301        while self.peek().kind == TokenKind::Semi {
302            self.advance();
303        }
304
305        // Stray tokens after the closing brace are the next read's syntax error
306        // (`parse_all_partial`).
307        Ok(())
308    }
309
310    fn parse_stmt_list(&mut self) -> Result<()> {
311        while self.peek().kind != TokenKind::RBrace && self.peek().kind != TokenKind::Eof {
312            if self.peek().kind == TokenKind::Semi {
313                self.advance();
314                continue;
315            }
316            self.parse_stmt()?;
317            if self.peek().kind == TokenKind::Semi {
318                self.advance();
319            }
320        }
321        Ok(())
322    }
323
324    fn parse_stmt(&mut self) -> Result<()> {
325        match self.peek().kind {
326            TokenKind::Graph | TokenKind::Node | TokenKind::Edge => self.parse_attr_stmt(),
327            TokenKind::Subgraph | TokenKind::LBrace => self.parse_compound(),
328            _ if Self::is_atom_token(&self.peek().kind) => {
329                if let Some(span) = self.atom_span_len(0)
330                    && self.peek_n(span).kind == TokenKind::Equals
331                {
332                    let key = self.parse_atom()?;
333                    self.expect(&TokenKind::Equals)?;
334                    let val = self.parse_atom()?;
335                    if !self.fixed_at_root(DictKind::Graph, &key) {
336                        self.attr_stmt(DictKind::Graph, &key, &val);
337                        self.current_graph_attrs_mut().insert(key, val);
338                    } else {
339                        if let Some(sym) = self.dict_mut(0, DictKind::Graph).get_mut(&key) {
340                            sym.print = true;
341                        }
342                    }
343                    return Ok(());
344                }
345                self.parse_compound()
346            }
347            _ => Err(syntax_error(self.peek())),
348        }
349    }
350
351    fn parse_attr_stmt(&mut self) -> Result<()> {
352        let attr_tok = self.advance();
353        // Optional macro name `atom '='` before `[` (`optmacroname` in grammar.y)
354        if let Some(span) = self.atom_span_len(0)
355            && self.peek_n(span).kind == TokenKind::Equals
356        {
357            let _macro_name = self.parse_atom()?;
358            self.expect(&TokenKind::Equals)?;
359        }
360
361        if self.peek().kind != TokenKind::LBracket {
362            return Err(syntax_error(self.peek()));
363        }
364
365        let attrs = self.parse_opt_attr_list()?;
366        let kind = match attr_tok.kind {
367            TokenKind::Graph => DictKind::Graph,
368            TokenKind::Node => DictKind::Node,
369            _ => DictKind::Edge,
370        };
371        // `-G`/`-N`/`-E` symbols are `fixed`: root attribute statements keep them.
372        let mut kept = AttrMap::new();
373        for (k, v) in attrs {
374            if self.fixed_at_root(kind, &k) {
375                if let Some(sym) = self.dict_mut(0, kind).get_mut(&k) {
376                    sym.print = true;
377                }
378            } else {
379                kept.insert(k, v);
380            }
381        }
382        let attrs = kept;
383        for k in std::mem::take(&mut self.attr_order) {
384            if let Some(v) = attrs.get(&k) {
385                self.attr_stmt(kind, &k, v);
386            }
387        }
388        match attr_tok.kind {
389            TokenKind::Graph => self.current_graph_attrs_mut().extend(attrs),
390            TokenKind::Node => self.current_node_defaults_mut().extend(attrs),
391            TokenKind::Edge => self.current_edge_defaults_mut().extend(attrs),
392            _ => unreachable!(),
393        }
394        Ok(())
395    }
396
397    fn parse_opt_attr_list(&mut self) -> Result<AttrMap> {
398        let mut attrs = AttrMap::new();
399        self.attr_order.clear();
400        while self.peek().kind == TokenKind::LBracket {
401            self.advance(); // consume `[`
402            // attrdefs: `atom '=' atom` followed by at most one `,` or `;`.
403            while self.peek().kind != TokenKind::RBracket && self.peek().kind != TokenKind::Eof {
404                let key = self.parse_atom()?;
405                self.expect(&TokenKind::Equals)?;
406                let val = self.parse_atom()?;
407                if !attrs.contains_key(&key) {
408                    self.attr_order.push(key.clone());
409                }
410                attrs.insert(key, val);
411                if matches!(self.peek().kind, TokenKind::Comma | TokenKind::Semi) {
412                    self.advance();
413                }
414            }
415            self.expect(&TokenKind::RBracket)?;
416        }
417        Ok(attrs)
418    }
419
420    fn parse_compound(&mut self) -> Result<()> {
421        let first = self.parse_simple()?;
422
423        if matches!(self.peek().kind, TokenKind::Arrow | TokenKind::DashDash) {
424            let mut chain = vec![first.into_endpoints()];
425            while matches!(self.peek().kind, TokenKind::Arrow | TokenKind::DashDash) {
426                let op_tok = self.advance();
427                // scan.l returns the other graph type's edge operator as the token '-',
428                // which no rule accepts.
429                match (&op_tok.kind, self.graph.kind) {
430                    (TokenKind::Arrow, GraphKind::Undirected)
431                    | (TokenKind::DashDash, GraphKind::Directed) => {
432                        return Err(syntax_error(&op_tok));
433                    }
434                    _ => {}
435                }
436                let next_item = self.parse_simple()?;
437                chain.push(next_item.into_endpoints());
438            }
439
440            let edge_attrs = self.parse_opt_attr_list()?;
441            for k in std::mem::take(&mut self.attr_order) {
442                if k != "key" {
443                    self.bind_attr(DictKind::Edge, &k);
444                }
445            }
446            for pair in chain.windows(2) {
447                let tails = &pair[0];
448                let heads = &pair[1];
449                for (tail_id, tail_port) in tails {
450                    for (head_id, head_port) in heads {
451                        self.create_edge(
452                            *tail_id,
453                            tail_port.clone(),
454                            *head_id,
455                            head_port.clone(),
456                            &edge_attrs,
457                        );
458                    }
459                }
460            }
461        } else {
462            let attrs = self.parse_opt_attr_list()?;
463            if matches!(first, SimpleItem::Nodes(_)) {
464                for k in std::mem::take(&mut self.attr_order) {
465                    self.bind_attr(DictKind::Node, &k);
466                }
467            }
468            if let SimpleItem::Nodes(nodes) = first
469                && !attrs.is_empty()
470            {
471                for (node_id, _port) in nodes {
472                    self.graph.nodes[node_id.0 as usize]
473                        .attrs
474                        .extend(attrs.clone());
475                }
476            }
477        }
478
479        Ok(())
480    }
481
482    fn parse_simple(&mut self) -> Result<SimpleItem> {
483        if matches!(self.peek().kind, TokenKind::Subgraph | TokenKind::LBrace) {
484            let subg_id = self.parse_subgraph()?;
485            // `agfstnode(subg)..agnxtnode`: every node of the subgraph
486            // (nested subgraphs included) in node sequence order.
487            let mut ids: Vec<NodeId> = Vec::new();
488            let mut stack = vec![subg_id];
489            while let Some(s) = stack.pop() {
490                let sg = &self.graph.subgraphs[s.0 as usize];
491                ids.extend(sg.nodes.iter().copied());
492                stack.extend(sg.subgraphs.iter().copied());
493            }
494            ids.sort_by_key(|n| n.0);
495            ids.dedup();
496            let endpoints = ids.into_iter().map(|nid| (nid, Port::default())).collect();
497            Ok(SimpleItem::Subgraph(endpoints))
498        } else {
499            let mut nodes = vec![self.parse_node_ref()?];
500            while self.peek().kind == TokenKind::Comma {
501                self.advance();
502                nodes.push(self.parse_node_ref()?);
503            }
504            Ok(SimpleItem::Nodes(nodes))
505        }
506    }
507
508    fn parse_node_ref(&mut self) -> Result<(NodeId, Port)> {
509        let name = self.parse_atom()?;
510        let mut port = Port::default();
511        if self.peek().kind == TokenKind::Colon {
512            self.advance();
513            let p1 = self.parse_atom()?;
514            if self.peek().kind == TokenKind::Colon {
515                self.advance();
516                let p2 = self.parse_atom()?;
517                port.name = Some(p1);
518                port.compass = Some(p2);
519            } else {
520                port.name = Some(p1);
521            }
522        }
523        let node_id = self.ensure_scoped_node(&name);
524        Ok((node_id, port))
525    }
526
527    fn parse_subgraph(&mut self) -> Result<SubgraphId> {
528        let name = if self.peek().kind == TokenKind::Subgraph {
529            self.advance();
530            if Self::is_atom_token(&self.peek().kind) {
531                self.parse_atom()?
532            } else {
533                String::new()
534            }
535        } else {
536            String::new()
537        };
538
539        if self.scope_stack.len() >= MAX_SUBGRAPH_DEPTH {
540            let tok = self.peek();
541            return Err(GraphvizError::SyntaxError {
542                line: tok.c_line,
543                col: tok.col,
544                message: format!("subgraphs nested more than {MAX_SUBGRAPH_DEPTH} deep"),
545                file: None,
546            });
547        }
548
549        let subg_id = self.open_subgraph(name.clone());
550
551        if self.peek().kind == TokenKind::LBrace {
552            self.scope_stack.push(subg_id);
553            self.advance(); // consume `{`
554            self.parse_stmt_list()?;
555            self.expect(&TokenKind::RBrace)?;
556            self.scope_stack.pop();
557        } else if name.is_empty() {
558            return Err(syntax_error(self.peek()));
559        }
560
561        Ok(subg_id)
562    }
563
564    fn open_subgraph(&mut self, name: String) -> SubgraphId {
565        let current_parent = self.scope_stack.last().copied();
566        if !name.is_empty()
567            && let Some(existing_id) = self.graph.subgraphs.iter().find_map(|s| {
568                if s.name != name {
569                    return None;
570                }
571                let has_same_parent = match current_parent {
572                    Some(pid) => self.graph.subgraphs[pid.0 as usize]
573                        .subgraphs
574                        .contains(&s.id),
575                    None => !self
576                        .graph
577                        .subgraphs
578                        .iter()
579                        .any(|p| p.subgraphs.contains(&s.id)),
580                };
581                has_same_parent.then_some(s.id)
582            })
583        {
584            return existing_id;
585        }
586
587        let id = SubgraphId(self.graph.subgraphs.len() as u32);
588        if name.is_empty() {
589            self.graph.anon_subgraph_ids.insert(id.0, self.anon_ctr);
590            self.anon_ctr += 2;
591        }
592        let is_cluster = name.to_ascii_lowercase().starts_with("cluster");
593        let graph_attrs = self.current_graph_attrs().clone();
594        let node_defaults = self.current_node_defaults().clone();
595        let edge_defaults = self.current_edge_defaults().clone();
596
597        self.sub_parent
598            .push(self.scope_stack.last().map(|p| p.0 as usize));
599        self.graph
600            .dicts
601            .push(graphviz_pure_core::AttrDicts::default());
602        if let Some(&parent_id) = self.scope_stack.last() {
603            let parent_subgraphs = &mut self.graph.subgraphs[parent_id.0 as usize].subgraphs;
604            if !parent_subgraphs.contains(&id) {
605                parent_subgraphs.push(id);
606            }
607        }
608
609        self.graph.subgraphs.push(SubgraphData {
610            id,
611            name,
612            is_cluster,
613            graph_attrs,
614            node_defaults,
615            edge_defaults,
616            nodes: Vec::new(),
617            edges: Vec::new(),
618            subgraphs: Vec::new(),
619            bb: BoundingBox::default(),
620        });
621
622        id
623    }
624
625    /// Dictionary index of the graph in scope (0: root).
626    /// `sym->fixed` in `attrstmt`: a command-line default (`global_def`) that a
627    /// root-level attribute statement does not change (subgraphs still may).
628    fn fixed_at_root(&self, kind: DictKind, name: &str) -> bool {
629        self.scope_dict() == 0
630            && match kind {
631                DictKind::Graph => self.globals.graph.contains_key(name),
632                DictKind::Node => self.globals.node.contains_key(name),
633                DictKind::Edge => self.globals.edge.contains_key(name),
634            }
635    }
636
637    fn scope_dict(&self) -> usize {
638        self.scope_stack.last().map_or(0, |s| s.0 as usize + 1)
639    }
640
641    fn dict_mut(&mut self, gi: usize, kind: DictKind) -> &mut AttrMap2 {
642        let d = &mut self.graph.dicts[gi];
643        match kind {
644            DictKind::Graph => &mut d.graph,
645            DictKind::Node => &mut d.node,
646            DictKind::Edge => &mut d.edge,
647        }
648    }
649
650    fn dict(&self, gi: usize, kind: DictKind) -> &AttrMap2 {
651        let d = &self.graph.dicts[gi];
652        match kind {
653            DictKind::Graph => &d.graph,
654            DictKind::Node => &d.node,
655            DictKind::Edge => &d.edge,
656        }
657    }
658
659    /// Dictionary lookup through the view path up to the root (`agdictsym`).
660    fn lookup_view(&self, gi: usize, kind: DictKind, name: &str) -> bool {
661        let mut cur = Some(gi);
662        while let Some(g) = cur {
663            if self.dict(g, kind).contains_key(name) {
664                return true;
665            }
666            cur = if g == 0 {
667                None
668            } else {
669                Some(self.sub_parent[g - 1].map_or(0, |p| p + 1))
670            };
671        }
672        false
673    }
674
675    /// `bindattrs`: an attribute unknown in scope is declared on the root with default "".
676    fn bind_attr(&mut self, kind: DictKind, name: &str) {
677        let gi = self.scope_dict();
678        if !self.lookup_view(gi, kind, name) {
679            let d = self.dict_mut(0, kind);
680            let id = d.len();
681            d.insert(
682                name.to_string(),
683                graphviz_pure_core::AttrSym {
684                    id,
685                    ..Default::default()
686                },
687            );
688        }
689    }
690
691    /// `attrstmt` + `setattr`: a default set in the current scope.
692    fn attr_stmt(&mut self, kind: DictKind, name: &str, value: &str) {
693        self.bind_attr(kind, name);
694        let gi = self.scope_dict();
695        if self.dict(gi, kind).contains_key(name) {
696            if kind == DictKind::Graph {
697                // unviewsubgraphsattr: direct children without a local value keep theirs.
698                let children: Vec<usize> = if gi == 0 {
699                    (0..self.sub_parent.len())
700                        .filter(|&i| self.sub_parent[i].is_none())
701                        .collect()
702                } else {
703                    self.graph.subgraphs[gi - 1]
704                        .subgraphs
705                        .iter()
706                        .map(|c| c.0 as usize)
707                        .collect()
708                };
709                for c in children {
710                    if !self.graph.dicts[c + 1].graph.contains_key(name) {
711                        let v = self.graph.subgraphs[c]
712                            .graph_attrs
713                            .get(name)
714                            .cloned()
715                            .unwrap_or_default();
716                        self.graph.dicts[c + 1].graph.insert(
717                            name.to_string(),
718                            graphviz_pure_core::AttrSym {
719                                defval: v,
720                                ..Default::default()
721                            },
722                        );
723                    }
724                }
725            }
726            self.dict_mut(gi, kind).get_mut(name).unwrap().defval = value.to_string();
727        } else {
728            self.dict_mut(gi, kind).insert(
729                name.to_string(),
730                graphviz_pure_core::AttrSym {
731                    defval: value.to_string(),
732                    ..Default::default()
733                },
734            );
735        }
736        if gi == 0 {
737            self.dict_mut(0, kind).get_mut(name).unwrap().print = true;
738        }
739    }
740
741    fn ensure_scoped_node(&mut self, name: &str) -> NodeId {
742        let is_new = !self.graph.node_lookup.contains_key(name);
743        let node_id = self.graph.ensure_node(name);
744        if is_new {
745            let mut attrs = self.current_node_defaults().clone();
746            attrs
747                .entry("label".to_string())
748                .or_insert_with(|| "\\N".to_string());
749            self.graph.nodes[node_id.0 as usize].attrs = attrs;
750        }
751        self.record_node_in_active_subgraphs(node_id);
752        node_id
753    }
754
755    fn create_edge(
756        &mut self,
757        tail: NodeId,
758        mut tail_port: Port,
759        head: NodeId,
760        mut head_port: Port,
761        edge_attrs: &AttrMap,
762    ) -> EdgeId {
763        let mut merged_attrs = self.current_edge_defaults().clone();
764        merged_attrs.extend(edge_attrs.clone());
765
766        // agedge on a strict graph returns the existing edge, to which applyattrs adds
767        // only this statement's attributes (the defaults applied when it was made).
768        let undirected = self.graph.kind == graphviz_pure_core::GraphKind::Undirected;
769        let exists = self.graph.strict
770            && self.graph.edges.iter().any(|e| {
771                (e.tail == tail && e.head == head)
772                    || (undirected && e.tail == head && e.head == tail)
773            });
774        if exists {
775            merged_attrs = edge_attrs.clone();
776        }
777        let before = self.graph.edges.len();
778        let edge_id = self.graph.add_edge(tail, head, merged_attrs.clone());
779        let is_new = self.graph.edges.len() > before;
780        if is_new {
781            self.anon_ctr += 2;
782        }
783        if let Some(edge) = self.graph.edges.get(edge_id.0 as usize) {
784            // grammar.y `newedge`: an existing undirected edge found the other way round
785            // takes the endpoint ports swapped.
786            if edge.tail != edge.head && edge.head == tail {
787                std::mem::swap(&mut tail_port, &mut head_port);
788            }
789            // `mkport` declares the `tailport`/`headport` edge symbol (default "") the
790            // first time an `n:port` endpoint is seen, tail then head, after `bindattrs`
791            // bound this statement's list. Symbol ids count it at that point, which the
792            // nested flat-edge layout's stale `E_arrowsz` lookup depends on.
793            for (key, endpoint) in [("tailport", &tail_port), ("headport", &head_port)] {
794                if *endpoint != Port::default() {
795                    self.bind_attr(DictKind::Edge, key);
796                }
797            }
798        }
799        if let Some(edge) = self.graph.edges.get_mut(edge_id.0 as usize) {
800            // `newedge` sets the endpoint ports (`mkport`) after `agedge` applied the edge
801            // defaults and before `applyattrs` applies the statement's attribute list, so
802            // an explicit `tailport`/`headport` in the list wins over `n:port`, which wins
803            // over a default.
804            for (key, endpoint) in [("tailport", &tail_port), ("headport", &head_port)] {
805                if *endpoint != Port::default() && !edge_attrs.contains_key(key) {
806                    merged_attrs.insert(key.to_string(), port_to_str(endpoint));
807                }
808            }
809            edge.attrs.extend(merged_attrs);
810            edge.tail_port = edge
811                .attrs
812                .get("tailport")
813                .map(|s| parse_port_str(s))
814                .unwrap_or_default();
815            edge.head_port = edge
816                .attrs
817                .get("headport")
818                .map(|s| parse_port_str(s))
819                .unwrap_or_default();
820        }
821
822        self.record_node_in_active_subgraphs(tail);
823        self.record_node_in_active_subgraphs(head);
824        self.record_edge_in_active_subgraphs(edge_id);
825        edge_id
826    }
827
828    fn record_node_in_active_subgraphs(&mut self, node_id: NodeId) {
829        for &subg_id in &self.scope_stack {
830            let nodes = &mut self.graph.subgraphs[subg_id.0 as usize].nodes;
831            if !nodes.contains(&node_id) {
832                nodes.push(node_id);
833            }
834        }
835    }
836
837    fn record_edge_in_active_subgraphs(&mut self, edge_id: EdgeId) {
838        for &subg_id in &self.scope_stack {
839            let edges = &mut self.graph.subgraphs[subg_id.0 as usize].edges;
840            if !edges.contains(&edge_id) {
841                edges.push(edge_id);
842            }
843        }
844    }
845
846    fn current_graph_attrs(&self) -> &AttrMap {
847        if let Some(&subg_id) = self.scope_stack.last() {
848            &self.graph.subgraphs[subg_id.0 as usize].graph_attrs
849        } else {
850            &self.graph.graph_attrs
851        }
852    }
853
854    fn current_graph_attrs_mut(&mut self) -> &mut AttrMap {
855        if let Some(&subg_id) = self.scope_stack.last() {
856            &mut self.graph.subgraphs[subg_id.0 as usize].graph_attrs
857        } else {
858            &mut self.graph.graph_attrs
859        }
860    }
861
862    fn current_node_defaults(&self) -> &AttrMap {
863        if let Some(&subg_id) = self.scope_stack.last() {
864            &self.graph.subgraphs[subg_id.0 as usize].node_defaults
865        } else {
866            &self.graph.node_defaults
867        }
868    }
869
870    fn current_node_defaults_mut(&mut self) -> &mut AttrMap {
871        if let Some(&subg_id) = self.scope_stack.last() {
872            &mut self.graph.subgraphs[subg_id.0 as usize].node_defaults
873        } else {
874            &mut self.graph.node_defaults
875        }
876    }
877
878    fn current_edge_defaults(&self) -> &AttrMap {
879        if let Some(&subg_id) = self.scope_stack.last() {
880            &self.graph.subgraphs[subg_id.0 as usize].edge_defaults
881        } else {
882            &self.graph.edge_defaults
883        }
884    }
885
886    fn current_edge_defaults_mut(&mut self) -> &mut AttrMap {
887        if let Some(&subg_id) = self.scope_stack.last() {
888            &mut self.graph.subgraphs[subg_id.0 as usize].edge_defaults
889        } else {
890            &mut self.graph.edge_defaults
891        }
892    }
893}
894
895/// flex's `YY_BUF_SIZE`, which `aagerror` names as a possible cause.
896const YY_BUF_SIZE: usize = 16384;
897
898/// `aagerror` (`lib/cgraph/scan.l`) for bison's "syntax error" on lookahead token `tok`:
899/// the line the scanner is on once `tok` is read, then `near '<yytext>'`, or, when yytext
900/// is empty (end of input), the start condition the scanner was left in.
901fn syntax_error(tok: &Token) -> GraphvizError {
902    let mut message = format!("syntax error in line {}", tok.c_line);
903    if !tok.text.is_empty() {
904        message.push_str(&format!(" near '{}'", tok.text));
905    } else {
906        // `"%.80s"` of `Sbuf`: its first 80 bytes.
907        let first_80 = |s: &str| {
908            let bytes = graphviz_pure_core::charset::encode(s);
909            graphviz_pure_core::charset::decode_slice(&bytes[..bytes.len().min(80)])
910        };
911        match &tok.state {
912            ScanState::QString(sbuf) => {
913                message.push_str(&format!(
914                    " scanning a quoted string (missing endquote? longer than {YY_BUF_SIZE}?)"
915                ));
916                if !sbuf.is_empty() {
917                    message.push_str(&format!("\nString starting:\"{}", first_80(sbuf)));
918                }
919            }
920            ScanState::HString(sbuf) => {
921                message.push_str(&format!(
922                    " scanning a HTML string (missing '>'? bad nesting? longer than {YY_BUF_SIZE}?)"
923                ));
924                if !sbuf.is_empty() {
925                    message.push_str(&format!("\nString starting:<{}", first_80(sbuf)));
926                }
927            }
928            ScanState::Comment => message.push_str(&format!(
929                " scanning a /*...*/ comment (missing '*/? longer than {YY_BUF_SIZE}?)"
930            )),
931            ScanState::Initial => {}
932        }
933    }
934    GraphvizError::SyntaxError {
935        line: tok.c_line,
936        col: tok.col,
937        message,
938        file: tok.file.as_deref().map(str::to_string),
939    }
940}
941
942/// grammar.y `concatPort`: `name` or `name:compass`.
943fn port_to_str(p: &Port) -> String {
944    match (&p.name, &p.compass) {
945        (Some(n), Some(c)) => format!("{n}:{c}"),
946        (Some(n), None) => n.clone(),
947        (None, Some(c)) => c.clone(),
948        (None, None) => String::new(),
949    }
950}
951
952fn parse_port_str(s: &str) -> Port {
953    if s.is_empty() {
954        return Port::default();
955    }
956    if let Some((p1, p2)) = s.split_once(':') {
957        Port {
958            name: Some(p1.to_string()),
959            compass: Some(p2.to_string()),
960        }
961    } else {
962        Port {
963            name: Some(s.to_string()),
964            compass: None,
965        }
966    }
967}