simple_markdown_parser/extras/
emit.rs

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
50/// Un-highlighted code and panics on `RegExp`
51pub 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            // TODO
179            // inner_to_html(out, emitter, text)?;
180            writeln!(out, "</blockquote>")?;
181        }
182        MarkdownElement::Paragraph(text) => {
183            if text.0.starts_with("![") || text.0.starts_with("[![") {
184                // Don't wrap media in `<p>`
185                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        // TODO
201        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        // TODO how much to do here
230        MarkdownElement::HTMLElement(_) => {}
231        // TODO at start?
232        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::Media {
246        //     alt: _,
247        //     link: _,
248        //     source: _,
249        // } => {}
250        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            // TODO videos?
291            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}