ironmark 1.7.0

Fast Markdown to HTML parser written in Rust with WebAssembly bindings
Documentation
use super::*;
use crate::ParseOptions;
use crate::html::is_dangerous_url;

static EM_CLOSE: [&str; 6] = ["</em>", "</em>", "</strong>", "</del>", "</mark>", "</u>"];
static EM_OPEN: [&str; 6] = ["<em>", "<em>", "<strong>", "<del>", "<mark>", "<u>"];

impl<'a> InlineScanner<'a> {
    pub(super) fn render_to_html(&self, out: &mut String, opts: &ParseOptions) {
        let mut tag_buf: [u8; 16] = [0; 16];
        let mut tag_len: usize = 0;
        let mut i = 0;

        while i < self.items.len() {
            match &self.items[i] {
                InlineItem::TextRange(start, end) => {
                    escape_html_into(out, &self.input[*start..*end]);
                }
                InlineItem::TextOwned(t) => out.push_str(t),
                InlineItem::TextStatic(t) => out.push_str(t),
                InlineItem::TextInline { buf, len } => {
                    // SAFETY: `buf` is constructed from UTF-8 bytes and `len` tracks initialized prefix length.
                    out.push_str(unsafe { std::str::from_utf8_unchecked(&buf[..*len as usize]) });
                }
                InlineItem::RawHtml(start, end) => {
                    if opts.disable_raw_html {
                        escape_html_into(out, &self.input[*start..*end]);
                    } else {
                        out.push_str(&self.input[*start..*end]);
                    }
                }
                InlineItem::Autolink(start, end, is_email) => {
                    let content = &self.input[*start as usize..*end as usize];
                    out.push_str("<a href=\"");
                    if *is_email {
                        out.push_str("mailto:");
                    }
                    crate::html::encode_url_escaped_into(out, content);
                    out.push_str("\">");
                    escape_html_into(out, content);
                    out.push_str("</a>");
                }
                InlineItem::Code(c) => {
                    out.push_str("<code>");
                    escape_html_into(out, c);
                    out.push_str("</code>");
                }
                InlineItem::CodeRange(s, e) => {
                    out.push_str("<code>");
                    escape_html_into(out, &self.input[*s as usize..*e as usize]);
                    out.push_str("</code>");
                }
                InlineItem::HardBreak => out.push_str("<br />\n"),
                InlineItem::SoftBreak => {
                    if opts.hard_breaks {
                        out.push_str("<br />\n");
                    } else {
                        out.push('\n');
                    }
                }
                InlineItem::DelimRun {
                    kind,
                    count,
                    open_em,
                    close_em,
                    ..
                } => {
                    for &size in close_em.as_slice() {
                        if tag_len > 0 && tag_buf[tag_len - 1] == size {
                            tag_len -= 1;
                            out.push_str(EM_CLOSE[size as usize]);
                        }
                    }
                    if *count > 0 {
                        for _ in 0..*count {
                            out.push(*kind as char);
                        }
                    }
                    for &size in open_em.as_slice().iter().rev() {
                        if tag_len < 16 {
                            tag_buf[tag_len] = size;
                            tag_len += 1;
                        }
                        out.push_str(EM_OPEN[size as usize]);
                    }
                }
                InlineItem::BracketOpen { is_image, .. } => {
                    if *is_image {
                        out.push_str("![");
                    } else {
                        out.push('[');
                    }
                }
                InlineItem::LinkStart(link_idx) => {
                    let LinkInfo {
                        dest,
                        title,
                        is_image,
                    } = &self.links[*link_idx as usize];
                    let safe = !is_dangerous_link_dest(dest, self.input);
                    if *is_image {
                        let alt_start = i + 1;
                        let mut alt_end = alt_start;
                        let mut depth = 1;
                        while alt_end < self.items.len() {
                            match &self.items[alt_end] {
                                InlineItem::LinkStart(..) => depth += 1,
                                InlineItem::LinkEnd => {
                                    depth -= 1;
                                    if depth == 0 {
                                        break;
                                    }
                                }
                                _ => {}
                            }
                            alt_end += 1;
                        }
                        let alt = self.collect_alt_text(alt_start, alt_end);
                        out.push_str("<img src=\"");
                        if safe {
                            write_link_dest(out, dest, self.input);
                        }
                        out.push_str("\" alt=\"");
                        out.push_str(&alt);
                        out.push('"');
                        if let Some(t) = title {
                            out.push_str(" title=\"");
                            escape_html_into(out, t);
                            out.push('"');
                        }
                        out.push_str(" />");
                        i = alt_end;
                    } else {
                        out.push_str("<a href=\"");
                        if safe {
                            write_link_dest(out, dest, self.input);
                        }
                        out.push('"');
                        if let Some(t) = title {
                            out.push_str(" title=\"");
                            escape_html_into(out, t);
                            out.push('"');
                        }
                        out.push('>');
                        if tag_len < 16 {
                            tag_buf[tag_len] = 0;
                            tag_len += 1;
                        }
                    }
                }
                InlineItem::LinkEnd => {
                    if tag_len > 0 && tag_buf[tag_len - 1] == 0 {
                        tag_len -= 1;
                        out.push_str("</a>");
                    }
                }
            }
            i += 1;
        }
    }

    pub(super) fn collect_alt_text(&self, start: usize, end: usize) -> String {
        let mut s = String::new();
        for idx in start..end {
            match &self.items[idx] {
                InlineItem::TextRange(a, b) => s.push_str(&self.input[*a..*b]),
                InlineItem::CodeRange(a, b) => s.push_str(&self.input[*a as usize..*b as usize]),
                InlineItem::TextOwned(t) | InlineItem::Code(t) => s.push_str(t),
                InlineItem::TextStatic(t) => s.push_str(t),
                InlineItem::TextInline { buf, len } => {
                    // SAFETY: `buf` is constructed from UTF-8 bytes and `len` tracks initialized prefix length.
                    s.push_str(unsafe { std::str::from_utf8_unchecked(&buf[..*len as usize]) });
                }
                InlineItem::DelimRun { kind, count, .. } => {
                    for _ in 0..*count {
                        s.push(*kind as char);
                    }
                }
                InlineItem::BracketOpen { is_image: true, .. } => s.push_str("!["),
                InlineItem::BracketOpen { .. } => s.push('['),
                _ => {}
            }
        }
        s
    }
}

/// Check if a LinkDest points to a dangerous URL without allocating.
#[inline]
fn is_dangerous_link_dest(dest: &LinkDest, input: &str) -> bool {
    match dest {
        LinkDest::Range(s, e) => {
            let s = *s as usize;
            let e = *e as usize;
            if s < e {
                is_dangerous_url(&input[s..e])
            } else {
                false
            }
        }
        LinkDest::Owned(d) => is_dangerous_url(d),
    }
}

#[inline]
pub(super) fn write_link_dest(out: &mut String, dest: &LinkDest, input: &str) {
    match dest {
        LinkDest::Range(s, e) => {
            let s = *s as usize;
            let e = *e as usize;
            if s < e {
                crate::html::encode_url_escaped_into(out, &input[s..e]);
            }
        }
        LinkDest::Owned(d) => {
            crate::html::encode_url_escaped_into(out, d);
        }
    }
}