diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/examples/custom_code_blocks.rs b/examples/custom_code_blocks.rs new file mode 100644 index 00000000..d3051b21 --- /dev/null +++ b/examples/custom_code_blocks.rs @@ -0,0 +1,93 @@ +use comrak::{ + Options, adapters::CodefenceBlockAdapter, markdown_to_html_with_plugins, nodes::Sourcepos, + options::Plugins, +}; +use std::fmt::{self, Write}; + +fn main() { + let adapter = CustomCodeBlockAdapter; + let options = Options::default(); + let mut plugins = Plugins::default(); + plugins.render.codefence_block_renderer = Some(&adapter); + + print_html( + "Some prose.\n\n```rust\nfn main() {}\n```\n\nMore prose.", + &options, + &plugins, + ); + + print_html( + "```rust title=\"hello.rs\"\nfn greet() {\n println!(\"hi\");\n}\n```", + &options, + &plugins, + ); + + print_html("```\njust some plain code\n```", &options, &plugins); + + print_html("```sh\necho 'hello world'\n```", &options, &plugins); +} + +struct CustomCodeBlockAdapter; + +impl CodefenceBlockAdapter for CustomCodeBlockAdapter { + fn render( + &self, + output: &mut dyn Write, + lang: &str, + meta: &str, + literal: &str, + sourcepos: Option, + ) -> fmt::Result { + let frame = match lang { + "sh" | "bash" | "zsh" | "shell" | "console" => "terminal", + _ => "editor", + }; + + write!(output, "
")?; + + let title = extract_title(meta).unwrap_or(lang); + if !title.is_empty() { + write!(output, "
")?; + escape(output, title)?; + output.write_str("
")?; + } + + output.write_str("
")?;
+        escape(output, literal)?;
+        output.write_str("
")?; + + output.write_str("
") + } +} + +fn extract_title(meta: &str) -> Option<&str> { + let after = meta.strip_prefix("title=\"")?; + let end = after.find('"')?; + Some(&after[..end]) +} + +fn escape(output: &mut dyn Write, text: &str) -> fmt::Result { + for ch in text.chars() { + match ch { + '&' => output.write_str("&")?, + '<' => output.write_str("<")?, + '>' => output.write_str(">")?, + '"' => output.write_str(""")?, + c => output.write_char(c)?, + } + } + Ok(()) +} + +fn print_html(document: &str, options: &Options, plugins: &Plugins) { + let html = markdown_to_html_with_plugins(document, options, plugins); + println!("{}", html); +} diff --git a/src/adapters.rs b/src/adapters.rs index ab21ef64..56b5b74d 100644 --- a/src/adapters.rs +++ b/src/adapters.rs @@ -8,6 +8,19 @@ use std::fmt; use crate::nodes::Sourcepos; +/// TODO +pub trait CodefenceBlockAdapter { + /// TODO + fn render( + &self, + output: &mut dyn fmt::Write, + lang: &str, + meta: &str, + literal: &str, + sourcepos: Option, + ) -> std::fmt::Result; +} + /// Implement this adapter for custom rendering of codefence blocks. pub trait CodefenceRendererAdapter: Send + Sync { /// Render a codefence block. diff --git a/src/html.rs b/src/html.rs index 468eb985..bb1300a5 100644 --- a/src/html.rs +++ b/src/html.rs @@ -483,85 +483,93 @@ fn render_code_block( entering: bool, ncb: &NodeCodeBlock, ) -> Result { - if entering { - let info = &ncb.info; - let info_bytes = info.as_bytes(); - let mut first_tag = 0; + if !entering { + return Ok(ChildRendering::HTML); + } - while first_tag < info.len() && !isspace(info_bytes[first_tag]) { - first_tag += 1; - } + let info = &ncb.info; + let info_bytes = info.as_bytes(); + let mut first_tag = 0; - let lang = &info[..first_tag]; - let meta = info[first_tag..].trim(); + while first_tag < info.len() && !isspace(info_bytes[first_tag]) { + first_tag += 1; + } - if lang.eq("math") { - render_math_code_block(context, node, &ncb.literal)?; - } else if !lang.is_empty() { - if let Some(adapter) = context.plugins.render.codefence_renderers.get(lang) { - context.cr()?; - let sourcepos = if context.options.render.sourcepos { - Some(node.data().sourcepos) - } else { - None - }; + let lang = &info[..first_tag]; + let meta = info[first_tag..].trim(); - adapter.write(context, lang, meta, &ncb.literal, sourcepos)?; - return Ok(ChildRendering::HTML); - } - } + let sourcepos = if context.options.render.sourcepos { + Some(node.data().sourcepos) + } else { + None + }; - if !lang.eq("math") { - context.cr()?; + // Full-block adapter takes precedence over everything else. + if let Some(adapter) = context.plugins.render.codefence_block_renderer { + context.cr()?; + let out: &mut dyn fmt::Write = context; + adapter.render(out, lang, meta, &ncb.literal, sourcepos)?; + context.lf()?; + return Ok(ChildRendering::HTML); + } + + // Built-in math rendering. + if lang == "math" { + render_math_code_block(context, node, &ncb.literal)?; + return Ok(ChildRendering::HTML); + } - let mut pre_attributes: HashMap<&str, Cow> = HashMap::new(); - let mut code_attributes: HashMap<&str, Cow> = HashMap::new(); - let code_attr: String; + // Per-language renderer. + if !lang.is_empty() { + if let Some(adapter) = context.plugins.render.codefence_renderers.get(lang) { + context.cr()?; + adapter.write(context, lang, meta, &ncb.literal, sourcepos)?; + return Ok(ChildRendering::HTML); + } + } - let literal = &ncb.literal; + // Default
 rendering, optionally passed through a syntax highlighter.
+    context.cr()?;
 
-            if !info.is_empty() {
-                if context.options.render.github_pre_lang {
-                    pre_attributes.insert("lang", lang.into());
+    let mut pre_attributes: HashMap<&str, Cow> = HashMap::new();
+    let mut code_attributes: HashMap<&str, Cow> = HashMap::new();
+    let code_attr: String;
 
-                    if context.options.render.full_info_string && !meta.is_empty() {
-                        pre_attributes.insert("data-meta", meta.trim().into());
-                    }
-                } else {
-                    code_attr = format!("language-{}", lang);
-                    code_attributes.insert("class", code_attr.into());
+    if !info.is_empty() {
+        if context.options.render.github_pre_lang {
+            pre_attributes.insert("lang", lang.into());
 
-                    if context.options.render.full_info_string && !meta.is_empty() {
-                        code_attributes.insert("data-meta", meta.into());
-                    }
-                }
+            if context.options.render.full_info_string && !meta.is_empty() {
+                pre_attributes.insert("data-meta", meta.trim().into());
             }
+        } else {
+            code_attr = format!("language-{}", lang);
+            code_attributes.insert("class", code_attr.into());
 
-            if context.options.render.sourcepos {
-                let ast = node.data();
-                pre_attributes.insert("data-sourcepos", ast.sourcepos.to_string().into());
+            if context.options.render.full_info_string && !meta.is_empty() {
+                code_attributes.insert("data-meta", meta.into());
             }
+        }
+    }
 
-            match context.plugins.render.codefence_syntax_highlighter {
-                None => {
-                    write_opening_tag(context, "pre", pre_attributes.into_iter())?;
-                    write_opening_tag(context, "code", code_attributes.into_iter())?;
-
-                    context.escape(literal)?;
-
-                    context.write_str("
")?; - context.lf()? - } - Some(highlighter) => { - highlighter.write_pre_tag(context, pre_attributes)?; - highlighter.write_code_tag(context, code_attributes)?; - - highlighter.write_highlighted(context, Some(lang), &ncb.literal)?; + if context.options.render.sourcepos { + pre_attributes.insert("data-sourcepos", node.data().sourcepos.to_string().into()); + } - context.write_str("")?; - context.lf()? - } - } + match context.plugins.render.codefence_syntax_highlighter { + None => { + write_opening_tag(context, "pre", pre_attributes.into_iter())?; + write_opening_tag(context, "code", code_attributes.into_iter())?; + context.escape(&ncb.literal)?; + context.write_str("")?; + context.lf()?; + } + Some(highlighter) => { + highlighter.write_pre_tag(context, pre_attributes)?; + highlighter.write_code_tag(context, code_attributes)?; + highlighter.write_highlighted(context, Some(lang), &ncb.literal)?; + context.write_str("")?; + context.lf()?; } } diff --git a/src/parser/options.rs b/src/parser/options.rs index 9c6a1a2e..6f5af6e6 100644 --- a/src/parser/options.rs +++ b/src/parser/options.rs @@ -8,7 +8,9 @@ use std::panic::RefUnwindSafe; use std::str; use std::sync::Arc; -use crate::adapters::{CodefenceRendererAdapter, HeadingAdapter, SyntaxHighlighterAdapter}; +use crate::adapters::{ + CodefenceBlockAdapter, CodefenceRendererAdapter, HeadingAdapter, SyntaxHighlighterAdapter, +}; use crate::parser::ResolvedReference; #[derive(Default, Debug, Clone)] @@ -1314,6 +1316,9 @@ pub struct Plugins<'p> { #[cfg_attr(feature = "bon", derive(Builder))] /// Plugins for alternative rendering. pub struct RenderPlugins<'p> { + /// TODO + pub codefence_block_renderer: Option<&'p dyn CodefenceBlockAdapter>, + /// Provide language-specific renderers for codefence blocks. /// /// `math` codefence blocks are handled separately by Comrak's built-in math renderer, diff --git a/src/tests/plugins.rs b/src/tests/plugins.rs index e8aa5940..3c8b14de 100644 --- a/src/tests/plugins.rs +++ b/src/tests/plugins.rs @@ -64,9 +64,9 @@ fn language_specific_codefence_renderer_plugin() { code: &str, _sourcepos: Option, ) -> std::fmt::Result { - write!( + writeln!( output, - "
{code}
\n" + "
{code}
" ) } } @@ -98,7 +98,7 @@ fn language_specific_codefence_renderer_precedes_highlighter() { code: &str, _sourcepos: Option, ) -> std::fmt::Result { - write!(output, "
{code}
\n") + writeln!(output, "
{code}
") } } @@ -158,9 +158,9 @@ fn language_specific_codefence_renderer_receives_sourcepos() { sourcepos: Option, ) -> std::fmt::Result { let sourcepos = sourcepos.expect("sourcepos should be passed to adapter"); - write!( + writeln!( output, - "
{code}
\n" + "
{code}
" ) } }