concisemark/render/
html.rs

1use super::{mark, RenderType};
2use crate::{
3    node::{Emphasis, Node, NodeTagName},
4    utils,
5};
6
7pub fn generate<S: AsRef<str>, F>(
8    node: &Node,
9    content: S,
10    hook: Option<&F>,
11) -> String
12where
13    F: Fn(&Node) -> Option<String>,
14{
15    if let Some(hook) = hook {
16        if let Some(html) = hook(node) {
17            return html;
18        }
19    }
20
21    let content = content.as_ref();
22    let nodedata = node.data.borrow();
23    let body = &content[nodedata.range.start..nodedata.range.end];
24    let tagname = nodedata.tag.name;
25
26    // Render all void tag.
27    //
28    // Void tag contains no content, but only name and optional attrs see [4.3. Elements](https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#syntax-elements).
29    //
30    // Note that ConciseMark extends this concept to denote a node that contains optional
31    // characters body as its value.
32    //
33    match tagname {
34        NodeTagName::Text => {
35            let mut text = String::new();
36            let line_count = body.lines().count();
37            let mut previous_line_trimmed = false;
38            for (i, line) in body.lines().enumerate() {
39                let line = line.trim_start();
40                if line.is_empty() {
41                    continue;
42                }
43                let mut is_backquote_only_line = false;
44                let ch = line.chars().next().unwrap();
45                if ch == '>' {
46                    // if the first character of paragraph line is backquote character
47                    // and it contains more text, this is case like following
48                    //
49                    //     > some text
50                    //
51                    if line.trim() != ">" {
52                        text.push_str(&utils::escape_to_html(line[1..].trim()));
53                    } else {
54                        // or else the line contains only a single `>` character such as
55                        //
56                        //     >
57                        //
58                        // but if the original line is
59                        //
60                        //     > *line*
61                        //
62                        // we will only see `> `, should we put a <br/> here?
63                        // fortunately, this case will always be the last line!
64                        if i + 1 != line_count {
65                            is_backquote_only_line = true;
66                            text.push_str("<br/>");
67                        }
68                    }
69                } else {
70                    // see test `test_para_ending_whitesapce 1)`
71                    if previous_line_trimmed
72                        && (ch.is_ascii_alphanumeric()
73                            || ch.is_ascii_punctuation()
74                            || ch.is_ascii_whitespace())
75                    {
76                        text.push(' ');
77                    }
78                    text.push_str(&utils::escape_to_html(line.trim()));
79                }
80                // see test `test_para_ending_whitesapce 2)` and `test_backquote_unicode`
81                if let Some(ch) = line.trim_end().chars().last() {
82                    if (ch.is_ascii_alphanumeric()
83                        || ch.is_ascii_punctuation()
84                        || ch.is_ascii_whitespace())
85                        && (!is_backquote_only_line)
86                    {
87                        text.push(' ');
88                        previous_line_trimmed = false;
89                    } else {
90                        previous_line_trimmed = true;
91                    }
92                }
93            }
94            return text;
95        }
96        NodeTagName::Code => {
97            if node.is_inlined(content) {
98                return format!(
99                    "<code>{}</code>",
100                    utils::escape_to_html(
101                        body.trim_matches(|c| c == '`').trim()
102                    )
103                );
104            } else {
105                return format!(
106                    "<pre><code>{}</pre></code>",
107                    utils::escape_to_html(utils::remove_indent(body).trim())
108                );
109            }
110        }
111        NodeTagName::Math => {
112            let body = body.trim_matches(|x| x == '$');
113            if node.is_inlined(content) {
114                return format!("${body}$");
115            } else {
116                return format!("$${body}$$");
117            }
118        }
119        NodeTagName::Link => {
120            let url = node.get_attr_or("href", "");
121            let mut name = node.get_attr_or("name", url.as_str());
122            if name.is_empty() {
123                name = url.clone();
124            }
125            return format!(
126                r#" <a href="{}">{}</a> "#,
127                utils::escape_html_double_quote(&url),
128                utils::escape_to_html(&name)
129            );
130        }
131        NodeTagName::Image => {
132            let alt = node.get_attr_or("name", "image link is broken");
133            let src = node.get_attr_or("src", "");
134            return format!(
135                r#"<img alt="{}" src="{}"/>"#,
136                utils::escape_html_double_quote(&alt),
137                utils::escape_html_double_quote(&src),
138            );
139        }
140        NodeTagName::Emphasis(t) => {
141            let tag = match t {
142                Emphasis::Italics => "em",
143                Emphasis::Bold => "strong",
144            };
145            let body = utils::escape_to_html(body.trim_matches('*'));
146            return format!(r#"<{tag}> {body} </{tag}>"#);
147        }
148        NodeTagName::Extension => {
149            if let Some(value) = mark::generate(body, RenderType::Html) {
150                return value;
151            } else {
152                log::warn!("unsupported mark element: {}", body);
153                return format!(
154                    "<pre><code>{}</pre></code>",
155                    utils::escape_to_html(body)
156                );
157            }
158        }
159        _ => {}
160    }
161
162    // Render all non-void element
163    let markup = match tagname {
164        NodeTagName::Heading => {
165            let level = match nodedata
166                .tag
167                .attrs
168                .get("level")
169                .map(|s| s.as_str().parse::<usize>())
170            {
171                Some(Ok(level)) => level,
172                _ => {
173                    log::warn!(
174                        "heading level parse failed: {:?}, set it to level 1",
175                        nodedata.tag.attrs.get("level")
176                    );
177                    1
178                }
179            };
180            Some(format!("h{level}"))
181        }
182        NodeTagName::Section => Some("div".to_owned()),
183        NodeTagName::Para => Some("p".to_owned()),
184        NodeTagName::Code => Some("code".to_owned()),
185        NodeTagName::Link => Some("a".to_owned()),
186        NodeTagName::Image => Some("img".to_owned()),
187        NodeTagName::List => Some("ul".to_owned()),
188        NodeTagName::ListItem => Some("li".to_owned()),
189        _ => None,
190    };
191    let (start_tag, end_tag) = if let Some(mark) = markup {
192        if tagname == NodeTagName::Para && body.trim_start().starts_with('>') {
193            ("<blockquote><p>".to_owned(), "</p></blockquote>".to_owned())
194        } else {
195            (format!("<{mark}>"), format!("</{mark}>"))
196        }
197    } else {
198        ("".to_owned(), "".to_owned())
199    };
200
201    let mut html = String::new();
202    html += &start_tag;
203    for child in node.children().iter().filter(|x| {
204        let (start, end) =
205            (x.data.borrow().range.start, x.data.borrow().range.end);
206        !content[start..end].trim().is_empty()
207    }) {
208        html.push_str(generate(child, content, hook).as_str());
209    }
210    html += &end_tag;
211
212    html
213}