markdown-it-rust / markdown-it

markdown-it js library rewritten in rust
Other
80 stars 9 forks source link

Render back to markdown #30

Open jryio opened 1 year ago

jryio commented 1 year ago

I have a use case where I wish to write a markdown parser that converts [[Wiki Style Links]] into valid common mark links E.g. [Wiki Style Links]({generated_source_url}].

Reading the docs and source code it does not seem possible to parse some Markdown source, do AST processing, and output a new Markdown source at a String/str.

Is my understanding correct? Also perhaps @chrisjsewell might know how to do this as they've written several plugins for this crate over at (https://github.com/chrisjsewell/markdown-it-plugins.rs)

chrisjsewell commented 1 year ago

Heya, so no I don't believe there is currently a "Markdown Renderer" for markdown-it.rs.

If someone is to create one, I would suggest having a look at https://github.com/executablebooks/mdformat, which is effectively the equivalent for https://github.com/executablebooks/markdown-it-py

This does bring up another issue I was going to open, which is what is the best way to implement renderers?

The HTML renderer (https://github.com/rlidwka/markdown-it.rs/blob/master/src/parser/renderer.rs) effectively holds a "priviledged" position, since NodeValue has a specific render method for it (https://github.com/rlidwka/markdown-it.rs/blob/47b48b8dd0b84f8d95761a7b2d2b530e328074c6/src/parser/node.rs#L223)

But for rendering to e.g. Markdown, JSON, ..., I'm not sure what the best practice way would be? Any thoughts @rlidwka?

chrisjsewell commented 1 year ago

For example, this is something I was playing around with for rendering to https://github.com/syntax-tree/mdast

But obviously it means you have to make sure to add every possible node type that you want to render, and there is some boilerplate

use std::any::TypeId;
use markdown_it::*;
use serde_json::{json, Value};

pub struct Config;

pub trait RenderMdastNode {
    fn render_mdast(&self, config: &Config) -> Value;
}

impl RenderMdastNode for Node {
    fn render_mdast(&self, config: &Config) -> Value {
        macro_rules! render_node {
            ($node_type:ty) => {
                self.node_value
                    .downcast_ref::<$node_type>()
                    .unwrap()
                    .to_mdast(self, config)
            };
        }
        match self.node_type.id {
            id if id == TypeId::of::<Root>() => render_node!(Root),
            id if id == TypeId::of::<Paragraph>() => render_node!(Paragraph),
            id if id == TypeId::of::<Text>() => render_node!(Text),
            // ...
            _ => unimplemented!("node type not implemented: {:?}", self.node_type)
        }
    }
}

trait RenderMdast: NodeValue {
    fn to_mdast(&self, node: &Node, config: &Config) -> Value;
    fn create_children(&self, node: &Node, config: &Config) -> Vec<Value> {
        return node
            .children
            .iter()
            .map(|child| child.render_mdast(config))
            .collect::<Vec<Value>>();
    }
}

impl RenderMdast for Root {
    fn to_mdast(&self, node: &Node, config: &Config) -> Value {
        json!({
            "type": "root",
            "children": self.create_children(node, config),
        })
    }
}

impl RenderMdast for Paragraph {
    fn to_mdast(&self, node: &Node, config: &Config) -> Value {
        json!({
            "type": "paragraph",
            "children": self.create_children(node, config),
        })
    }
}

impl RenderMdast for Text {
    fn to_mdast(&self, _: &Node, _: &Config) -> Value {
        json!({
            "type": "text",
            "value": self.content,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_render() {
        let md = &mut markdown_it::MarkdownIt::new();
        markdown_it::plugins::cmark::add(md);
        md.parse("hallo").render_mdast(&Config);
    }
}
rlidwka commented 1 year ago

This does bring up another issue I was going to open, which is what is the best way to implement renderers?

@chrisjsewell, your assessment above is correct.

As of now, you can override Renderer to render slightly differently (e.g. html vs xhtml). But there is no good way to implement rendering into something that's not html-like.

What are use-cases for this? Render back into markdown and... anything else?

chrisjsewell commented 1 year ago

Well in theory, anything that pandoc can output 😅

But I think at a "minimum"; a round-trip (i.e. Markdown) renderer, a programming language agnostic (e.g. JSON) AST renderer

jryio commented 1 year ago

This does bring up another issue I was going to open, which is what is the best way to implement renderers?

@chrisjsewell, your assessment above is correct.

As of now, you can override Renderer to render slightly differently (e.g. html vs xhtml). But there is no good way to implement rendering into something that's not html-like.

What are use-cases for this? Render back into markdown and... anything else?

The use case is to parse the source markdown syntax, transform the AST based on any number of rules/replacements/special markup syntax, then output valid common mark markdown.

Nothing other than that. There do not seem to be too many Rust crates which are capable of this.