use rowan::NodeOrToken; use std::cmp::min; use std::fmt; use std::fmt::Write as _; use super::event::{Container, Event}; use super::TraversalContext; use super::Traverser; use crate::{SyntaxElement, SyntaxKind, SyntaxNode}; /// A wrapper for escaping sensitive characters in html. /// /// ```rust /// use orgize::export::HtmlEscape as Escape; /// /// assert_eq!(format!("{}", Escape("< < <")), "< < <"); /// assert_eq!( /// format!("{}", Escape("")), /// "<script>alert('Hello XSS')</script>" /// ); /// ``` pub struct HtmlEscape>(pub S); impl> fmt::Display for HtmlEscape { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut pos = 0; let content = self.0.as_ref(); let bytes = content.as_bytes(); while let Some(off) = jetscii::bytes!(b'<', b'>', b'&', b'\'', b'"').find(&bytes[pos..]) { write!(f, "{}", &content[pos..pos + off])?; pos += off + 1; match bytes[pos - 1] { b'<' => write!(f, "<")?, b'>' => write!(f, ">")?, b'&' => write!(f, "&")?, b'\'' => write!(f, "'")?, b'"' => write!(f, """)?, _ => {} } } write!(f, "{}", &content[pos..]) } } #[derive(Default)] pub struct HtmlExport { output: String, in_descriptive_list: Vec, table_row: TableRow, } #[derive(Default, PartialEq, Eq)] enum TableRow { #[default] HeaderRule, Header, BodyRule, Body, } impl HtmlExport { pub fn push_str(&mut self, s: impl AsRef) { self.output += s.as_ref(); } pub fn finish(self) -> String { self.output } /// Render syntax node to html string /// /// ```rust /// use orgize::{Org, ast::Bold, export::HtmlExport, rowan::ast::AstNode}; /// /// let org = Org::parse("* /hello/ *world*"); /// let bold = org.first_node::().unwrap(); /// let mut html = HtmlExport::default(); /// html.render(bold.syntax()); /// assert_eq!(html.finish(), "world"); /// ``` pub fn render(&mut self, node: &SyntaxNode) { let mut ctx = TraversalContext::default(); self.element(SyntaxElement::Node(node.clone()), &mut ctx); } } impl Traverser for HtmlExport { fn event(&mut self, event: Event, ctx: &mut TraversalContext) { match event { Event::Enter(Container::Document(_)) => self.output += "
", Event::Leave(Container::Document(_)) => self.output += "
", Event::Enter(Container::Headline(headline)) => { let level = min(headline.level(), 6); let _ = write!(&mut self.output, ""); for elem in headline.title() { self.element(elem, ctx); } let _ = write!(&mut self.output, ""); } Event::Leave(Container::Headline(_)) => {} Event::Enter(Container::Paragraph(_)) => self.output += "

", Event::Leave(Container::Paragraph(_)) => self.output += "

", Event::Enter(Container::Section(_)) => self.output += "
", Event::Leave(Container::Section(_)) => self.output += "
", Event::Enter(Container::Italic(_)) => self.output += "", Event::Leave(Container::Italic(_)) => self.output += "", Event::Enter(Container::Bold(_)) => self.output += "", Event::Leave(Container::Bold(_)) => self.output += "", Event::Enter(Container::Strike(_)) => self.output += "", Event::Leave(Container::Strike(_)) => self.output += "", Event::Enter(Container::Underline(_)) => self.output += "", Event::Leave(Container::Underline(_)) => self.output += "", Event::Enter(Container::Verbatim(_)) => self.output += "", Event::Leave(Container::Verbatim(_)) => self.output += "", Event::Enter(Container::Code(_)) => self.output += "", Event::Leave(Container::Code(_)) => self.output += "", Event::Enter(Container::SourceBlock(block)) => { let _ = write!( &mut self.output, r#"
"#,
                    HtmlEscape(&block.language().unwrap_or_default())
                );
            }
            Event::Leave(Container::SourceBlock(_)) => self.output += "
", Event::Enter(Container::QuoteBlock(_)) => self.output += "
", Event::Leave(Container::QuoteBlock(_)) => self.output += "
", Event::Enter(Container::VerseBlock(_)) => self.output += "

", Event::Leave(Container::VerseBlock(_)) => self.output += "

