From 09bbabfdc238ce6e3d50e2758af0e491024f06d5 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 5 Sep 2020 14:31:07 +0200 Subject: [PATCH] Change html rendering to use a writer for memory efficiency Signed-off-by: trivernis --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/elements/mod.rs | 13 + src/format/html.rs | 608 ------------------------ src/format/{ => html}/assets/style.css | 0 src/format/html/html_writer.rs | 28 ++ src/format/html/mod.rs | 2 + src/format/html/to_html.rs | 631 +++++++++++++++++++++++++ src/main.rs | 17 +- 9 files changed, 690 insertions(+), 613 deletions(-) delete mode 100644 src/format/html.rs rename src/format/{ => html}/assets/style.css (100%) create mode 100644 src/format/html/html_writer.rs create mode 100644 src/format/html/mod.rs create mode 100644 src/format/html/to_html.rs diff --git a/Cargo.lock b/Cargo.lock index 7bd803b..bde2f5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1142,7 +1142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "snekdown" -version = "0.24.0" +version = "0.25.0" dependencies = [ "asciimath-rs 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index a0a098d..454a366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snekdown" -version = "0.24.0" +version = "0.25.0" authors = ["trivernis "] edition = "2018" license-file = "LICENSE" diff --git a/src/elements/mod.rs b/src/elements/mod.rs index e67a00a..1eba1d0 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -737,3 +737,16 @@ impl BibReference { return "citation needed".to_string(); } } + +impl MetadataValue { + pub fn to_string(&self) -> String { + match self { + MetadataValue::String(s) => s.clone(), + MetadataValue::Placeholder(_) => "".to_string(), + MetadataValue::Integer(i) => i.to_string(), + MetadataValue::Float(f) => f.to_string(), + MetadataValue::Bool(b) => b.to_string(), + MetadataValue::Template(_) => "".to_string(), + } + } +} diff --git a/src/format/html.rs b/src/format/html.rs deleted file mode 100644 index 549fa75..0000000 --- a/src/format/html.rs +++ /dev/null @@ -1,608 +0,0 @@ -use crate::elements::*; -use crate::format::PlaceholderTemplate; -use crate::references::templates::{Template, TemplateVariable}; -use asciimath_rs::format::mathml::ToMathML; -use htmlescape::{encode_attribute, encode_minimal}; -use minify::html::minify; -use rayon::prelude::*; -use std::cell::RefCell; -use syntect::highlighting::ThemeSet; -use syntect::html::highlighted_html_for_string; -use syntect::parsing::SyntaxSet; - -macro_rules! combine_with_lb { - ($a:expr, $b:expr) => { - if $a.len() > 0 { - format!("{}
{}", $a, $b.to_html()) - } else { - $b.to_html() - } - }; -} - -pub trait ToHtml { - fn to_html(&self) -> String; -} - -impl ToHtml for Element { - fn to_html(&self) -> String { - match self { - Element::Block(block) => block.to_html(), - Element::Inline(inline) => inline.to_html(), - Element::Line(line) => line.to_html(), - } - } -} - -impl ToHtml for Line { - fn to_html(&self) -> String { - match self { - Line::Text(text) => text.to_html(), - Line::Ruler(ruler) => ruler.to_html(), - Line::RefLink(anchor) => anchor.to_html(), - Line::Centered(centered) => centered.to_html(), - Line::BibEntry(_) => "".to_string(), - Line::Anchor(a) => a.to_html(), - } - } -} - -impl ToHtml for Inline { - fn to_html(&self) -> String { - match self { - Inline::Url(url) => url.to_html(), - Inline::Monospace(mono) => mono.to_html(), - Inline::Striked(striked) => striked.to_html(), - Inline::Plain(plain) => plain.to_html(), - Inline::Italic(italic) => italic.to_html(), - Inline::Underlined(under) => under.to_html(), - Inline::Bold(bold) => bold.to_html(), - Inline::Image(img) => img.to_html(), - Inline::Placeholder(placeholder) => placeholder.read().unwrap().to_html(), - Inline::Superscript(superscript) => superscript.to_html(), - Inline::Checkbox(checkbox) => checkbox.to_html(), - Inline::Emoji(emoji) => emoji.to_html(), - Inline::Colored(colored) => colored.to_html(), - Inline::BibReference(bibref) => bibref.read().unwrap().to_html(), - Inline::TemplateVar(var) => var.read().unwrap().to_html(), - Inline::Math(m) => m.to_html(), - Inline::LineBreak => "
".to_string(), - Inline::CharacterCode(code) => code.to_html(), - } - } -} - -impl ToHtml for Block { - fn to_html(&self) -> String { - match self { - Block::Paragraph(para) => para.to_html(), - Block::List(list) => list.to_html(), - Block::Table(table) => table.to_html(), - Block::CodeBlock(code) => code.to_html(), - Block::Quote(quote) => quote.to_html(), - Block::Section(section) => section.to_html(), - Block::Import(import) => import.to_html(), - Block::Placeholder(placeholder) => placeholder.read().unwrap().to_html(), - Block::MathBlock(m) => m.to_html(), - Block::Null => "".to_string(), - } - } -} - -impl ToHtml for MetadataValue { - fn to_html(&self) -> String { - match self { - MetadataValue::String(string) => encode_minimal(string), - MetadataValue::Integer(num) => format!("{}", num), - MetadataValue::Placeholder(ph) => ph.read().unwrap().to_html(), - MetadataValue::Bool(b) => format!("{}", b), - MetadataValue::Float(f) => format!("{}", f), - MetadataValue::Template(t) => t.to_html(), - } - } -} - -impl ToHtml for Document { - fn to_html(&self) -> String { - let inner = self - .elements - .par_iter() - .fold(|| String::new(), |a, b| format!("{}{}", a, b.to_html())) - .reduce( - || String::new(), - |mut a: String, b: String| { - a.push_str(&b); - a - }, - ); - let path = if let Some(path) = &self.path { - format!("path='{}'", encode_attribute(path.as_str())) - } else { - "".to_string() - }; - if self.is_root { - let language = if let Some(entry) = self.config.get_entry("lang") { - entry.get().as_string() - } else { - "en".to_string() - }; - let style = minify(std::include_str!("assets/style.css")); - format!( - "\ - \ - \ - \ - - \ - {}\ - \ - \ -
{}
\ - \ - ", - encode_minimal(language.as_str()), - path, style, self.stylesheets.iter().fold("".to_string(), |a, b| format!("{}", a, encode_minimal(b))), inner - ) - } else { - format!( - "
{}
", - path, inner - ) - } - } -} - -impl ToHtml for Math { - fn to_html(&self) -> String { - format!( - "{}", - self.expression.to_mathml() - ) - } -} - -impl ToHtml for MathBlock { - fn to_html(&self) -> String { - format!( - "{}", - self.expression.to_mathml() - ) - } -} - -impl ToHtml for Import { - fn to_html(&self) -> String { - let anchor = self.anchor.read().unwrap(); - if let Some(document) = &anchor.document { - document.to_html() - } else { - "".to_string() - } - } -} - -impl ToHtml for Section { - fn to_html(&self) -> String { - let inner = self - .elements - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())); - format!("
{}{}
", self.header.to_html(), inner) - } -} - -impl ToHtml for Header { - fn to_html(&self) -> String { - format!( - "{2}", - self.size, - encode_attribute(self.anchor.as_str()), - self.line.to_html() - ) - } -} - -impl ToHtml for Paragraph { - fn to_html(&self) -> String { - let inner = self - .elements - .iter() - .fold("".to_string(), |a, b| combine_with_lb!(a, b)); - format!("
{}
", inner) - } -} - -impl ToHtml for List { - fn to_html(&self) -> String { - let inner = self - .items - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())); - if self.ordered { - format!("
    {}
", inner) - } else { - format!("
    {}
", inner) - } - } -} - -impl ToHtml for ListItem { - fn to_html(&self) -> String { - let inner = self - .children - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())); - if let Some(first) = self.children.first() { - if first.ordered { - format!("
  • {}
      {}
  • ", self.text.to_html(), inner) - } else { - format!("
  • {}
      {}
  • ", self.text.to_html(), inner) - } - } else { - format!("
  • {}
  • ", self.text.to_html()) - } - } -} - -impl ToHtml for Table { - fn to_html(&self) -> String { - let head = self.header.cells.iter().fold("".to_string(), |a, b| { - format!("{}{}", a, b.text.to_html()) - }); - let body = self - .rows - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())); - format!( - "
    {}{}
    ", - head, body - ) - } -} - -impl ToHtml for Row { - fn to_html(&self) -> String { - let inner = self - .cells - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())); - format!("{}", inner) - } -} - -impl ToHtml for Cell { - fn to_html(&self) -> String { - format!("{}", self.text.to_html()) - } -} - -thread_local! {static PS: RefCell = RefCell::new(SyntaxSet::load_defaults_nonewlines());} -thread_local! {static TS: RefCell = RefCell::new(ThemeSet::load_defaults());} - -impl ToHtml for CodeBlock { - fn to_html(&self) -> String { - if self.language.len() > 0 { - PS.with(|ps_cell| { - let ps = ps_cell.borrow(); - if let Some(syntax) = ps.find_syntax_by_token(self.language.as_str()) { - TS.with(|ts_cell| { - let ts = ts_cell.borrow(); - format!( - "
    {}
    ", - encode_attribute(self.language.clone().as_str()), - highlighted_html_for_string( - self.code.as_str(), - &ps, - syntax, - &ts.themes["InspiredGitHub"] - ) - ) - }) - } else { - format!( - "
    {}
    ", - encode_attribute(self.language.clone().as_str()), - encode_minimal(self.code.as_str()) - ) - } - }) - } else { - format!( - "
    {}
    ", - encode_minimal(self.code.as_str()) - ) - } - } -} - -impl ToHtml for Quote { - fn to_html(&self) -> String { - let text = self - .text - .iter() - .fold("".to_string(), |a, b| combine_with_lb!(a, b)); - if let Some(meta) = self.metadata.clone() { - format!( - "
    {}
    ", - text, meta.to_html() - ) - } else { - format!("
    {}
    ", text) - } - } -} - -impl ToHtml for Ruler { - fn to_html(&self) -> String { - "
    ".to_string() - } -} - -impl ToHtml for TextLine { - fn to_html(&self) -> String { - self.subtext - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - } -} - -impl ToHtml for Image { - fn to_html(&self) -> String { - let mut style = String::new(); - - let url = if let Some(content) = self.get_content() { - let mime_type = mime_guess::from_path(&self.url.url).first_or(mime::IMAGE_PNG); - format!( - "data:{};base64,{}", - mime_type.to_string(), - base64::encode(content) - ) - } else { - encode_attribute(self.url.url.as_str()) - }; - if let Some(meta) = &self.metadata { - if let Some(width) = meta.data.get("width") { - style = format!("{}width: {};", style, width.to_html()) - } - if let Some(height) = meta.data.get("height") { - style = format!("{}height: {};", style, height.to_html()) - } - } - if let Some(description) = self.url.description.clone() { - let description = description - .iter() - .fold("".to_string(), |a, b| format!("{} {}", a, b.to_html())); - minify( - format!( - "
    \ - \ - {}\ - \ - \ -
    ", - encode_attribute(self.url.url.as_str()), - url, - encode_attribute(description.as_str()), - style, - description - ) - .as_str(), - ) - } else { - format!("", url, style) - } - } -} - -impl ToHtml for BoldText { - fn to_html(&self) -> String { - format!( - "{}", - self.value - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - ) - } -} - -impl ToHtml for UnderlinedText { - fn to_html(&self) -> String { - format!( - "{}", - self.value - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - ) - } -} - -impl ToHtml for ItalicText { - fn to_html(&self) -> String { - format!( - "{}", - self.value - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - ) - } -} - -impl ToHtml for StrikedText { - fn to_html(&self) -> String { - format!( - "{}", - self.value - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - ) - } -} - -impl ToHtml for SuperscriptText { - fn to_html(&self) -> String { - format!( - "{}", - self.value - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - ) - } -} - -impl ToHtml for MonospaceText { - fn to_html(&self) -> String { - format!( - "{}", - encode_minimal(self.value.as_str()) - ) - } -} - -impl ToHtml for Url { - fn to_html(&self) -> String { - if let Some(description) = self.description.clone() { - format!( - "{}", - self.url.clone(), - description - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - .as_str() - ) - } else { - format!( - "{}", - encode_attribute(self.url.clone().as_str()), - encode_minimal(self.url.clone().as_str()) - ) - } - } -} - -impl ToHtml for PlainText { - fn to_html(&self) -> String { - encode_minimal(self.value.clone().as_str()) - } -} - -impl ToHtml for Placeholder { - fn to_html(&self) -> String { - if let Some(value) = &self.value { - value.to_html() - } else { - format!("Unknown placeholder '{}'!", encode_minimal(&self.name)) - } - } -} - -impl ToHtml for RefLink { - fn to_html(&self) -> String { - format!( - "{}", - encode_attribute(self.reference.as_str()), - self.description.to_html() - ) - } -} - -impl ToHtml for InlineMetadata { - fn to_html(&self) -> String { - if let Some(MetadataValue::String(format)) = self.data.get("display") { - let mut template = PlaceholderTemplate::new(format.clone()); - self.data - .iter() - .for_each(|(k, v)| template.add_replacement(k, v.to_html().as_str())); - - template.render() - } else { - self.data.iter().fold("".to_string(), |s, (k, v)| { - format!("{} {}={},", s, k, v.to_html()) - }) - } - } -} - -impl ToHtml for Centered { - fn to_html(&self) -> String { - format!("
    {}
    ", self.line.to_html()) - } -} - -impl ToHtml for Checkbox { - fn to_html(&self) -> String { - if self.value { - format!("") - } else { - format!("") - } - } -} - -impl ToHtml for Emoji { - fn to_html(&self) -> String { - format!( - "{}", - encode_attribute(self.name.as_str()), - self.value - ) - } -} - -impl ToHtml for Colored { - fn to_html(&self) -> String { - format!( - "{}", - encode_attribute(self.color.as_str()), - self.value.to_html() - ) - } -} - -impl ToHtml for BibReference { - fn to_html(&self) -> String { - format!( - "{}", - self.key.clone(), - self.get_formatted() - ) - } -} - -impl ToHtml for Template { - fn to_html(&self) -> String { - self.text - .iter() - .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) - } -} - -impl ToHtml for TemplateVariable { - fn to_html(&self) -> String { - if let Some(value) = &self.value { - format!( - "{}{}{}", - encode_minimal(self.prefix.as_str()), - value.to_html(), - encode_minimal(self.suffix.as_str()) - ) - } else { - "".to_string() - } - } -} - -impl ToHtml for CharacterCode { - fn to_html(&self) -> String { - format!("&{};", encode_minimal(self.code.as_str())) - } -} - -impl ToHtml for Anchor { - fn to_html(&self) -> String { - format!( - "
    {}
    ", - encode_attribute(self.key.as_str()), - self.inner.to_html() - ) - } -} diff --git a/src/format/assets/style.css b/src/format/html/assets/style.css similarity index 100% rename from src/format/assets/style.css rename to src/format/html/assets/style.css diff --git a/src/format/html/html_writer.rs b/src/format/html/html_writer.rs new file mode 100644 index 0000000..72abb80 --- /dev/null +++ b/src/format/html/html_writer.rs @@ -0,0 +1,28 @@ +use std::io; +use std::io::Write; + +pub struct HTMLWriter { + inner: Box, +} + +impl HTMLWriter { + /// Creates a new writer + pub fn new(inner: Box) -> Self { + Self { inner } + } + + /// Writes a raw string + pub fn write(&mut self, html: String) -> io::Result<()> { + self.inner.write_all(html.as_bytes()) + } + + /// Writes an escaped string + pub fn write_escaped(&mut self, html: String) -> io::Result<()> { + self.write(htmlescape::encode_minimal(html.as_str())) + } + + /// Writes an escaped attribute + pub fn write_attribute(&mut self, attribute_value: String) -> io::Result<()> { + self.write(htmlescape::encode_attribute(attribute_value.as_str())) + } +} diff --git a/src/format/html/mod.rs b/src/format/html/mod.rs new file mode 100644 index 0000000..7c72de5 --- /dev/null +++ b/src/format/html/mod.rs @@ -0,0 +1,2 @@ +pub mod html_writer; +pub mod to_html; diff --git a/src/format/html/to_html.rs b/src/format/html/to_html.rs new file mode 100644 index 0000000..0b5381f --- /dev/null +++ b/src/format/html/to_html.rs @@ -0,0 +1,631 @@ +use crate::elements::*; +use crate::format::html::html_writer::HTMLWriter; +use crate::format::PlaceholderTemplate; +use crate::references::templates::{Template, TemplateVariable}; +use asciimath_rs::format::mathml::ToMathML; +use htmlescape::encode_attribute; +use minify::html::minify; +use std::cell::RefCell; +use std::io; +use syntect::highlighting::ThemeSet; +use syntect::html::highlighted_html_for_string; +use syntect::parsing::SyntaxSet; + +pub trait ToHtml { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()>; +} + +impl ToHtml for Element { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + match self { + Element::Block(block) => block.to_html(writer), + Element::Inline(inline) => inline.to_html(writer), + Element::Line(line) => line.to_html(writer), + } + } +} + +impl ToHtml for Line { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + match self { + Line::Text(text) => text.to_html(writer), + Line::Ruler(ruler) => ruler.to_html(writer), + Line::RefLink(anchor) => anchor.to_html(writer), + Line::Centered(centered) => centered.to_html(writer), + Line::Anchor(a) => a.to_html(writer), + _ => Ok(()), + } + } +} + +impl ToHtml for Inline { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + match self { + Inline::Url(url) => url.to_html(writer), + Inline::Monospace(mono) => mono.to_html(writer), + Inline::Striked(striked) => striked.to_html(writer), + Inline::Plain(plain) => plain.to_html(writer), + Inline::Italic(italic) => italic.to_html(writer), + Inline::Underlined(under) => under.to_html(writer), + Inline::Bold(bold) => bold.to_html(writer), + Inline::Image(img) => img.to_html(writer), + Inline::Placeholder(placeholder) => placeholder.read().unwrap().to_html(writer), + Inline::Superscript(superscript) => superscript.to_html(writer), + Inline::Checkbox(checkbox) => checkbox.to_html(writer), + Inline::Emoji(emoji) => emoji.to_html(writer), + Inline::Colored(colored) => colored.to_html(writer), + Inline::BibReference(bibref) => bibref.read().unwrap().to_html(writer), + Inline::TemplateVar(var) => var.read().unwrap().to_html(writer), + Inline::Math(m) => m.to_html(writer), + Inline::LineBreak => writer.write("
    ".to_string()), + Inline::CharacterCode(code) => code.to_html(writer), + } + } +} + +impl ToHtml for Block { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + match self { + Block::Paragraph(para) => para.to_html(writer), + Block::List(list) => list.to_html(writer), + Block::Table(table) => table.to_html(writer), + Block::CodeBlock(code) => code.to_html(writer), + Block::Quote(quote) => quote.to_html(writer), + Block::Section(section) => section.to_html(writer), + Block::Import(import) => import.to_html(writer), + Block::Placeholder(placeholder) => placeholder.read().unwrap().to_html(writer), + Block::MathBlock(m) => m.to_html(writer), + _ => Ok(()), + } + } +} + +impl ToHtml for MetadataValue { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + match self { + MetadataValue::String(string) => writer.write_escaped(string.clone()), + MetadataValue::Integer(num) => writer.write(num.to_string()), + MetadataValue::Placeholder(ph) => ph.read().unwrap().to_html(writer), + MetadataValue::Bool(b) => writer.write(b.to_string()), + MetadataValue::Float(f) => writer.write(f.to_string()), + MetadataValue::Template(t) => t.to_html(writer), + } + } +} + +impl ToHtml for Document { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + let path = if let Some(path) = &self.path { + format!("path='{}'", encode_attribute(path.as_str())) + } else { + "".to_string() + }; + if self.is_root { + let language = if let Some(entry) = self.config.get_entry("lang") { + entry.get().as_string() + } else { + "en".to_string() + }; + let style = minify(std::include_str!("assets/style.css")); + writer.write("".to_string())?; + writer.write("".to_string())?; + writer.write("".to_string())?; + + for stylesheet in &self.stylesheets { + writer.write("".to_string())?; + } + writer.write("
    ".to_string())?; + for element in &self.elements { + element.to_html(writer)?; + } + writer.write("
    ".to_string())?; + } else { + writer.write("
    ".to_string())?; + + for element in &self.elements { + element.to_html(writer)?; + } + writer.write("
    ".to_string())?; + } + + Ok(()) + } +} + +impl ToHtml for Math { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + writer.write(self.expression.to_mathml())?; + + writer.write("".to_string()) + } +} + +impl ToHtml for MathBlock { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write( + "".to_string(), + )?; + writer.write(self.expression.to_mathml())?; + + writer.write("".to_string()) + } +} + +impl ToHtml for Import { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + let anchor = self.anchor.read().unwrap(); + if let Some(document) = &anchor.document { + document.to_html(writer) + } else { + Ok(()) + } + } +} + +impl ToHtml for Section { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    ".to_string())?; + self.header.to_html(writer)?; + for element in &self.elements { + element.to_html(writer)?; + } + writer.write("
    ".to_string()) + } +} + +impl ToHtml for Header { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write(format!("".to_string())?; + self.line.to_html(writer)?; + + writer.write(format!("", self.size)) + } +} + +impl ToHtml for Paragraph { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    ".to_string())?; + for element in &self.elements { + element.to_html(writer)?; + writer.write("
    ".to_string())?; + } + + writer.write("
    ".to_string()) + } +} + +impl ToHtml for List { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + if self.ordered { + writer.write("
      ".to_string())?; + for item in &self.items { + item.to_html(writer)?; + } + writer.write("
    ".to_string()) + } else { + writer.write("
      ".to_string())?; + for item in &self.items { + item.to_html(writer)?; + } + + writer.write("
    ".to_string()) + } + } +} + +impl ToHtml for ListItem { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
  • ".to_string())?; + self.text.to_html(writer)?; + + if let Some(first) = self.children.first() { + if first.ordered { + writer.write("
      ".to_string())?; + for item in &self.children { + item.to_html(writer)?; + } + writer.write("
    ".to_string())?; + } else { + writer.write("
      ".to_string())?; + for item in &self.children { + item.to_html(writer)?; + } + writer.write("
    ".to_string())?; + } + } + + writer.write("
  • ".to_string()) + } +} + +impl ToHtml for Table { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    ".to_string())?; + + for cell in &self.header.cells { + writer.write("".to_string())?; + } + writer.write("".to_string())?; + for row in &self.rows { + row.to_html(writer)?; + } + + writer.write("
    ".to_string())?; + cell.text.to_html(writer)?; + writer.write("
    ".to_string()) + } +} + +impl ToHtml for Row { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + + for cell in &self.cells { + cell.to_html(writer)?; + } + + writer.write("".to_string()) + } +} + +impl ToHtml for Cell { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + self.text.to_html(writer)?; + + writer.write("".to_string()) + } +} + +thread_local! {static PS: RefCell = RefCell::new(SyntaxSet::load_defaults_nonewlines());} +thread_local! {static TS: RefCell = RefCell::new(ThemeSet::load_defaults());} + +impl ToHtml for CodeBlock { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    0 { + writer.write(" lang=\"".to_string())?; + writer.write_attribute(self.language.clone())?; + writer.write("\">".to_string())?; + PS.with(|ps_cell| { + let ps = ps_cell.borrow(); + if let Some(syntax) = ps.find_syntax_by_token(self.language.as_str()) { + TS.with(|ts_cell| { + let ts = ts_cell.borrow(); + + let _ = writer.write(highlighted_html_for_string( + self.code.as_str(), + &ps, + syntax, + &ts.themes["InspiredGitHub"], + )); + }) + } else { + let _ = writer.write("
    ".to_string());
    +                    let _ = writer.write(self.code.clone());
    +                    let _ = writer.write("
    ".to_string()); + } + }) + } else { + writer.write(">
    ".to_string())?;
    +            writer.write_escaped(self.code.clone())?;
    +            writer.write("
    ".to_string())?; + } + + writer.write("
    ".to_string()) + } +} + +impl ToHtml for Quote { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    ".to_string())?; + for line in &self.text { + line.to_html(writer)?; + writer.write("
    ".to_string())?; + } + if let Some(meta) = self.metadata.clone() { + writer.write("".to_string())?; + } + writer.write("
    ".to_string()) + } +} + +impl ToHtml for Ruler { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    ".to_string()) + } +} + +impl ToHtml for TextLine { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + for text in &self.subtext { + text.to_html(writer)?; + } + + Ok(()) + } +} + +impl ToHtml for Image { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + let mut style = String::new(); + + let url = if let Some(content) = self.get_content() { + let mime_type = mime_guess::from_path(&self.url.url).first_or(mime::IMAGE_PNG); + format!( + "data:{};base64,{}", + mime_type.to_string(), + base64::encode(content) + ) + } else { + encode_attribute(self.url.url.as_str()) + }; + if let Some(meta) = &self.metadata { + if let Some(width) = meta.get_string("width") { + style = format!("{}width: {};", style, width) + } + if let Some(height) = meta.get_string("height") { + style = format!("{}height: {};", style, height) + } + } + if let Some(description) = self.url.description.clone() { + writer.write("

    ".to_string())?; + } else { + writer.write("".to_string())?; + } + + Ok(()) + } +} + +impl ToHtml for BoldText { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + for element in &self.value { + element.to_html(writer)?; + } + writer.write("".to_string()) + } +} + +impl ToHtml for UnderlinedText { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + for element in &self.value { + element.to_html(writer)?; + } + writer.write("".to_string()) + } +} + +impl ToHtml for ItalicText { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + for element in &self.value { + element.to_html(writer)?; + } + writer.write("".to_string()) + } +} + +impl ToHtml for StrikedText { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + for element in &self.value { + element.to_html(writer)?; + } + writer.write("".to_string()) + } +} + +impl ToHtml for SuperscriptText { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + for element in &self.value { + element.to_html(writer)?; + } + writer.write("".to_string()) + } +} + +impl ToHtml for MonospaceText { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + writer.write_escaped(self.value.clone())?; + + writer.write("".to_string()) + } +} + +impl ToHtml for Url { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + if let Some(description) = self.description.clone() { + for desc in description { + desc.to_html(writer)?; + } + } else { + writer.write_escaped(self.url.clone())?; + } + + writer.write("".to_string()) + } +} + +impl ToHtml for PlainText { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write_escaped(self.value.clone()) + } +} + +impl ToHtml for Placeholder { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + if let Some(value) = &self.value { + value.to_html(writer) + } else { + writer.write("Unknown placeholder '".to_string())?; + writer.write_escaped(self.name.clone())?; + writer.write_escaped("'".to_string()) + } + } +} + +impl ToHtml for RefLink { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + self.description.to_html(writer)?; + + writer.write("".to_string()) + } +} + +impl ToHtml for InlineMetadata { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + if let Some(MetadataValue::String(format)) = self.data.get("display") { + let mut template = PlaceholderTemplate::new(format.clone()); + self.data + .iter() + .for_each(|(k, v)| template.add_replacement(k, &v.to_string())); + + writer.write(template.render())?; + } else { + for (k, v) in &self.data { + writer.write_escaped(format!("{}={},", k, v.to_string()))?; + } + } + Ok(()) + } +} + +impl ToHtml for Centered { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    ".to_string())?; + self.line.to_html(writer)?; + + writer.write("
    ".to_string()) + } +} + +impl ToHtml for Checkbox { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string()) + } +} + +impl ToHtml for Emoji { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + writer.write(self.value.to_string())?; + + writer.write("".to_string()) + } +} + +impl ToHtml for Colored { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + self.value.to_html(writer)?; + + writer.write("".to_string()) + } +} + +impl ToHtml for BibReference { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("".to_string())?; + writer.write(self.get_formatted())?; + + writer.write("".to_string()) + } +} + +impl ToHtml for Template { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + for element in &self.text { + element.to_html(writer)?; + } + + Ok(()) + } +} + +impl ToHtml for TemplateVariable { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + if let Some(value) = &self.value { + writer.write_escaped(self.prefix.clone())?; + value.to_html(writer)?; + writer.write_escaped(self.suffix.clone())?; + } + + Ok(()) + } +} + +impl ToHtml for CharacterCode { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("&".to_string())?; + writer.write_escaped(self.code.clone())?; + + writer.write(";".to_string()) + } +} + +impl ToHtml for Anchor { + fn to_html(&self, writer: &mut HTMLWriter) -> io::Result<()> { + writer.write("
    ".to_string())?; + self.inner.to_html(writer)?; + + writer.write("
    ".to_string()) + } +} diff --git a/src/main.rs b/src/main.rs index 9d5df4f..0d600ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ use colored::Colorize; use notify::{watcher, RecursiveMode, Watcher}; -use snekdown::format::html::ToHtml; +use snekdown::format::html::html_writer::HTMLWriter; +use snekdown::format::html::to_html::ToHtml; use snekdown::Parser; -use std::fs::write; +use std::fs::OpenOptions; +use std::io::BufWriter; use std::path::PathBuf; use std::sync::mpsc::channel; use std::time::{Duration, Instant}; @@ -85,8 +87,17 @@ fn render(opt: &Opt) -> Parser { format!("Parsing took: {:?}", start.elapsed()).italic() ); let start_render = Instant::now(); + let file = OpenOptions::new() + .write(true) + .read(true) + .open(&opt.output) + .unwrap(); + let writer = BufWriter::new(file); match opt.format.as_str() { - "html" => write(opt.output.to_str().unwrap(), document.to_html()).unwrap(), + "html" => { + let mut writer = HTMLWriter::new(Box::new(writer)); + document.to_html(&mut writer).unwrap() + } _ => println!("Unknown format {}", opt.format), } println!(