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}