#![doc = include_str!("../README.md")]
use rushdown::as_kind_data;
use rushdown::ast::Arena;
use rushdown::ast::CodeBlock;
use rushdown::ast::NodeRef;
use rushdown::ast::WalkStatus;
use rushdown::renderer;
use rushdown::renderer::html;
use rushdown::renderer::html::Renderer;
use rushdown::renderer::html::RendererExtension;
use rushdown::renderer::html::RendererExtensionFn;
use rushdown::renderer::BoxRenderNode;
use rushdown::renderer::NodeRenderer;
use rushdown::renderer::NodeRendererRegistry;
use rushdown::renderer::RenderNode;
use rushdown::renderer::RendererOptions;
use rushdown::renderer::TextWrite;
use rushdown::Result;
use std::any::TypeId;
use std::rc::Rc;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::html::css_for_theme_with_class_style;
use syntect::html::styled_line_to_highlighted_html;
use syntect::html::ClassStyle;
use syntect::html::ClassedHTMLGenerator;
use syntect::html::IncludeBackground;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
#[derive(Debug, Clone)]
pub struct HighlightingHtmlRendererOptions {
pub theme: &'static str,
pub mode: HighlightingMode,
pub theme_set: Option<Rc<ThemeSet>>,
}
impl Default for HighlightingHtmlRendererOptions {
fn default() -> Self {
Self {
theme: "InspiredGitHub",
mode: HighlightingMode::default(),
theme_set: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum HighlightingMode {
#[default]
Attribute,
Class,
}
pub fn generate_css(theme: &str, theme_set: Option<&ThemeSet>) -> Option<String> {
if let Some(ts) = theme_set {
let theme = ts.themes.get(theme)?;
css_for_theme_with_class_style(theme, ClassStyle::Spaced).ok()
} else {
let ts = ThemeSet::load_defaults();
let theme = ts.themes.get(theme)?;
css_for_theme_with_class_style(theme, ClassStyle::Spaced).ok()
}
}
impl RendererOptions for HighlightingHtmlRendererOptions {}
#[allow(dead_code)]
struct HighlightingHtmlRenderer<W: TextWrite> {
_phantom: core::marker::PhantomData<W>,
writer: html::Writer,
options: HighlightingHtmlRendererOptions,
syntax_set: SyntaxSet,
default_theme_set: ThemeSet,
}
impl<W: TextWrite> HighlightingHtmlRenderer<W> {
fn with_options(options: HighlightingHtmlRendererOptions, html_opts: html::Options) -> Self {
Self {
_phantom: core::marker::PhantomData,
writer: html::Writer::with_options(html_opts),
syntax_set: SyntaxSet::load_defaults_newlines(),
default_theme_set: ThemeSet::load_defaults(),
options,
}
}
fn render_code_to_html_attr(
&self,
language: &str,
code: &str,
theme_name: &str,
) -> Option<String> {
let ps = &self.syntax_set;
let ts = if let Some(ref theme_set) = self.options.theme_set {
theme_set
} else {
&self.default_theme_set
};
let theme = ts
.themes
.get(theme_name)
.unwrap_or_else(|| &ts.themes["InspiredGitHub"]);
let lang = if language.is_empty() {
"plaintext"
} else {
language
};
let syntax = ps
.find_syntax_by_token(lang)
.or_else(|| ps.find_syntax_by_extension(lang))
.unwrap_or_else(|| ps.find_syntax_plain_text());
let bg = theme
.settings
.background
.map(|c| format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b))
.unwrap_or_else(|| "#ffffff".to_string());
let mut out = String::new();
out.push_str(&format!(
r#"<pre style="background-color: {}; padding: 12px; overflow: auto;"><code class="language-{}">"#,
bg, language
));
let mut h = HighlightLines::new(syntax, theme);
for line in LinesWithEndings::from(code) {
let regions = h.highlight_line(line, ps).ok()?;
let html_line =
styled_line_to_highlighted_html(®ions[..], IncludeBackground::No).ok()?;
out.push_str(&html_line);
}
out.push_str("</code></pre>\n");
Some(out)
}
fn render_with_classes(&self, language: &str, code: &str) -> Option<String> {
let ps = &self.syntax_set;
let lang = if language.is_empty() {
"plaintext"
} else {
language
};
let syntax = ps
.find_syntax_by_token(lang)
.or_else(|| ps.find_syntax_by_extension(lang))
.unwrap_or_else(|| ps.find_syntax_plain_text());
let mut html_gen =
ClassedHTMLGenerator::new_with_class_style(syntax, ps, ClassStyle::Spaced);
let mut html = String::new();
html.push_str(&format!(
r#"<pre class="code"><code class="language-{}">"#,
language
));
for line in LinesWithEndings::from(code) {
html_gen
.parse_html_for_line_which_includes_newline(line)
.ok()?;
}
html.push_str(&html_gen.finalize());
html.push_str("</code></pre>\n");
Some(html)
}
}
impl<W: TextWrite> RenderNode<W> for HighlightingHtmlRenderer<W> {
fn render_node<'a>(
&self,
w: &mut W,
source: &'a str,
arena: &'a Arena,
node_ref: NodeRef,
entering: bool,
_ctx: &mut renderer::Context,
) -> Result<WalkStatus> {
if entering {
let kd = as_kind_data!(arena, node_ref, CodeBlock);
let mut code = String::new();
for line in kd.value().iter(source) {
code.push_str(&line);
}
let lang = kd.language_str(source).unwrap_or("plaintext");
match self.options.mode {
HighlightingMode::Attribute => {
if let Some(html) =
self.render_code_to_html_attr(lang, &code, self.options.theme)
{
w.write_str(&html)?;
return Ok(WalkStatus::Continue);
}
}
HighlightingMode::Class => {
if let Some(html) = self.render_with_classes(lang, &code) {
w.write_str(&html)?;
return Ok(WalkStatus::Continue);
}
}
}
self.writer.write_safe_str(w, "<pre><code")?;
if let Some(lang) = kd.language_str(source) {
self.writer.write_safe_str(w, " class=\"language-")?;
self.writer.write(w, lang)?;
self.writer.write_safe_str(w, "\"")?;
}
self.writer.write_safe_str(w, ">")?;
for line in kd.value().iter(source) {
self.writer.raw_write(w, &line)?;
}
self.writer.write_safe_str(w, "</code></pre>\n")?;
}
Ok(WalkStatus::Continue)
}
}
impl<'r, W> NodeRenderer<'r, W> for HighlightingHtmlRenderer<W>
where
W: TextWrite + 'r,
{
fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'r, W>) {
nrr.register_node_renderer_fn(TypeId::of::<CodeBlock>(), BoxRenderNode::new(self));
}
}
pub fn highlighting_html_renderer_extension<'cb, W>(
options: impl Into<HighlightingHtmlRendererOptions>,
) -> impl RendererExtension<'cb, W>
where
W: TextWrite + 'cb,
{
RendererExtensionFn::new(move |r: &mut Renderer<'cb, W>| {
let options = options.into();
r.add_node_renderer(HighlightingHtmlRenderer::with_options, options);
})
}