1use crate::{MarkdownElement, MarkdownTextElement, RawText};
2use std::io::Write;
3
4#[cfg(target_family = "wasm")]
5use wasm_bindgen::prelude::*;
6
7#[cfg(target_family = "wasm")]
8#[wasm_bindgen]
9pub fn markdown_to_html_string(source: &str, emitter: Option<FeatureEmitterWASM>) -> String {
10 std::panic::set_hook(Box::new(console_error_panic_hook::hook));
11 let mut bytes: Vec<u8> = Vec::new();
12 let _ = match emitter {
13 Some(mut emitter) => markdown_to_html(source, &mut bytes, &mut emitter),
14 None => markdown_to_html(source, &mut bytes, &mut BlankFeatureEmitter),
15 };
16 match String::from_utf8(bytes) {
17 Ok(result) => result,
18 Err(_) => String::from("Non Utf8 output or markdown parser error"),
19 }
20}
21
22pub fn markdown_to_html(
23 source: &str,
24 out: &mut impl Write,
25 emitter: &mut impl FeatureEmitter,
26) -> Result<(), ()> {
27 let mut last_was_list_item: bool = false;
28 crate::parse(source, |item| {
29 let is_list_item = matches!(&item, MarkdownElement::ListItem { .. });
30 if !last_was_list_item && is_list_item {
31 writeln!(out, "<ul>").unwrap();
32 } else if last_was_list_item && !is_list_item {
33 writeln!(out, "</ul>").unwrap();
34 }
35 element_to_html(out, emitter, item).unwrap();
36 last_was_list_item = is_list_item;
37 })
38}
39
40pub trait FeatureEmitter {
41 fn code_block(&self, language: &str, code: &str) -> String;
42
43 fn latex(&self, code: &str) -> String;
44
45 fn command(&self, name: &str, args: Vec<(&str, &str)>, inner: &str) -> String;
46
47 fn interpolation(&self, expression: &str) -> String;
48}
49
50pub struct BlankFeatureEmitter;
52
53impl FeatureEmitter for BlankFeatureEmitter {
54 fn code_block(&self, _language: &str, code: &str) -> String {
55 code.to_owned()
56 }
57
58 fn latex(&self, _code: &str) -> String {
59 panic!("`BlankFeatureEmitter` does implement LaTeX HTML generation");
60 }
61
62 fn command(&self, _name: &str, _args: Vec<(&str, &str)>, _inner: &str) -> String {
63 panic!("`BlankFeatureEmitter` does implement command generation")
64 }
65
66 fn interpolation(&self, _expression: &str) -> String {
67 panic!("`BlankFeatureEmitter` does implement interpolation")
68 }
69}
70
71#[cfg(target_family = "wasm")]
72#[wasm_bindgen(skip_typescript)]
73pub struct FeatureEmitterWASM {
74 code_block_callback: js_sys::Function,
75 latex_callback: js_sys::Function,
76 command_callback: js_sys::Function,
77 interpolation_callback: js_sys::Function,
78}
79
80#[cfg(target_family = "wasm")]
81#[wasm_bindgen(typescript_custom_section)]
82const TS_APPEND_CONTENT: &'static str = r#"
83export class FeatureEmitterWASM {
84 constructor(
85 code_block_callback: (language: string, code: string) => string,
86 latex_callback: (code: string) => string,
87 command_callback: (name: string, args: Array<[string, string]>, inner: string) => string,
88 interpolation_callback: (expression: string) => string,
89 );
90}
91"#;
92
93#[cfg(target_family = "wasm")]
94#[wasm_bindgen]
95impl FeatureEmitterWASM {
96 #[wasm_bindgen(constructor)]
97 pub fn new(
98 code_block_callback: js_sys::Function,
99 latex_callback: js_sys::Function,
100 command_callback: js_sys::Function,
101 interpolation_callback: js_sys::Function,
102 ) -> Self {
103 Self {
104 code_block_callback,
105 latex_callback,
106 command_callback,
107 interpolation_callback,
108 }
109 }
110}
111
112#[cfg(target_family = "wasm")]
113fn result_to_string(result: Result<JsValue, JsValue>) -> String {
114 result
115 .ok()
116 .as_ref()
117 .and_then(JsValue::as_string)
118 .unwrap_or_else(|| "Error".to_owned())
119}
120
121#[cfg(target_family = "wasm")]
122impl FeatureEmitter for FeatureEmitterWASM {
123 fn code_block(&self, language: &str, code: &str) -> String {
124 let result = self.code_block_callback.call2(
125 &JsValue::NULL,
126 &JsValue::from_str(language),
127 &JsValue::from_str(code),
128 );
129 result_to_string(result)
130 }
131
132 fn latex(&self, code: &str) -> String {
133 let result = self
134 .latex_callback
135 .call1(&JsValue::NULL, &JsValue::from_str(code));
136 result_to_string(result)
137 }
138
139 fn command(&self, name: &str, args: Vec<(&str, &str)>, inner: &str) -> String {
140 use js_sys::Array;
141
142 let args_array = Array::new();
143 args.into_iter().for_each(|(l, r)| {
144 args_array.push(&Array::of2(&JsValue::from_str(l), &JsValue::from_str(r)).into());
145 });
146 let result = self.command_callback.call3(
147 &JsValue::NULL,
148 &JsValue::from_str(name),
149 &args_array.into(),
150 &JsValue::from_str(inner),
151 );
152 result_to_string(result)
153 }
154
155 fn interpolation(&self, expression: &str) -> String {
156 let result = self
157 .interpolation_callback
158 .call1(&JsValue::NULL, &JsValue::from_str(expression));
159 result_to_string(result)
160 }
161}
162
163#[allow(clippy::match_same_arms)]
164pub fn element_to_html(
165 out: &mut impl Write,
166 emitter: &mut impl FeatureEmitter,
167 item: MarkdownElement,
168) -> Result<(), Box<dyn std::error::Error>> {
169 match item {
170 MarkdownElement::Heading { level, text } => {
171 assert!(level < 7, "heading level too much for HTML");
172 writeln!(out, "<h{level}>")?;
173 inner_to_html(out, emitter, text)?;
174 writeln!(out, "</h{level}>")?;
175 }
176 MarkdownElement::Quote(_text) => {
177 writeln!(out, "<blockquote>")?;
178 writeln!(out, "</blockquote>")?;
181 }
182 MarkdownElement::Paragraph(text) => {
183 if text.0.starts_with("![") || text.0.starts_with("[![") {
184 inner_to_html(out, emitter, text)?;
186 } else {
187 writeln!(out, "<p>")?;
188 inner_to_html(out, emitter, text)?;
189 writeln!(out, "</p>")?;
190 }
191 }
192 MarkdownElement::ListItem {
193 level: _level,
194 text,
195 } => {
196 writeln!(out, "<li>")?;
197 inner_to_html(out, emitter, text)?;
198 writeln!(out, "</li>")?;
199 }
200 MarkdownElement::Table(table) => {
202 writeln!(out, "<table>")?;
203 let mut rows = table.rows();
204 writeln!(out, "<thead><tr>")?;
205 for cell in rows.next().unwrap().cells() {
206 write!(out, "<th>")?;
207 inner_to_html(out, emitter, cell)?;
208 writeln!(out, "</th>")?;
209 }
210 writeln!(out, "</tr></thead>")?;
211 writeln!(out, "<tbody>")?;
212 for row in rows {
213 write!(out, "<tr>")?;
214 for cell in row.cells() {
215 write!(out, "<td>")?;
216 inner_to_html(out, emitter, cell)?;
217 write!(out, "</td>")?;
218 }
219 writeln!(out, "</tr>")?;
220 }
221 writeln!(out, "</tbody>")?;
222 writeln!(out, "</table>")?;
223 }
224 MarkdownElement::CodeBlock { language, code } => {
225 let inner = emitter.code_block(language, code);
226 writeln!(out, "<pre>{inner}</pre>")?;
227 }
228 MarkdownElement::LaTeXBlock { script: _ } => {}
229 MarkdownElement::HTMLElement(_) => {}
231 MarkdownElement::Frontmatter(inner) => {
233 writeln!(out, "<pre>{inner}</pre>")?;
234 }
235 MarkdownElement::HorizontalRule => {
236 writeln!(out, "<hr>")?;
237 }
238 MarkdownElement::CommandBlock(command) => {
239 writeln!(
240 out,
241 "{result}",
242 result = emitter.command(command.name, command.arguments(), command.inner.0)
243 )?;
244 }
245 MarkdownElement::Footnote => {}
251 MarkdownElement::CommentBlock(_) | MarkdownElement::Empty => {}
252 }
253
254 Ok(())
255}
256
257pub fn inner_to_html(
258 out: &mut impl Write,
259 emitter: &mut impl FeatureEmitter,
260 text: RawText,
261) -> Result<(), Box<dyn std::error::Error>> {
262 for part in text.parts() {
263 text_element_to_html(out, emitter, part)?;
264 }
265 Ok(())
266}
267
268#[allow(clippy::match_same_arms)]
269pub fn text_element_to_html(
270 out: &mut impl Write,
271 emitter: &mut impl FeatureEmitter,
272 item: MarkdownTextElement,
273) -> Result<(), Box<dyn std::error::Error>> {
274 match item {
275 MarkdownTextElement::Plain(content) => write!(out, "{content}")?,
276 MarkdownTextElement::Bold(content) => write!(out, "<strong>{content}</strong>")?,
277 MarkdownTextElement::Italic(content) => write!(out, "<em>{content}</em>")?,
278 MarkdownTextElement::BoldAndItalic(content) => {
279 write!(out, "<strong><em>{content}</em></strong>")?;
280 }
281 MarkdownTextElement::Code(content) => write!(out, "<code>{content}</code>")?,
282 MarkdownTextElement::StrikeThrough(content) => write!(out, "{content}")?,
283 MarkdownTextElement::Emoji(content) => write!(out, "{content}")?,
284 MarkdownTextElement::Latex(content) => write!(out, "{content}")?,
285 MarkdownTextElement::Highlight(content) => write!(out, "{content}")?,
286 MarkdownTextElement::Subscript(content) => write!(out, "{content}")?,
287 MarkdownTextElement::Superscript(content) => write!(out, "{content}")?,
288 MarkdownTextElement::Tag(content) => write!(out, "{content}")?,
289 MarkdownTextElement::Media { alt, source } => {
290 write!(out, "<img alt=\"{alt}\" src=\"{source}\">")?;
292 }
293 MarkdownTextElement::Expression(item) => {
294 write!(out, "{result}", result = emitter.interpolation(item))?;
295 }
296 MarkdownTextElement::Link { on, to } => {
297 write!(out, "<a href=\"{to}\">")?;
298 inner_to_html(out, emitter, on)?;
299 write!(out, "</a>")?;
300 }
301 };
302
303 Ok(())
304}