1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#[macro_use]
extern crate pest_derive;
use railroad as rr;

use pest::iterators::Pair;
use pest::Parser;
use railroad::svg;

#[derive(Parser)]
#[grammar = "parser.pest"]
struct RRParser;

pub struct Diagram {
    pub width: i64,
    pub height: i64,
    pub diagram: rr::Diagram<Box<dyn rr::Node>>,
}

fn unescape(pair: &Pair<'_, Rule>) -> String {
    let s = pair.as_str();
    let mut result = String::with_capacity(s.len());
    let mut iter = s[1..s.len() - 1].chars();
    while let Some(ch) = iter.next() {
        result.push(match ch {
            '\\' => iter.next().expect("no escaped char?"),
            _ => ch,
        });
    }
    result
}

fn binary<F, T>(pair: Pair<'_, Rule>, f: F) -> Box<dyn rr::Node>
where
    T: rr::Node + 'static,
    F: FnOnce(Box<dyn rr::Node>, Pair<'_, Rule>) -> T,
{
    let mut inner = pair.into_inner();
    let node = make_node(inner.next().expect("pair cannot be empty"));
    if let Some(pair) = inner.next() {
        Box::new(f(node, pair))
    } else {
        node
    }
}

fn make_node(pair: Pair<'_, Rule>) -> Box<dyn rr::Node> {
    use Rule::*;
    match pair.as_rule() {
        term => Box::new(rr::Terminal::new(unescape(&pair))),
        nonterm => Box::new(rr::NonTerminal::new(unescape(&pair))),
        comment => Box::new(rr::Comment::new(unescape(&pair))),
        empty => Box::new(rr::Empty),
        sequence => Box::new(rr::Sequence::new(
            pair.into_inner().map(make_node).collect(),
        )),
        stack => Box::new(rr::Stack::new(pair.into_inner().map(make_node).collect())),
        choice => Box::new(rr::Choice::new(pair.into_inner().map(make_node).collect())),
        opt_expr => binary(pair, |node, _| rr::Optional::new(node)),
        rpt_expr => binary(pair, |first, second| {
            rr::Repeat::new(first, make_node(second))
        }),
        lbox_expr => binary(pair, |first, second| {
            rr::LabeledBox::new(first, make_node(second))
        }),
        _ => unreachable!(),
    }
}

fn start_to_end(root: Box<dyn rr::Node>) -> Box<dyn rr::Node> {
    Box::new(rr::Sequence::new(vec![
        Box::new(rr::SimpleStart) as Box<dyn rr::Node>,
        root,
        Box::new(rr::SimpleEnd),
    ]))
}

pub fn compile(src: &str, css: &str) -> Result<Diagram, Box<pest::error::Error<Rule>>> {
    let mut result = RRParser::parse(Rule::input, src)?;
    let trees = result.next().expect("expected root_expr").into_inner();
    let mut trees: Vec<_> = trees.map(|p| start_to_end(make_node(p))).collect();
    let root = if trees.len() == 1 {
        trees.remove(0)
    } else {
        Box::new(rr::VerticalGrid::new(trees))
    };

    let mut diagram = rr::Diagram::new(root);
    diagram.add_element(
        svg::Element::new("style")
            .set("type", "text/css")
            .raw_text(css),
    );

    let width = (&diagram as &dyn rr::Node).width();
    let height = (&diagram as &dyn rr::Node).height();
    Ok(Diagram {
        width,
        height,
        diagram,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use railroad::DEFAULT_CSS;
    use std::env;
    use std::fs;
    use std::io::Read;
    use std::path;

    #[test]
    fn examples_must_parse() {
        let home = env::var_os("CARGO_MANIFEST_DIR").unwrap();
        let mut exmpl_dir = path::PathBuf::from(home);
        exmpl_dir.push("examples");
        for path in fs::read_dir(exmpl_dir).unwrap().filter_map(Result::ok) {
            if let Some(filename) = path.file_name().to_str() {
                if filename.ends_with("diagram.txt") {
                    eprintln!("Compiling `{filename}`");
                    let mut buffer = String::new();
                    fs::File::open(path.path())
                        .unwrap()
                        .read_to_string(&mut buffer)
                        .unwrap();
                    if let Err(e) = compile(&buffer, DEFAULT_CSS) {
                        panic!("Failed to compile {}", e.with_path(filename));
                    }
                }
            }
        }
    }
}