Skip to content

Commit

Permalink
Merge pull request #266 from lucperkins/heading-adapter
Browse files Browse the repository at this point in the history
Add custom heading adapter
  • Loading branch information
kivikakk authored Jan 9, 2023
2 parents 4c3d3f0 + a11bde4 commit 2e219a9
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 30 deletions.
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",
] }
55 changes: 55 additions & 0 deletions examples/custom_headings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 enter(&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 exit(&self, heading: &HeadingMeta) -> String {
format!("</h{}>", heading.level)
}
}

fn print_html(document: &str, options: &ComrakOptions, plugins: &ComrakPlugins) {
let html = markdown_to_html_with_plugins(document, options, plugins);
println!("{}", html);
}
24 changes: 24 additions & 0 deletions src/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,27 @@ 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`] for custom heading implementations.
#[derive(Clone, Debug)]
pub struct HeadingMeta {
/// The level of the heading; from 1 to 6 for ATX headings, 1 or 2 for setext headings.
pub level: u8,

/// The content of the heading as a "flattened" string&mdash;flattened in the sense that any
/// `<strong>` or other tags are removed. In the Markdown heading `## This is **bold**`, for
/// example, the would be the string `"This is bold"`.
pub content: String,
}

/// Implement this adapter for creating a plugin for custom headings (`h1`, `h2`, etc.). The `enter`
/// method defines what's rendered prior the AST content of the heading while the `exit` method
/// defines what's rendered after it. Both methods provide access to a [`HeadingMeta`] struct and
/// leave the AST content of the heading unchanged.
pub trait HeadingAdapter {
/// Called prior to rendering
fn enter(&self, heading: &HeadingMeta) -> String;

/// Close tags.
fn exit(&self, heading: &HeadingMeta) -> String;
}
62 changes: 41 additions & 21 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 @@ -455,29 +457,47 @@ impl<'o> HtmlFormatter<'o> {
self.output.write_all(b"</dd>\n")?;
}
}
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
)?;
NodeValue::Heading(ref nch) => match self.plugins.render.heading_adapter {
None => {
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
)?;
}
} else {
writeln!(self.output, "</h{}>", nch.level)?;
}
} else {
writeln!(self.output, "</h{}>", nch.level)?;
}
}
Some(adapter) => {
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: nch.level,
content,
};

if entering {
self.cr()?;
write!(self.output, "{}", adapter.enter(&heading))?;
} else {
write!(self.output, "{}", adapter.exit(&heading))?;
}
}
},
NodeValue::CodeBlock(ref ncb) => {
if entering {
self.cr()?;
Expand Down
2 changes: 1 addition & 1 deletion src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ pub struct NodeCodeBlock {
#[derive(Default, Debug, Clone, Copy)]
pub struct NodeHeading {
/// The level of the header; from 1 to 6 for ATX headings, 1 or 2 for setext headings.
pub level: u32,
pub level: u8,

/// Whether the heading is setext (if not, ATX).
pub setext: bool,
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
54 changes: 50 additions & 4 deletions src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::nodes::{AstNode, NodeCode, NodeValue};
use crate::{
adapters::{HeadingAdapter, HeadingMeta},
nodes::{AstNode, NodeCode, NodeValue},
};
use adapters::SyntaxHighlighterAdapter;
use cm;
use html;
Expand Down Expand Up @@ -220,6 +223,38 @@ fn syntax_highlighter_plugin() {
html_plugins(input, expected, &plugins);
}

#[test]
fn heading_adapter_plugin() {
struct MockAdapter;

impl HeadingAdapter for MockAdapter {
fn enter(&self, heading: &HeadingMeta) -> String {
format!("<h{} data-heading=\"true\">", heading.level + 1)
}

fn exit(&self, heading: &HeadingMeta) -> String {
format!("</h{}>", heading.level + 1)
}
}

let mut plugins = ::ComrakPlugins::default();
let adapter = MockAdapter {};
plugins.render.heading_adapter = Some(&adapter);

let cases: Vec<(&str, &str)> = vec![
("# Simple heading", "<h2 data-heading=\"true\">Simple heading</h2>"),
(
"## Heading with **bold text** and `code`",
"<h3 data-heading=\"true\">Heading with <strong>bold text</strong> and <code>code</code></h3>",
),
("###### Whoa, an h7!", "<h7 data-heading=\"true\">Whoa, an h7!</h7>"),
("####### This is not a heading", "<p>####### This is not a heading</p>\n")
];
for (input, expected) in cases {
html_plugins(input, expected, &plugins);
}
}

#[test]
#[cfg(feature = "syntect")]
fn syntect_plugin() {
Expand Down Expand Up @@ -1393,11 +1428,22 @@ fn exercise_full_api<'a>() {
}
}

let syntax_highlighter_adapter = MockAdapter {};
impl HeadingAdapter for MockAdapter {
fn enter(&self, heading: &HeadingMeta) -> String {
format!("<h{}>", heading.level)
}

fn exit(&self, heading: &HeadingMeta) -> String {
format!("</h{}>", heading.level)
}
}

let mock_adapter = MockAdapter {};

let _ = ::ComrakPlugins {
render: ::ComrakRenderPlugins {
codefence_syntax_highlighter: Some(&syntax_highlighter_adapter),
codefence_syntax_highlighter: Some(&mock_adapter),
heading_adapter: Some(&mock_adapter),
},
};

Expand Down Expand Up @@ -1440,7 +1486,7 @@ fn exercise_full_api<'a>() {
}
::nodes::NodeValue::Paragraph => {}
::nodes::NodeValue::Heading(nh) => {
let _: u32 = nh.level;
let _: u8 = nh.level;
let _: bool = nh.setext;
}
::nodes::NodeValue::ThematicBreak => {}
Expand Down

0 comments on commit 2e219a9

Please sign in to comment.