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

Create custom HTML formatters. #540

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

Create custom HTML formatters. #540

wants to merge 13 commits into from

Conversation

kivikakk
Copy link
Owner

o/ @digitalmoksha

See examples/custom_formatter.rs for what this looks like when being used from a different crate.

I've refactored some things out, added the new html::Context struct (which contains everything HtmlFormatter itself used to), moved WriteWithLast's functionality into that, added html::RenderMode (instead of the plain boolean).

Still need to document newly public things!

Let me know your thoughts.

Copy link
Contributor

Command Mean [ms] Min [ms] Max [ms] Relative
./bench.sh ./comrak-27e8928 319.1 ± 2.3 315.3 323.5 2.93 ± 0.03
./bench.sh ./comrak-main 318.0 ± 2.8 314.5 324.9 2.92 ± 0.04
./bench.sh ./pulldown-cmark 109.0 ± 1.0 107.8 112.1 1.00
./bench.sh ./cmark-gfm 115.2 ± 1.5 112.7 117.9 1.06 ± 0.02
./bench.sh ./markdown-it 480.4 ± 5.3 475.5 501.6 4.41 ± 0.06

Run on Sat Feb 22 02:06:27 UTC 2025

@digitalmoksha
Copy link
Collaborator

Looking pretty good! I think we're going to need to have the context and the node available within the formatter in order to be able to do certain overrides. It doesn't look like that's exposed at the moment.

@digitalmoksha
Copy link
Collaborator

Love it, you've really separated out the macro, it's getting pretty clean.

@kivikakk
Copy link
Owner Author

OK! Keep in mind you may not actually always need the node itself (i.e. the AstNode); a lot of the time what you want will be available in the pattern-matched part.

I've updated this to now expose a variety of things, depending on the names (!) of the captures. I've also added the ability to suppress children from being rendered, so you can completely take control of a subtree. The updated example shows off all the captures available:

create_formatter!(CustomFormatter, {
    NodeValue::Emph => |output, entering| {
        if entering {
            output.write_all(b"<i>")?;
        } else {
            output.write_all(b"</i>")?;
        }
    },
    NodeValue::Strong => |context, entering| {
        use std::io::Write;
        context.write_all(if entering { b"<b>" } else { b"</b>" })?;
    },
    NodeValue::Image(ref nl) => |output, node, entering, suppress_children| {
        assert!(node.data.borrow().sourcepos == (3, 1, 3, 18).into());
        if entering {
            output.write_all(nl.url.to_uppercase().as_bytes())?;
            *suppress_children = true;
        }
    },
});

@digitalmoksha
Copy link
Collaborator

Looks good. I'm frazzled for the night. I'll have a chance to look more closely on Sun. But really, I think it's about ready to go.

@kivikakk
Copy link
Owner Author

👍 I'll finish off the documentation; let me know, do try taking it for an actual spin (using a Git branch dependency) before we move to merge!

@digitalmoksha
Copy link
Collaborator

I've already got one in progress 😄

Copy link
Collaborator

@digitalmoksha digitalmoksha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kivikakk this is really good! Really appreciate you working on this and fleshing it out. I just had a couple minor comments.

I picked overriding NodeValue::Heading because I actually need to modify it to add aria-describedby to give better assistance support.

@kivikakk kivikakk force-pushed the custom-html-formatter branch 4 times, most recently from 9403862 to aac8059 Compare February 25, 2025 06:09
@kivikakk kivikakk force-pushed the custom-html-formatter branch from aac8059 to 78467cc Compare February 25, 2025 06:20
@digitalmoksha
Copy link
Collaborator

digitalmoksha commented Feb 25, 2025

@kivikakk this is awesome! It's working well for me. Really appreciate you knocking this out 😍

Here's the code I'm using

This is just about moving the text of the header inside the <a> tag for better accessibility.

pub fn markdown_to_html_with_plugins(md: &str, options: &Options, plugins: &Plugins) -> String {
    let arena = Arena::new();
    let root = parse_document(&arena, md, options);
    let mut bw = BufWriter::new(Vec::new());

    CustomFormatter::format_document_with_plugins(root, options, &mut bw, plugins).unwrap();
    String::from_utf8(bw.into_inner().unwrap()).unwrap()
}

create_formatter!(CustomFormatter, {
    NodeValue::Heading(_) => |context, node, entering| {
        return render_heading(context, node, entering);
    },
});

fn render_heading<'a>(
    context: &mut Context,
    node: &'a AstNode<'a>,
    entering: bool,
) -> io::Result<ChildRendering> {
    let NodeValue::Heading(ref nch) = node.data.borrow().value else {
        panic!()
    };

    match context.plugins.render.heading_adapter {
        None => {
            if entering {
                context.cr()?;
                write!(context, "<h{}", nch.level)?;
                render_sourcepos(context, node)?;
                context.write_all(b">")?;

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

                    let mut id = String::from_utf8(text_content).unwrap();
                    id = context.anchorizer.anchorize(id);
                    write!(
                        context,
                        "<a href=\"#{}\" aria-hidden=\"true\" class=\"anchor\" id=\"{}{}\">",
                        id, prefix, id
                    )?;
                }
            } else {
                if let Some(ref _prefix) = context.options.extension.header_ids {
                    write!(context, "</a>")?;
                }
                writeln!(context, "</h{}>", nch.level)?;
            }
            Ok(ChildRendering::HTML)
        }
        Some(_adapter) => { format_node_default(context, node, entering) }
    }
}

I think this is ready to go! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants