blob: 576accd2d6bdbf92f38a30767d3a012f7f6ff467 [file] [log] [blame] [edit]
//! Support for rendering the grammar.
use diagnostics::{Diagnostics, warn_or_err};
use grammar::{GRAMMAR_RE, Grammar};
use mdbook_preprocessor::book::Chapter;
use regex::{Captures, Regex};
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::sync::LazyLock;
mod render_markdown;
mod render_railroad;
static NAMES_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)^(?:@root )?([A-Za-z0-9_]+)(?: \([^)]+\))? ->").unwrap());
#[derive(Debug)]
pub struct RenderCtx {
md_link_map: HashMap<String, String>,
rr_link_map: HashMap<String, String>,
for_summary: bool,
}
/// Replaces the text grammar in the given chapter with the rendered version.
pub fn insert_grammar(grammar: &Grammar, chapter: &Chapter, diag: &mut Diagnostics) -> String {
let link_map = make_relative_link_map(grammar, chapter);
let mut content = GRAMMAR_RE
.replace_all(&chapter.content, |cap: &Captures<'_>| {
let names: Vec<_> = NAMES_RE
.captures_iter(&cap[2])
.map(|cap| cap.get(1).unwrap().as_str())
.collect();
let for_lexer = &cap[1] == "lexer";
render_names(grammar, &names, &link_map, for_lexer, chapter, diag)
})
.to_string();
// Make all production names easily linkable.
let is_summary = is_summary(chapter);
for (name, path) in &link_map {
let id = render_markdown::markdown_id(name, is_summary);
if is_summary {
// On the summary page, link to the production on the summary page.
writeln!(content, "[{name}]: #{id}").unwrap();
} else {
// This includes two variants, one for convenience (like
// `[ArrayExpression]`), and one with the `grammar-` prefix to
// disambiguate links that have the same name as a rule (rules
// take precedence).
writeln!(
content,
"[{name}]: {path}#{id}\n\
[grammar-{name}]: {path}#{id}"
)
.unwrap();
}
}
content
}
/// Creates a map of production name -> relative link path.
fn make_relative_link_map(grammar: &Grammar, chapter: &Chapter) -> HashMap<String, String> {
let current_path = chapter.path.as_ref().unwrap().parent().unwrap();
grammar
.productions
.values()
.map(|p| {
let relative = pathdiff::diff_paths(&p.path, current_path).unwrap();
// Adjust paths for Windows.
let relative = relative.display().to_string().replace('\\', "/");
(p.name.clone(), relative)
})
.collect()
}
/// Helper to take a list of production names and to render all of those to a
/// mixture of markdown and HTML.
fn render_names(
grammar: &Grammar,
names: &[&str],
link_map: &HashMap<String, String>,
for_lexer: bool,
chapter: &Chapter,
diag: &mut Diagnostics,
) -> String {
let for_summary = is_summary(chapter);
let mut output = String::new();
output.push_str(
"<div class=\"grammar-container\">\n\
\n",
);
if for_lexer {
output.push_str("**<sup>Lexer</sup>**\n");
} else {
output.push_str("**<sup>Syntax</sup>**\n");
}
output.push_str("<br>\n");
// Convert the link map to add the id.
let update_link_map = |get_id: fn(&str, bool) -> String| -> HashMap<String, String> {
link_map
.iter()
.map(|(name, path)| {
let id = get_id(name, for_summary);
let path = if for_summary {
format!("#{id}")
} else {
format!("{path}#{id}")
};
(name.clone(), path)
})
.collect()
};
let render_ctx = RenderCtx {
md_link_map: update_link_map(render_markdown::markdown_id),
rr_link_map: update_link_map(render_railroad::railroad_id),
for_summary,
};
if let Err(e) = render_markdown::render_markdown(grammar, &render_ctx, &names, &mut output) {
warn_or_err!(
diag,
"grammar failed in chapter {:?}: {e}",
chapter.source_path.as_ref().unwrap()
);
}
output.push_str(
"\n\
<button class=\"grammar-toggle-railroad\" type=\"button\" \
title=\"Toggle railroad display\" \
onclick=\"toggle_railroad()\">\
Show Railroad\
</button>\n\
</div>\n\
<div class=\"grammar-railroad grammar-hidden\">\n\
\n",
);
if let Err(e) = render_railroad::render_railroad(grammar, &render_ctx, &names, &mut output) {
warn_or_err!(
diag,
"grammar failed in chapter {:?}: {e}",
chapter.source_path.as_ref().unwrap()
);
}
output.push_str("</div>\n");
output
}
pub fn is_summary(chapter: &Chapter) -> bool {
chapter.name == "Grammar summary"
}
/// Inserts the summary of all grammar rules into the grammar summary chapter.
pub fn insert_summary(grammar: &Grammar, chapter: &Chapter, diag: &mut Diagnostics) -> String {
let link_map = make_relative_link_map(grammar, chapter);
let mut seen = HashSet::new();
let categories: Vec<_> = grammar
.name_order
.iter()
.map(|name| &grammar.productions[name].category)
.filter(|cat| seen.insert(*cat))
.collect();
let mut grammar_summary = String::new();
for category in categories {
let mut chars = category.chars();
let cap = chars.next().unwrap().to_uppercase().collect::<String>() + chars.as_str();
write!(grammar_summary, "\n## {cap} summary\n\n").unwrap();
let names: Vec<_> = grammar
.name_order
.iter()
.filter(|name| grammar.productions[*name].category == *category)
.map(|s| s.as_str())
.collect();
let for_lexer = category == "lexer";
let s = render_names(grammar, &names, &link_map, for_lexer, chapter, diag);
grammar_summary.push_str(&s);
}
chapter
.content
.replace("{{ grammar-summary }}", &grammar_summary)
}