concisemark/render/
html.rs1use 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 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 line.trim() != ">" {
52 text.push_str(&utils::escape_to_html(line[1..].trim()));
53 } else {
54 if i + 1 != line_count {
65 is_backquote_only_line = true;
66 text.push_str("<br/>");
67 }
68 }
69 } else {
70 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 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 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}