use crate::error;
use crate::model::block::{
Alignment, Caption, Column, FrontMatter, HasAlignment, HasCaption, HeadingLevel, Label,
ListKind,
};
use crate::model::document::Metadata;
use crate::model::inline::{Character, HyperLink, HyperLinkTarget, Image, Math, SpanStyle, Text};
use crate::model::visitor::{
walk_document, BlockVisitor, DocumentVisitor, InlineVisitor, TableVisitor,
};
use crate::model::Document;
use crate::write::utils::string_of_strings;
use crate::write::{ConfigurableWriter, OutputFormat, Writer};
use std::cell::RefCell;
use std::fmt::{Display, Formatter};
use std::io::Write;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq)]
pub enum MarkdownFlavor {
Strict,
CommonMark,
GitHub,
Multi,
PhpExtra,
XWiki,
}
#[derive(Debug)]
pub struct MarkdownWriter<'a, W: Write> {
flavor: MarkdownFlavor,
in_metadata: RefCell<bool>,
list_prefix_stack: RefCell<Vec<ListKind>>,
line_prefix_stack: RefCell<Vec<String>>,
table_sep_row: RefCell<Vec<String>>,
w: RefCell<&'a mut W>,
debug: bool,
}
enum DebugMark {
SOB,
EOB,
SOQ,
EOQ,
SOL,
EOL,
}
#[inline]
pub fn writer<W: Write>(
doc: &Document,
flavor: MarkdownFlavor,
w: &mut W,
) -> crate::error::Result<()> {
let writer = MarkdownWriter::new_with(w, flavor);
writer.write_document(doc)
}
impl Default for MarkdownFlavor {
fn default() -> Self {
Self::CommonMark
}
}
impl Display for MarkdownFlavor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
MarkdownFlavor::Strict => "strict",
MarkdownFlavor::CommonMark => "commonmark",
MarkdownFlavor::GitHub => "gfm",
MarkdownFlavor::Multi => "multi",
MarkdownFlavor::PhpExtra => "extra",
MarkdownFlavor::XWiki => "xwiki",
}
)
}
}
impl Into<OutputFormat> for MarkdownFlavor {
fn into(self) -> OutputFormat {
OutputFormat::Markdown(self)
}
}
impl FromStr for MarkdownFlavor {
type Err = error::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"og" | "strict" => Ok(Self::Strict),
"cm" | "common" | "commonmark" => Ok(Self::CommonMark),
"github" | "gfm" => Ok(Self::GitHub),
"mmd" | "multi" => Ok(Self::Multi),
"php_extra" | "extra" => Ok(Self::PhpExtra),
"xwiki" => Ok(Self::XWiki),
_ => Err(error::ErrorKind::UnknownFormat.into()),
}
}
}
impl<'a, W: Write> Writer<'a, W> for MarkdownWriter<'a, W> {
fn new(w: &'a mut W) -> Self {
Self::new_with(w, Default::default())
}
fn write_document(&self, doc: &Document) -> crate::error::Result<()> {
walk_document(doc, self)?;
Ok(())
}
}
impl<'a, W: Write> ConfigurableWriter<'a, W, MarkdownFlavor> for MarkdownWriter<'a, W> {
fn new_with(w: &'a mut W, config: MarkdownFlavor) -> Self {
Self {
flavor: config,
in_metadata: RefCell::from(false),
list_prefix_stack: RefCell::from(Vec::default()),
line_prefix_stack: RefCell::from(Vec::default()),
table_sep_row: RefCell::from(Vec::default()),
w: RefCell::from(w),
debug: false,
}
}
}
impl<'a, W: Write> MarkdownWriter<'a, W> {
#[inline]
fn debug(&self, mark: DebugMark) -> crate::error::Result<()> {
if self.debug {
self.write(&mark.to_string())?;
}
Ok(())
}
fn write_label_before(&self, label: &Option<Label>) -> crate::error::Result<()> {
if let Some(label) = label {
match self.flavor {
MarkdownFlavor::Multi => {
self.write(&format!("[{}] ", label.to_string()))?;
}
MarkdownFlavor::XWiki => {
self.write(&format!("{{{{id name=\"{}\" /}}}}", label.to_string()))?;
self.end_line()?;
self.start_line()?;
}
_ => {}
}
}
Ok(())
}
fn write_label_after(&self, label: &Option<Label>) -> crate::error::Result<()> {
if let Some(label) = label {
match self.flavor {
MarkdownFlavor::PhpExtra => {
self.write(&format!(" {{#{}}}", label.to_string()))?;
}
_ => {}
}
}
Ok(())
}
fn write(&self, text: &str) -> crate::error::Result<()> {
if self.line_prefix_stack.borrow().is_empty() || !text.contains('\n') {
write!(&mut self.w.borrow_mut(), "{}", text)?;
} else {
for line in text.split('\n') {
self.write(line)?;
self.end_line()?;
self.start_line()?;
}
if text.ends_with('\n') {
self.end_line()?;
}
}
Ok(())
}
fn start_line(&self) -> crate::error::Result<()> {
self.debug(DebugMark::SOL)?;
if !self.line_prefix_stack.borrow().is_empty() {
let prefix = self.line_prefix_stack.borrow().join("");
self.write(&format!("{} ", prefix))?;
}
Ok(())
}
fn end_line(&self) -> crate::error::Result<()> {
self.debug(DebugMark::EOL)?;
writeln!(&mut self.w.borrow_mut())?;
Ok(())
}
fn make_style_stack(&self, styles: &[SpanStyle]) -> Vec<&str> {
let mut style_stack = Vec::new();
for style in styles {
match style {
SpanStyle::Plain => {
style_stack.clear();
}
SpanStyle::Italic => {
if self.flavor == MarkdownFlavor::XWiki {
style_stack.push("//")
} else {
style_stack.push("*")
}
}
SpanStyle::Bold => style_stack.push("**"),
SpanStyle::Mono | SpanStyle::Code => {
if self.flavor == MarkdownFlavor::XWiki {
style_stack.push("##")
} else {
style_stack.push("`")
}
}
SpanStyle::Strikethrough => {
if self.flavor == MarkdownFlavor::GitHub {
style_stack.push("~~")
} else if self.flavor == MarkdownFlavor::XWiki {
style_stack.push("--")
}
}
SpanStyle::Underline => {
if self.flavor == MarkdownFlavor::XWiki {
style_stack.push("__")
}
}
SpanStyle::Superscript => {
if self.flavor == MarkdownFlavor::XWiki {
style_stack.push("^^")
}
}
SpanStyle::Subscript => {
if self.flavor == MarkdownFlavor::XWiki {
style_stack.push(",,")
}
}
_ => {}
};
}
style_stack
}
}
impl<'a, W: Write> DocumentVisitor for MarkdownWriter<'a, W> {
fn metadata(&self, meta_datum: &Metadata) -> crate::error::Result<()> {
if !self.in_metadata.replace(true) {
match &self.flavor {
MarkdownFlavor::GitHub | MarkdownFlavor::Multi => {
let _ = self.write("---");
let _ = self.end_line();
}
MarkdownFlavor::XWiki => {
let _ = self.write("{{comment}}");
let _ = self.end_line();
}
_ => {}
}
}
match &self.flavor {
MarkdownFlavor::Strict
| MarkdownFlavor::CommonMark
| MarkdownFlavor::GitHub
| MarkdownFlavor::Multi
| MarkdownFlavor::PhpExtra => {
let _ = self.write(&format!(
"[_metadata_:{}]:- \"{}\"",
meta_datum.key(),
meta_datum.value_string()
));
}
MarkdownFlavor::XWiki => {
let _ = self.write(&meta_datum.yaml_string());
}
}
self.end_line()?;
Ok(())
}
fn block_visitor(&self) -> Option<&dyn BlockVisitor> {
if self.in_metadata.replace(false) {
match self.flavor {
MarkdownFlavor::GitHub | MarkdownFlavor::Multi => {
let _ = self.write("---");
let _ = self.end_line();
}
MarkdownFlavor::XWiki => {
let _ = self.write("{{/comment}}");
let _ = self.end_line();
}
_ => {}
}
let _ = self.end_line();
}
Some(self)
}
}
impl<'a, W: Write> BlockVisitor for MarkdownWriter<'a, W> {
fn start_block(&self) -> crate::error::Result<()> {
self.debug(DebugMark::SOB)?;
self.end_line()?;
self.start_line()
}
fn comment(&self, value: &str) -> crate::error::Result<()> {
match self.flavor {
MarkdownFlavor::Strict
| MarkdownFlavor::CommonMark
| MarkdownFlavor::GitHub
| MarkdownFlavor::Multi
| MarkdownFlavor::PhpExtra => {
for line in value.split('\n') {
self.write(&format!("[//]: # \"{}\"", line))?;
self.end_line()?;
self.start_line()?;
}
}
MarkdownFlavor::XWiki => {
self.write(&format!("{{{{comment}}}}\n{}\n{{{{/comment}}}}", value,))?;
self.end_line()?;
}
}
Ok(())
}
fn front_matter(&self, value: &FrontMatter) -> crate::error::Result<()> {
match self.flavor {
MarkdownFlavor::Multi => {
if matches!(value, FrontMatter::TableOfContents) {
self.write("{{TOC}}")?;
self.end_line()?;
}
}
MarkdownFlavor::XWiki => {
if matches!(value, FrontMatter::TableOfContents) {
self.write("{{toc/}}")?;
self.end_line()?;
}
}
_ => {}
}
Ok(())
}
fn start_heading(
&self,
level: &HeadingLevel,
label: &Option<Label>,
) -> crate::error::Result<()> {
self.write_label_before(label)?;
self.write(&format!(
"{} ",
string_of_strings(
if self.flavor == MarkdownFlavor::XWiki {
"="
} else {
"#"
},
level.clone() as usize
)
))
}
fn end_heading(&self, level: &HeadingLevel, label: &Option<Label>) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::XWiki {
let mut s = String::new();
for _ in 0..level.clone() as usize {
s.push('=');
}
self.write(&format!(" {}", s))?;
}
self.write_label_after(label)?;
Ok(())
}
fn image(
&self,
value: &Image,
_caption: &Option<Caption>,
label: &Option<Label>,
) -> crate::error::Result<()> {
if let Some(inline_visitor) = BlockVisitor::inline_visitor(self) {
self.end_line()?;
self.start_line()?;
self.write_label_before(label)?;
inline_visitor.image(value)?;
self.write_label_after(label)?;
}
Ok(())
}
fn math(
&self,
value: &Math,
_caption: &Option<Caption>,
label: &Option<Label>,
) -> crate::error::Result<()> {
if matches!(self.flavor, MarkdownFlavor::Multi | MarkdownFlavor::XWiki) {
if let Some(inline_visitor) = BlockVisitor::inline_visitor(self) {
self.end_line()?;
self.start_line()?;
self.write_label_before(label)?;
if matches!(self.flavor, MarkdownFlavor::Multi) {
self.write("$")?;
}
inline_visitor.math(value)?;
if matches!(self.flavor, MarkdownFlavor::Multi) {
self.write("$")?;
}
self.write_label_after(label)?;
}
}
Ok(())
}
fn start_list(&self, kind: &ListKind, label: &Option<Label>) -> crate::error::Result<()> {
self.write_label_before(label)?;
self.list_prefix_stack.borrow_mut().push(kind.clone());
Ok(())
}
fn end_list(&self, _: &ListKind, _: &Option<Label>) -> crate::error::Result<()> {
let _ = self.list_prefix_stack.borrow_mut().pop();
Ok(())
}
fn start_list_item(&self, _: &Option<Label>) -> crate::error::Result<()> {
let list_stack = self.list_prefix_stack.borrow();
if !list_stack.is_empty() {
let length = if self.flavor == MarkdownFlavor::XWiki {
list_stack.len()
} else {
list_stack.len() - 1
};
for kind in list_stack.iter().take(length) {
if self.flavor == MarkdownFlavor::XWiki {
self.write(match kind {
ListKind::Ordered => "1",
ListKind::Unordered => "*",
})?;
} else {
self.write(match kind {
ListKind::Ordered => " ",
ListKind::Unordered => " ",
})?;
}
}
if let Some(kind) = list_stack.last() {
if self.flavor == MarkdownFlavor::XWiki {
if *kind == ListKind::Ordered {
self.write(".")?;
}
self.write(" ")?;
} else if *kind == ListKind::Ordered {
self.write("1. ")?;
} else {
self.write("* ")?;
}
}
}
Ok(())
}
fn end_list_item(&self, _: &Option<Label>) -> crate::error::Result<()> {
self.end_line()
}
fn start_definition(&self, term: &str, label: &Option<Label>) -> crate::error::Result<()> {
self.write_label_before(label)?;
match self.flavor {
MarkdownFlavor::Multi | MarkdownFlavor::PhpExtra => {
self.write(term)?;
self.end_line()?;
self.start_line()?;
}
MarkdownFlavor::XWiki => {
self.write(&format!("; {}", term))?;
self.write_label_after(label)?;
self.end_line()?;
self.start_line()?;
}
_ => {
self.write_label_before(label)?;
self.write(&format!("**{}**:- ", term))?;
self.write_label_after(label)?;
}
}
Ok(())
}
fn start_definition_list_text(&self) -> crate::error::Result<()> {
match self.flavor {
MarkdownFlavor::Multi | MarkdownFlavor::PhpExtra | MarkdownFlavor::XWiki => {
write!(self.w.borrow_mut(), ": ")?;
}
_ => {}
}
Ok(())
}
fn formatted(&self, value: &str, label: &Option<Label>) -> crate::error::Result<()> {
self.write_label_before(label)?;
match self.flavor {
MarkdownFlavor::Strict
| MarkdownFlavor::CommonMark
| MarkdownFlavor::GitHub
| MarkdownFlavor::Multi
| MarkdownFlavor::PhpExtra => {
self.line_prefix_stack.borrow_mut().push(" ".to_string());
self.write(&format!(" {}\n", value))?;
let _ = self.line_prefix_stack.borrow_mut().pop();
}
MarkdownFlavor::XWiki => {
self.write(&format!("{{{{{{\n{}\n}}}}}}", value))?;
}
}
self.write_label_after(label)
}
fn code_block(
&self,
code: &str,
language: &Option<String>,
_caption: &Option<Caption>,
label: &Option<Label>,
) -> crate::error::Result<()> {
self.write_label_before(label)?;
match self.flavor {
MarkdownFlavor::Strict => {
self.line_prefix_stack.borrow_mut().push(" ".to_string());
self.write(&format!(" {}", code))?;
let _ = self.line_prefix_stack.borrow_mut().pop();
}
MarkdownFlavor::CommonMark | MarkdownFlavor::GitHub | MarkdownFlavor::Multi => {
if let Some(language) = language {
self.write(&format!("```{}\n{}\n```", language, code))?;
} else {
self.write(&format!("```\n{}\n```", code))?;
}
}
MarkdownFlavor::PhpExtra => {
if let Some(language) = language {
self.write(&format!("``` .{}\n{}\n```", language, code))?;
} else {
self.write(&format!("```\n{}\n```", code))?;
}
}
MarkdownFlavor::XWiki => {
if let Some(language) = language {
self.write(&format!(
"{{{{code language=\"{}\"}}}}\n{}\n{{{{/code}}}}",
language, code
))?;
} else {
self.write(&format!("{{{{code}}}}\n{}\n{{{{/code}}}}", code))?;
}
}
}
self.write_label_after(label)
}
fn start_quote(&self, label: &Option<Label>) -> crate::error::Result<()> {
self.debug(DebugMark::SOQ)?;
self.write_label_before(label)?;
let mut line_prefix_stack = self.line_prefix_stack.borrow_mut();
if self.flavor == MarkdownFlavor::XWiki {
line_prefix_stack.push(">".to_string());
} else {
if line_prefix_stack.is_empty() {
line_prefix_stack.push(">".to_string());
} else {
line_prefix_stack.push(" >".to_string());
}
}
Ok(())
}
fn end_quote(&self, label: &Option<Label>) -> crate::error::Result<()> {
self.debug(DebugMark::EOQ)?;
let _ = self.line_prefix_stack.borrow_mut().pop();
self.write_label_after(label)
}
fn thematic_break(&self) -> crate::error::Result<()> {
self.write("-----")
}
fn end_block(&self) -> crate::error::Result<()> {
self.debug(DebugMark::EOB)?;
self.end_line()?;
self.start_line()
}
fn table_visitor(&self) -> Option<&dyn TableVisitor> {
match self.flavor {
MarkdownFlavor::CommonMark
| MarkdownFlavor::GitHub
| MarkdownFlavor::Multi
| MarkdownFlavor::PhpExtra
| MarkdownFlavor::XWiki => Some(self),
_ => None,
}
}
fn inline_visitor(&self) -> Option<&dyn InlineVisitor> {
Some(self)
}
}
impl<'a, W: Write> TableVisitor for MarkdownWriter<'a, W> {
fn start_table(
&self,
_caption: &Option<Caption>,
label: &Option<Label>,
) -> crate::error::Result<()> {
self.write_label_before(label)?;
if self.flavor == MarkdownFlavor::CommonMark {
self.write("<table>")?;
self.end_line()?;
self.start_line()?;
}
Ok(())
}
fn start_table_header_row(&self) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::CommonMark {
self.write("<tr>")?;
}
Ok(())
}
fn table_header_cell(
&self,
column_cell: &Column,
_column_idx: usize,
) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::CommonMark {
self.write(&format!("<th>{}</th>", column_cell.text()))
} else {
self.write(&format!(
"|{}{}",
if self.flavor == MarkdownFlavor::XWiki {
"="
} else {
self.table_sep_row.borrow_mut().push(
match column_cell.alignment() {
Alignment::Justified => "-----",
Alignment::Left => ":----",
Alignment::Right => "----:",
Alignment::Centered => "--:--",
}
.to_string(),
);
""
},
column_cell.text()
))
}
}
fn end_table_header_row(&self) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::CommonMark {
self.write("</tr>")?;
} else if self.flavor != MarkdownFlavor::XWiki {
self.write("|")?;
self.end_line()?;
self.start_line()?;
self.write(&format!("|{}|", self.table_sep_row.borrow().join("|")))?;
self.table_sep_row.borrow_mut().clear();
}
self.end_line()?;
self.start_line()?;
Ok(())
}
fn start_table_row(&self, _: usize) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::CommonMark {
self.write("<tr>")?;
}
Ok(())
}
fn start_table_cell(&self, _: usize, label: &Option<Label>) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::CommonMark {
self.write("<td>")
} else {
self.write_label_before(label)?;
self.write("|")
}
}
fn end_table_cell(&self, _: usize, label: &Option<Label>) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::CommonMark {
self.write("</td>")
} else {
self.write_label_after(label)
}
}
fn end_table_row(&self, _: usize) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::CommonMark {
self.write("</tr>")?;
} else if self.flavor != MarkdownFlavor::XWiki {
self.write("|")?;
}
self.end_line()?;
self.start_line()
}
fn end_table(
&self,
_caption: &Option<Caption>,
label: &Option<Label>,
) -> crate::error::Result<()> {
self.write_label_after(label)?;
if self.flavor == MarkdownFlavor::CommonMark {
self.write("</table>")?;
}
Ok(())
}
fn inline_visitor(&self) -> Option<&dyn InlineVisitor> {
Some(self)
}
}
impl<'a, W: Write> InlineVisitor for MarkdownWriter<'a, W> {
fn link(&self, value: &HyperLink) -> crate::error::Result<()> {
if matches!(self.flavor, MarkdownFlavor::XWiki) {
self.write("[[")?;
if let Some(alt_text) = value.caption() {
self.write(alt_text.inner())?;
self.write(">>")?;
}
match value.target() {
HyperLinkTarget::External(value) => self.write(&format!("{}]]", value))?,
HyperLinkTarget::Internal(value) => {
self.write(&format!("||anchor={}]]", value.inner()))?
}
}
} else {
let target = match value.target() {
HyperLinkTarget::External(v) => {
if v.contains(' ') {
format!("<{}>", v)
} else {
v.to_string()
}
}
HyperLinkTarget::Internal(v) => format!("#{}", v),
};
self.write(&format!(
"[{}]({})",
match value.caption() {
Some(caption) => caption.inner(),
None => "",
},
target
))?;
}
Ok(())
}
fn image(&self, value: &Image) -> crate::error::Result<()> {
self.write(&if self.flavor == MarkdownFlavor::XWiki {
format!("image:{}", value.inner())
} else {
format!("", value.inner())
})
}
fn text(&self, value: &Text) -> crate::error::Result<()> {
self.write(value.inner())
}
fn math(&self, value: &Math) -> crate::error::Result<()> {
match self.flavor {
MarkdownFlavor::Multi => {
self.write(&format!("${}$", value.inner()))?;
}
MarkdownFlavor::XWiki => {
self.write(&format!("{{{{formula}}}}{}{{{{/formula}}}}", value.inner()))?;
}
_ => {}
}
Ok(())
}
fn character(&self, value: &Character) -> crate::error::Result<()> {
write!(
&mut self.w.borrow_mut(),
"{}",
match value {
Character::Space => " ".to_string(),
Character::NonBreakSpace => " ".to_string(),
Character::Hyphen => "-".to_string(),
Character::EmDash => "---".to_string(),
Character::EnDash => "--".to_string(),
Character::Emoji(name) => format!(":{}:", name.inner()),
Character::Other(c) => c.to_string(),
}
)?;
Ok(())
}
fn line_break(&self) -> crate::error::Result<()> {
if self.flavor == MarkdownFlavor::XWiki {
self.write("\\\\")?;
} else {
self.write(" \n")?;
}
Ok(())
}
fn start_span(&self, styles: &[SpanStyle]) -> crate::error::Result<()> {
let style_stack = self.make_style_stack(styles);
if !style_stack.is_empty() {
self.write(&style_stack.join(""))?;
}
Ok(())
}
fn end_span(&self, styles: &[SpanStyle]) -> crate::error::Result<()> {
let style_stack = self.make_style_stack(styles);
if !style_stack.is_empty() {
self.write(
&style_stack
.into_iter()
.rev()
.collect::<Vec<&str>>()
.join(""),
)?;
}
Ok(())
}
}
impl Display for DebugMark {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
DebugMark::SOB => "⤷",
DebugMark::EOB => "⤶",
DebugMark::SOQ => "«",
DebugMark::EOQ => "»",
DebugMark::SOL => "⇒",
DebugMark::EOL => "⇐",
}
)
}
}