", Event::Enter(Container::ExampleBlock(_)) => self.output += "
",
            Event::Leave(Container::ExampleBlock(_)) => self.output += "
", Event::Enter(Container::CenterBlock(_)) => self.output += "
", Event::Leave(Container::CenterBlock(_)) => self.output += "
", Event::Enter(Container::CommentBlock(_)) => self.output += "", Event::Enter(Container::Comment(_)) => self.output += "", Event::Enter(Container::Subscript(_)) => self.output += "", Event::Leave(Container::Subscript(_)) => self.output += "", Event::Enter(Container::Superscript(_)) => self.output += "", Event::Leave(Container::Superscript(_)) => self.output += "", Event::Enter(Container::List(list)) => { self.output += if list.is_ordered() { self.in_descriptive_list.push(false); "
    " } else if list.is_descriptive() { self.in_descriptive_list.push(true); "
    " } else { self.in_descriptive_list.push(false); "
      " }; } Event::Leave(Container::List(list)) => { self.output += if list.is_ordered() { "
" } else if let Some(true) = self.in_descriptive_list.last() { "" } else { "" }; self.in_descriptive_list.pop(); } Event::Enter(Container::ListItem(list_item)) => { if let Some(&true) = self.in_descriptive_list.last() { self.output += "
"; for elem in list_item.tag() { self.element(elem, ctx); } self.output += "
"; } else { self.output += "
  • "; } } Event::Leave(Container::ListItem(_)) => { if let Some(&true) = self.in_descriptive_list.last() { self.output += "
  • "; } else { self.output += ""; } } Event::Enter(Container::OrgTable(table)) => { self.output += ""; self.table_row = if table.has_header() { TableRow::HeaderRule } else { TableRow::BodyRule } } Event::Leave(Container::OrgTable(_)) => { match self.table_row { TableRow::Body => self.output += "", TableRow::Header => self.output += "", _ => {} } self.output += "
    "; } Event::Enter(Container::OrgTableRow(row)) => { if row.is_rule() { match self.table_row { TableRow::Body => { self.output += ""; self.table_row = TableRow::BodyRule; } TableRow::Header => { self.output += ""; self.table_row = TableRow::BodyRule; } _ => {} } ctx.skip(); } else { match self.table_row { TableRow::HeaderRule => { self.table_row = TableRow::Header; self.output += ""; } TableRow::BodyRule => { self.table_row = TableRow::Body; self.output += ""; } _ => {} } self.output += ""; } } Event::Leave(Container::OrgTableRow(row)) => { if row.is_rule() { match self.table_row { TableRow::Body => { self.output += ""; self.table_row = TableRow::BodyRule; } TableRow::Header => { self.output += ""; self.table_row = TableRow::BodyRule; } _ => {} } ctx.skip(); } else { self.output += ""; } } Event::Enter(Container::OrgTableCell(_)) => self.output += "", Event::Leave(Container::OrgTableCell(_)) => self.output += "", Event::Enter(Container::Link(link)) => { let path = link.path(); let path = path.trim_start_matches("file:"); if link.is_image() { let _ = write!(&mut self.output, r#""#, HtmlEscape(&path)); return ctx.skip(); } let _ = write!(&mut self.output, r#""#, HtmlEscape(&path)); if !link.has_description() { let _ = write!(&mut self.output, "{}", HtmlEscape(&path)); ctx.skip(); } } Event::Leave(Container::Link(_)) => self.output += "", Event::Text(text) => { let _ = write!(&mut self.output, "{}", HtmlEscape(text)); } Event::LineBreak(_) => self.output += "
    ", Event::Snippet(snippet) => { if snippet.backend().eq_ignore_ascii_case("html") { self.output += &snippet.value(); } } Event::Rule(_) => self.output += "
    ", Event::Timestamp(timestamp) => { self.output += r#""#; for e in timestamp.syntax.children_with_tokens() { match e { NodeOrToken::Token(t) if t.kind() == SyntaxKind::MINUS2 => { self.output += "–"; } NodeOrToken::Token(t) => { self.output += t.text(); } _ => {} } } self.output += r#""#; } Event::LatexFragment(latex) => { let _ = write!(&mut self.output, "{}", &latex.syntax); } Event::LatexEnvironment(latex) => { let _ = write!(&mut self.output, "{}", &latex.syntax); } Event::Entity(entity) => self.output += entity.html(), _ => {} } } }