leo_abnf/
main.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17// ABNF PARSING RULES
18//
19// Header:
20// ```abnf
21// ; Introduction
22// ; -------------
23// ```
24//
25// Code block in docs (note double whitespace after colon):
26// ```abnf
27// ;  code
28// ;  code
29//```
30//
31// Rule:
32// ```abnf
33// address = "address"
34// ```
35//
36// Line:
37// ``` abnf
38// ;;;;;;;;;
39// ```
40//
41
42#![forbid(unsafe_code)]
43
44use abnf::types::{Node, Rule};
45use anyhow::{Result, anyhow};
46use std::collections::{HashMap, HashSet};
47
48/// Processor's scope. Used when code block or definition starts or ends.
49#[derive(Debug, Clone)]
50enum Scope {
51    Free,
52    Code,
53    Definition(Rule),
54}
55
56/// Transforms abnf file into Markdown.
57#[derive(Debug, Clone)]
58struct Processor<'a> {
59    rules: HashMap<String, Rule>,
60    grammar: &'a str,
61    scope: Scope,
62    line: u32,
63    out: String,
64}
65
66impl<'a> Processor<'a> {
67    fn new(grammar: &'a str, abnf: Vec<Rule>) -> Processor<'a> {
68        // we need a hashmap to pull rules easily
69        let rules: HashMap<String, Rule> = abnf.into_iter().map(|rule| (rule.name().to_string(), rule)).collect();
70
71        Processor { grammar, line: 0, out: String::new(), rules, scope: Scope::Free }
72    }
73
74    /// Main function for this struct.
75    /// Goes through each line and transforms it into proper markdown.
76    fn process(&mut self) {
77        let lines = self.grammar.lines();
78        let mut prev = "";
79
80        for line in lines {
81            self.line += 1;
82
83            // code block in comment (not highlighted as abnf)
84            if let Some(code) = line.strip_prefix(";  ") {
85                self.enter_scope(Scope::Code);
86                self.append_str(code);
87
88            // just comment. end of code block
89            } else if let Some(code) = line.strip_prefix("; ") {
90                self.enter_scope(Scope::Free);
91                self.append_str(code);
92
93            // horizontal rule - section separator
94            } else if line.starts_with(";;;;;;;;;;") {
95                self.enter_scope(Scope::Free);
96                self.append_str("\n--------\n");
97
98            // empty line in comment. end of code block
99            } else if line.starts_with(';') {
100                self.enter_scope(Scope::Free);
101                self.append_str("\n\n");
102
103            // just empty line. end of doc, start of definition
104            } else if line.is_empty() {
105                self.enter_scope(Scope::Free);
106                self.append_str("");
107
108            // definition (may be multiline)
109            } else {
110                // if there's an equality sign and previous line was empty
111                if line.contains('=') && prev.is_empty() {
112                    let (def, _) = line.split_at(line.find('=').unwrap());
113                    let def = def.trim();
114
115                    // try to find rule matching definition or fail
116                    let rule = self.rules.get(def).cloned().unwrap();
117
118                    self.enter_scope(Scope::Definition(rule));
119                }
120
121                self.append_str(line);
122            }
123
124            prev = line;
125        }
126    }
127
128    /// Append new line into output, add newline character.
129    fn append_str(&mut self, line: &str) {
130        self.out.push_str(line);
131        self.out.push('\n');
132    }
133
134    /// Enter new scope (definition or code block). Allows customizing
135    /// pre and post lines for each scope entered or exited.
136    fn enter_scope(&mut self, new_scope: Scope) {
137        match (&self.scope, &new_scope) {
138            // exchange scopes between Free and Code
139            (Scope::Free, Scope::Code) => self.append_str("```"),
140            (Scope::Code, Scope::Free) => self.append_str("```"),
141            // exchange scopes between Free and Definition
142            (Scope::Free, Scope::Definition(rule)) => {
143                self.append_str(&format!("<a name=\"{}\"></a>", rule.name()));
144                self.append_str("```abnf");
145            }
146            (Scope::Definition(rule), Scope::Free) => {
147                let mut rules: Vec<String> = Vec::new();
148                parse_abnf_node(rule.node(), &mut rules);
149
150                // 1. leave only unique keys
151                // 2. map each rule into a link
152                // 3. sort the links so they don't keep changing order
153                // 4. join results as a list
154                // Note: GitHub only allows custom tags with 'user-content-' prefix
155                let mut keyvec = rules
156                    .into_iter()
157                    .collect::<HashSet<_>>()
158                    .into_iter()
159                    .map(|tag| format!("[{}](#user-content-{tag})", &tag))
160                    .collect::<Vec<String>>();
161                keyvec.sort();
162                let keys = keyvec.join(", ");
163
164                self.append_str("```");
165                if !keys.is_empty() {
166                    self.append_str(&format!("\nGo to: _{keys}_;\n"));
167                }
168            }
169            (_, _) => (),
170        };
171
172        self.scope = new_scope;
173    }
174}
175
176/// Recursively parse ABNF Node and fill sum vec with found rule names.
177fn parse_abnf_node(node: &Node, sum: &mut Vec<String>) {
178    match node {
179        // these two are just vectors of rules
180        Node::Alternatives(vec) | Node::Concatenation(vec) => {
181            for node in vec {
182                parse_abnf_node(node, sum);
183            }
184        }
185        Node::Group(node) | Node::Optional(node) => parse_abnf_node(node.as_ref(), sum),
186
187        // push rulename if it is known
188        Node::Rulename(name) => sum.push(name.clone()),
189
190        // do nothing for other nodes
191        _ => (),
192    }
193}
194
195fn main() -> Result<()> {
196    // Take Leo ABNF grammar file.
197    let args: Vec<String> = std::env::args().collect();
198    let abnf_path = if let Some(path) = args.get(1) {
199        std::path::Path::new(path)
200    } else {
201        return Err(anyhow!("Usage Error: expects one argument to abnf file to convert."));
202    };
203    let grammar = std::fs::read_to_string(abnf_path)?;
204
205    // Parse ABNF to get list of all definitions.
206    // Rust ABNF does not provide support for `%s` (case sensitive strings, part of
207    // the standard); so we need to remove all occurrences before parsing.
208    let parsed = abnf::rulelist(&str::replace(&grammar, "%s", "")).map_err(|e| {
209        eprintln!("{}", &e);
210        anyhow::anyhow!(e)
211    })?;
212
213    // Init parser and run it. That's it.
214    let mut parser = Processor::new(&grammar, parsed);
215    parser.process();
216
217    // Print result of conversion to STDOUT.
218    println!("{}", parser.out);
219
220    Ok(())
221}