| //! Renders the grammar to markdown. |
| |
| use super::{Characters, Expression, ExpressionKind, Production, RenderCtx}; |
| use crate::grammar::Grammar; |
| use anyhow::bail; |
| use regex::Regex; |
| use std::borrow::Cow; |
| use std::fmt::Write; |
| use std::sync::LazyLock; |
| |
| impl Grammar { |
| pub fn render_markdown( |
| &self, |
| cx: &RenderCtx, |
| names: &[&str], |
| output: &mut String, |
| ) -> anyhow::Result<()> { |
| let mut iter = names.into_iter().peekable(); |
| while let Some(name) = iter.next() { |
| let Some(prod) = self.productions.get(*name) else { |
| bail!("could not find grammar production named `{name}`"); |
| }; |
| prod.render_markdown(cx, output); |
| if iter.peek().is_some() { |
| output.push_str("\n"); |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| /// The HTML id for the production. |
| pub fn markdown_id(name: &str, for_summary: bool) -> String { |
| if for_summary { |
| format!("grammar-summary-{}", name) |
| } else { |
| format!("grammar-{}", name) |
| } |
| } |
| |
| impl Production { |
| fn render_markdown(&self, cx: &RenderCtx, output: &mut String) { |
| let dest = cx |
| .rr_link_map |
| .get(&self.name) |
| .map(|path| path.to_string()) |
| .unwrap_or_else(|| format!("missing")); |
| write!( |
| output, |
| "<span class=\"grammar-text grammar-production\" id=\"{id}\" \ |
| onclick=\"show_railroad()\"\ |
| >\ |
| [{name}]({dest})\ |
| </span> → ", |
| id = markdown_id(&self.name, cx.for_summary), |
| name = self.name, |
| ) |
| .unwrap(); |
| self.expression.render_markdown(cx, output); |
| output.push('\n'); |
| } |
| } |
| |
| impl Expression { |
| /// Returns the last [`ExpressionKind`] of this expression. |
| fn last(&self) -> &ExpressionKind { |
| match &self.kind { |
| ExpressionKind::Alt(es) | ExpressionKind::Sequence(es) => es.last().unwrap().last(), |
| ExpressionKind::Grouped(_) |
| | ExpressionKind::Optional(_) |
| | ExpressionKind::Repeat(_) |
| | ExpressionKind::RepeatNonGreedy(_) |
| | ExpressionKind::RepeatPlus(_) |
| | ExpressionKind::RepeatPlusNonGreedy(_) |
| | ExpressionKind::RepeatRange(_, _, _) |
| | ExpressionKind::Nt(_) |
| | ExpressionKind::Terminal(_) |
| | ExpressionKind::Prose(_) |
| | ExpressionKind::Break(_) |
| | ExpressionKind::Charset(_) |
| | ExpressionKind::NegExpression(_) |
| | ExpressionKind::Unicode(_) => &self.kind, |
| } |
| } |
| |
| fn render_markdown(&self, cx: &RenderCtx, output: &mut String) { |
| match &self.kind { |
| ExpressionKind::Grouped(e) => { |
| output.push_str("( "); |
| e.render_markdown(cx, output); |
| if !matches!(e.last(), ExpressionKind::Break(_)) { |
| output.push(' '); |
| } |
| output.push(')'); |
| } |
| ExpressionKind::Alt(es) => { |
| let mut iter = es.iter().peekable(); |
| while let Some(e) = iter.next() { |
| e.render_markdown(cx, output); |
| if iter.peek().is_some() { |
| if !matches!(e.last(), ExpressionKind::Break(_)) { |
| output.push(' '); |
| } |
| output.push_str("| "); |
| } |
| } |
| } |
| ExpressionKind::Sequence(es) => { |
| let mut iter = es.iter().peekable(); |
| while let Some(e) = iter.next() { |
| e.render_markdown(cx, output); |
| if iter.peek().is_some() && !matches!(e.last(), ExpressionKind::Break(_)) { |
| output.push(' '); |
| } |
| } |
| } |
| ExpressionKind::Optional(e) => { |
| e.render_markdown(cx, output); |
| output.push_str("<sup>?</sup>"); |
| } |
| ExpressionKind::Repeat(e) => { |
| e.render_markdown(cx, output); |
| output.push_str("<sup>\\*</sup>"); |
| } |
| ExpressionKind::RepeatNonGreedy(e) => { |
| e.render_markdown(cx, output); |
| output.push_str("<sup>\\* (non-greedy)</sup>"); |
| } |
| ExpressionKind::RepeatPlus(e) => { |
| e.render_markdown(cx, output); |
| output.push_str("<sup>+</sup>"); |
| } |
| ExpressionKind::RepeatPlusNonGreedy(e) => { |
| e.render_markdown(cx, output); |
| output.push_str("<sup>+ (non-greedy)</sup>"); |
| } |
| ExpressionKind::RepeatRange(e, a, b) => { |
| e.render_markdown(cx, output); |
| write!( |
| output, |
| "<sup>{}..{}</sup>", |
| a.map(|v| v.to_string()).unwrap_or_default(), |
| b.map(|v| v.to_string()).unwrap_or_default(), |
| ) |
| .unwrap(); |
| } |
| ExpressionKind::Nt(nt) => { |
| let dest = cx.md_link_map.get(nt).map_or("missing", |d| d.as_str()); |
| write!(output, "<span class=\"grammar-text\">[{nt}]({dest})</span>").unwrap(); |
| } |
| ExpressionKind::Terminal(t) => { |
| write!( |
| output, |
| "<span class=\"grammar-literal\">{}</span>", |
| markdown_escape(t) |
| ) |
| .unwrap(); |
| } |
| ExpressionKind::Prose(s) => { |
| write!(output, "<span class=\"grammar-text\">\\<{s}\\></span>").unwrap(); |
| } |
| ExpressionKind::Break(indent) => { |
| output.push_str("\\\n"); |
| output.push_str(&" ".repeat(*indent)); |
| } |
| ExpressionKind::Charset(set) => charset_render_markdown(cx, set, output), |
| ExpressionKind::NegExpression(e) => { |
| output.push('~'); |
| e.render_markdown(cx, output); |
| } |
| ExpressionKind::Unicode(s) => { |
| output.push_str("U+"); |
| output.push_str(s); |
| } |
| } |
| if let Some(suffix) = &self.suffix { |
| write!(output, "<sub class=\"grammar-text\">{suffix}</sub>").unwrap(); |
| } |
| if !cx.for_summary { |
| if let Some(footnote) = &self.footnote { |
| // The `ZeroWidthSpace` is to avoid conflicts with markdown link |
| // references. |
| write!(output, "​[^{footnote}]").unwrap(); |
| } |
| } |
| } |
| } |
| |
| fn charset_render_markdown(cx: &RenderCtx, set: &[Characters], output: &mut String) { |
| output.push_str("\\["); |
| let mut iter = set.iter().peekable(); |
| while let Some(chars) = iter.next() { |
| chars.render_markdown(cx, output); |
| if iter.peek().is_some() { |
| output.push(' '); |
| } |
| } |
| output.push(']'); |
| } |
| |
| impl Characters { |
| fn render_markdown(&self, cx: &RenderCtx, output: &mut String) { |
| match self { |
| Characters::Named(s) => { |
| let dest = cx.md_link_map.get(s).map_or("missing", |d| d.as_str()); |
| write!(output, "[{s}]({dest})").unwrap(); |
| } |
| Characters::Terminal(s) => write!( |
| output, |
| "<span class=\"grammar-literal\">{}</span>", |
| markdown_escape(s) |
| ) |
| .unwrap(), |
| Characters::Range(a, b) => write!( |
| output, |
| "<span class=\"grammar-literal\">{a}\ |
| </span>-<span class=\"grammar-literal\">{b}</span>" |
| ) |
| .unwrap(), |
| } |
| } |
| } |
| |
| /// Escapes characters that markdown would otherwise interpret. |
| fn markdown_escape(s: &str) -> Cow<'_, str> { |
| static ESC_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"[\\`_*\[\](){}'"]"#).unwrap()); |
| ESC_RE.replace_all(s, r"\$0") |
| } |