Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom heading adapter #266

Merged
merged 11 commits into from
Jan 9, 2023
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 23 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ readme = "README.md"
keywords = ["markdown", "commonmark"]
license = "BSD-2-Clause"
categories = ["text-processing", "parsing", "command-line-utilities"]
exclude = ["/hooks/*", "/script/*", "/vendor/*", "/.travis.yml", "/Makefile", "/spec_out.txt"]
exclude = [
"/hooks/*",
"/script/*",
"/vendor/*",
"/.travis.yml",
"/Makefile",
"/spec_out.txt",
]
resolver = "2"

[profile.release]
Expand All @@ -31,6 +38,7 @@ memchr = "2"
pest = "2"
pest_derive = "2"
shell-words = { version = "1.0", optional = true }
slug = "0.1.4"
emojis = { version = "0.5.2", optional = true }

[dev-dependencies]
Expand All @@ -48,9 +56,20 @@ shortcodes = ["emojis"]
xdg = { version = "^2.1", optional = true }

[target.'cfg(target_arch="wasm32")'.dependencies]
syntect = { version = "5.0", optional = true, default-features = false, features = ["default-fancy"] }
syntect = { version = "5.0", optional = true, default-features = false, features = [
"default-fancy",
] }
clap = { version = "4.0", optional = true, features = ["derive", "string"] }

[target.'cfg(not(target_arch="wasm32"))'.dependencies]
syntect = { version = "5.0", optional = true, default-features = false, features = ["default-themes", "default-syntaxes", "html", "regex-onig"] }
clap = { version = "4.0", optional = true, features = ["derive", "string", "wrap_help"] }
syntect = { version = "5.0", optional = true, default-features = false, features = [
"default-themes",
"default-syntaxes",
"html",
"regex-onig",
] }
clap = { version = "4.0", optional = true, features = [
"derive",
"string",
"wrap_help",
] }
51 changes: 51 additions & 0 deletions examples/custom_headings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
extern crate comrak;
extern crate slug;

use comrak::{
adapters::{HeadingAdapter, HeadingMeta},
markdown_to_html_with_plugins, ComrakOptions, ComrakPlugins,
};

fn main() {
let adapter = CustomHeadingAdapter;
let options = ComrakOptions::default();
let mut plugins = ComrakPlugins::default();
plugins.render.heading_adapter = Some(&adapter);

print_html(
"Some text.\n\n## Please hide me from search\n\nSome other text",
&options,
&plugins,
);
print_html(
"Some text.\n\n### Here is some `code`\n\nSome other text",
&options,
&plugins,
);
print_html(
"Some text.\n\n### Here is some **bold** text and some *italicized* text\n\nSome other text",
&options,
&plugins
);
print_html("# Here is a [link](/)", &options, &plugins);
}

struct CustomHeadingAdapter;

impl HeadingAdapter for CustomHeadingAdapter {
fn render(&self, heading: &HeadingMeta) -> String {
let id = slug::slugify(&heading.content);

let search_include = !&heading.content.contains("hide");

format!(
"<h{} id=\"{}\" data-search-include=\"{}\">",
heading.level, id, search_include
)
}
}

fn print_html(document: &str, options: &ComrakOptions, plugins: &ComrakPlugins) {
let html = markdown_to_html_with_plugins(document, options, plugins);
println!("{}", html);
}
16 changes: 16 additions & 0 deletions src/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,19 @@ pub trait SyntaxHighlighterAdapter {
/// `attributes`: A map of HTML attributes provided by comrak.
fn build_code_tag(&self, attributes: &HashMap<String, String>) -> String;
}

/// The struct passed to the `HeadingAdapter`.
#[derive(Clone, Debug)]
pub struct HeadingMeta {
/// The level of the header; from 1 to 6 for ATX headings, 1 or 2 for setext headings.
pub level: u32,

/// The text content of the heading.
pub content: String,
}

/// Implement this adapter for creating a plugin for custom headings.
pub trait HeadingAdapter {
/// The rendering function for headings.
fn render(&self, heading: &HeadingMeta) -> String;
}
52 changes: 36 additions & 16 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use std::io::{self, Write};
use std::str;
use strings::build_opening_tag;

use crate::adapters::HeadingMeta;

#[cfg(feature = "shortcodes")]
extern crate emojis;

Expand Down Expand Up @@ -457,22 +459,40 @@ impl<'o> HtmlFormatter<'o> {
}
NodeValue::Heading(ref nch) => {
if entering {
self.cr()?;
write!(self.output, "<h{}>", nch.level)?;

if let Some(ref prefix) = self.options.extension.header_ids {
let mut text_content = Vec::with_capacity(20);
self.collect_text(node, &mut text_content);

let mut id = String::from_utf8(text_content).unwrap();
id = self.anchorizer.anchorize(id);
write!(
self.output,
"<a href=\"#{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\"></a>",
id,
prefix,
id
)?;
match self.plugins.render.heading_adapter {
Some(adapter) => {
let level = nch.level;
let mut text_content = Vec::with_capacity(20);
self.collect_text(node, &mut text_content);

let content = &String::from_utf8(text_content).unwrap();

let heading = HeadingMeta {
level,
content: String::from(content),
};
lucperkins marked this conversation as resolved.
Show resolved Hide resolved

let rendered = adapter.render(&heading);
write!(self.output, "{}", rendered)?;
}
None => {
write!(self.output, "<h{}>", nch.level)?;
lucperkins marked this conversation as resolved.
Show resolved Hide resolved

if let Some(ref prefix) = self.options.extension.header_ids {
let mut text_content = Vec::with_capacity(20);
self.collect_text(node, &mut text_content);

let mut id = String::from_utf8(text_content).unwrap();
id = self.anchorizer.anchorize(id);
write!(
self.output,
"<a href=\"#{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\"></a>",
id,
prefix,
id
)?;
}
}
}
} else {
writeln!(self.output, "</h{}>", nch.level)?;
Expand Down
5 changes: 5 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use std::str;
use strings;
use typed_arena::Arena;

use crate::adapters::HeadingAdapter;

const TAB_STOP: usize = 4;
const CODE_INDENT: usize = 4;

Expand Down Expand Up @@ -529,6 +531,9 @@ pub struct ComrakRenderPlugins<'a> {
/// "<pre lang=\"rust\"><code class=\"language-rust\"><span class=\"lang-rust\">fn main<'a>();\n</span></code></pre>\n");
/// ```
pub codefence_syntax_highlighter: Option<&'a dyn SyntaxHighlighterAdapter>,

/// Optional heading adapter
pub heading_adapter: Option<&'a dyn HeadingAdapter>,
}

impl Debug for ComrakRenderPlugins<'_> {
Expand Down
1 change: 1 addition & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,7 @@ fn exercise_full_api<'a>() {
let _ = ::ComrakPlugins {
render: ::ComrakRenderPlugins {
codefence_syntax_highlighter: Some(&syntax_highlighter_adapter),
heading_adapter: None,
lucperkins marked this conversation as resolved.
Show resolved Hide resolved
},
};

Expand Down