marktwin 0.4.2

Marktwin format support for Eternaltwin.
Documentation
use crate::ast::traits;
use crate::ast::traits::{BlockCast, InlineCast};
use html_escape::{encode_quoted_attribute_to_writer, encode_text_to_writer};
use std::io;
use std::io::Write;

pub fn emit_html<W: Write, N: traits::Root>(writer: &mut W, mkt: &N) -> io::Result<()> {
  block_container(writer, mkt)
}

fn block_container<W: Write, N: traits::BlockContainer>(writer: &mut W, node: &N) -> io::Result<()> {
  for n in node.children() {
    block(writer, &*n)?;
  }
  Ok(())
}

fn block<W: Write, N: traits::Block>(writer: &mut W, node: &N) -> io::Result<()> {
  match node.cast() {
    BlockCast::Admin(n) => admin(writer, &*n),
    BlockCast::Mod(n) => r#mod(writer, &*n),
    BlockCast::Emphasis(n) => emphasis(writer, &*n),
    BlockCast::Icon(n) => icon(writer, &*n),
    BlockCast::Link(n) => link(writer, &*n),
    BlockCast::Newline(n) => newline(writer, &*n),
    BlockCast::Strikethrough(n) => strikethrough(writer, &*n),
    BlockCast::Strong(n) => strong(writer, &*n),
    BlockCast::Text(n) => text(writer, &*n),
  }
}

fn admin<W: Write, N: traits::Admin>(writer: &mut W, node: &N) -> io::Result<()> {
  write!(writer, "<div class=\"mkt-admin\">")?;
  block_container(writer, node)?;
  write!(writer, "</div>")?;
  Ok(())
}

fn r#mod<W: Write, N: traits::Mod>(writer: &mut W, node: &N) -> io::Result<()> {
  write!(writer, "<div class=\"mod\">")?;
  block_container(writer, node)?;
  write!(writer, "</div>")?;
  Ok(())
}

fn inline_container<W: Write, N: traits::InlineContainer>(writer: &mut W, node: &N) -> io::Result<()> {
  for n in node.children() {
    inline(writer, &*n)?;
  }
  Ok(())
}

fn inline<W: Write, N: traits::Inline>(writer: &mut W, node: &N) -> io::Result<()> {
  match node.cast() {
    InlineCast::Emphasis(n) => emphasis(writer, &*n),
    InlineCast::Icon(n) => icon(writer, &*n),
    InlineCast::Link(n) => link(writer, &*n),
    InlineCast::Newline(n) => newline(writer, &*n),
    InlineCast::Strikethrough(n) => strikethrough(writer, &*n),
    InlineCast::Strong(n) => strong(writer, &*n),
    InlineCast::Text(n) => text(writer, &*n),
  }
}

fn emphasis<W: Write, N: traits::Emphasis>(writer: &mut W, node: &N) -> io::Result<()> {
  write!(writer, "<em>")?;
  inline_container(writer, node)?;
  write!(writer, "</em>")?;
  Ok(())
}

fn icon<W: Write, N: traits::Icon>(writer: &mut W, node: &N) -> io::Result<()> {
  write!(writer, "<span class=\"mkt-icon mkt-icon-")?;
  escape_html_attribute(writer, &*node.key())?;
  write!(writer, "\"></span>")?;
  Ok(())
}

fn link<W: Write, N: traits::Link>(writer: &mut W, node: &N) -> io::Result<()> {
  write!(writer, "<a href=\"")?;
  escape_html_attribute(writer, &*node.uri())?;
  write!(writer, "\">")?;
  inline_container(writer, node)?;
  write!(writer, "</a>")?;
  Ok(())
}

fn newline<W: Write, N: traits::Newline>(writer: &mut W, _node: &N) -> io::Result<()> {
  #[allow(clippy::write_with_newline)]
  write!(writer, "<br />\n")?;
  Ok(())
}

fn strikethrough<W: Write, N: traits::Strikethrough>(writer: &mut W, node: &N) -> io::Result<()> {
  write!(writer, "<span class=\"strikethrough\">")?;
  inline_container(writer, node)?;
  write!(writer, "</span>")?;
  Ok(())
}

fn strong<W: Write, N: traits::Strong>(writer: &mut W, node: &N) -> io::Result<()> {
  write!(writer, "<strong>")?;
  inline_container(writer, node)?;
  write!(writer, "</strong>")?;
  Ok(())
}

fn text<W: Write, N: traits::Text>(writer: &mut W, node: &N) -> io::Result<()> {
  escape_html_text(writer, &*node.text())
}

fn escape_html_text<W: Write>(writer: &mut W, s: &str) -> io::Result<()> {
  // TODO: Drop `html-escape` dependency
  encode_text_to_writer(s, writer)
}

fn escape_html_attribute<W: Write>(writer: &mut W, s: &str) -> io::Result<()> {
  // TODO: Drop `html-escape` dependency
  encode_quoted_attribute_to_writer(s, writer)
}

#[cfg(test)]
mod parser_tests {
  use std::fs;
  use std::path::{Path, PathBuf};

  use crate::emitter::emit_html;
  use ::test_generator::test_resources;

  #[test_resources("./test-resources/[!.]*/")]
  fn test_parse_mkt(path: &str) {
    let path: PathBuf = Path::join(Path::new(".."), path);
    let _name = path
      .components()
      .last()
      .unwrap()
      .as_os_str()
      .to_str()
      .expect("Failed to retrieve sample name");

    let ast_json = fs::read_to_string(path.join("main.ast.json")).expect("Failed to read AST");
    let ast: crate::ast::owned::Root = serde_json::from_str(&ast_json).expect("Invalid AST");

    let mut actual_html_bytes: Vec<u8> = Vec::new();
    emit_html(&mut actual_html_bytes, &ast).expect("FailedToEmit");

    fs::write(path.join("local-main.rs.html"), &actual_html_bytes).unwrap();

    let actual_html = std::str::from_utf8(&actual_html_bytes).unwrap();

    let expected_html: String = fs::read_to_string(path.join("main.html")).expect("Failed to read HTML");

    assert_eq!(actual_html, expected_html);
  }
}