use crate::Atom;
use crate::Container;
use crate::Event;
/// Generate HTML from parsed events and push it to a unicode-accepting buffer or stream.
pub fn push<'s, I: Iterator- >, W: std::fmt::Write>(out: W, events: I) {
Writer::new(events, out).write().unwrap();
}
/// Generate HTML from parsed events and write it to a byte sink, encoded as UTF-8.
///
/// NOTE: This performs many small writes, so IO writes should be buffered with e.g.
/// [`std::io::BufWriter`].
pub fn write<'s, I: Iterator
- >, W: std::io::Write>(
mut out: W,
events: I,
) -> std::io::Result<()> {
struct Adapter<'a, T: ?Sized + 'a> {
inner: &'a mut T,
error: std::io::Result<()>,
}
impl std::fmt::Write for Adapter<'_, T> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
match self.inner.write_all(s.as_bytes()) {
Ok(()) => Ok(()),
Err(e) => {
self.error = Err(e);
Err(std::fmt::Error)
}
}
}
}
let mut output = Adapter {
inner: &mut out,
error: Ok(()),
};
Writer::new(events, &mut output)
.write()
.map_err(|_| output.error.unwrap_err())
}
enum Raw {
None,
Html,
Other,
}
struct Writer {
events: std::iter::Peekable,
out: W,
raw: Raw,
text_only: bool,
encountered_footnote: bool,
footnote_number: Option,
footnote_backlink_written: bool,
}
impl<'s, I: Iterator
- >, W: std::fmt::Write> Writer {
fn new(events: I, out: W) -> Self {
Self {
events: events.peekable(),
out,
raw: Raw::None,
text_only: false,
encountered_footnote: false,
footnote_number: None,
footnote_backlink_written: false,
}
}
fn write(&mut self) -> std::fmt::Result {
while let Some(e) = self.events.next() {
match e {
Event::Start(c, attrs) => {
if c.is_block() {
self.out.write_char('\n')?;
}
if self.text_only && !matches!(c, Container::Image(..)) {
continue;
}
match &c {
Container::Blockquote => self.out.write_str("
todo!(),
Container::ListItem => self.out.write_str(" self.out.write_str(" self.out.write_str("- {
assert!(self.footnote_number.is_none());
self.footnote_number = Some((*number).try_into().unwrap());
if !self.encountered_footnote {
self.encountered_footnote = true;
self.out
.write_str("\n
\n\n")?;
}
write!(self.out, "- ", number)?;
self.footnote_backlink_written = false;
continue;
}
Container::Table => self.out.write_str("
self.out.write_str(" self.out.write_str(" self.out.write_str("
write!(self.out, " self.out.write_str(" self.out.write_str("- self.out.write_str("
self.out.write_str(" {
if dst.is_empty() {
self.out.write_str(" {
self.text_only = true;
self.out.write_str(" self.out.write_str(" {
self.raw = if format == &"html" {
Raw::Html
} else {
Raw::Other
};
continue;
}
Container::Subscript => self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str(" self.out.write_str("‘")?,
Container::DoubleQuoted => self.out.write_str("“")?,
}
if matches!(c, Container::SingleQuoted | Container::DoubleQuoted) {
continue; // TODO add span to allow attributes?
}
if attrs.iter().any(|(a, _)| a == "class")
|| matches!(
c,
Container::Div { class: Some(_) } | Container::Math { .. }
)
{
self.out.write_str(r#" class=""#)?;
let mut classes = attrs
.iter()
.filter(|(a, _)| a == &"class")
.map(|(_, cls)| cls);
let has_attr = if let Container::Math { display } = c {
self.out.write_str(if display {
"math display"
} else {
"math inline"
})?;
true
} else if let Some(cls) = classes.next() {
self.out.write_str(cls)?;
for cls in classes {
self.out.write_char(' ')?;
self.out.write_str(cls)?;
}
true
} else {
false
};
if let Container::Div { class: Some(cls) } = c {
if has_attr {
self.out.write_char(' ')?;
}
self.out.write_str(cls)?;
}
self.out.write_char('"')?;
}
match c {
Container::CodeBlock { lang } => {
if let Some(l) = lang {
write!(self.out, r#">"#, l)?;
} else {
self.out.write_str(">")?;
}
}
Container::Image(..) => {
self.out.write_str(r#" alt=""#)?;
}
Container::Math { display } => {
self.out
.write_str(if display { r#">\["# } else { r#">\("# })?;
}
_ => self.out.write_char('>')?,
}
}
Event::End(c) => {
if c.is_block_container() && !matches!(c, Container::Footnote { .. }) {
self.out.write_char('\n')?;
}
if self.text_only && !matches!(c, Container::Image(..)) {
continue;
}
match c {
Container::Blockquote => self.out.write_str("")?,
Container::List(..) => todo!(),
Container::ListItem => self.out.write_str("")?,
Container::DescriptionList => self.out.write_str("")?,
Container::DescriptionDetails => self.out.write_str("")?,
Container::Footnote { number, .. } => {
if !self.footnote_backlink_written {
write!(
self.out,
"\n↩︎︎ ",
number,
)?;
}
self.out.write_str("\n")?;
self.footnote_number = None;
}
Container::Table => self.out.write_str(" |
")?,
Container::TableRow => self.out.write_str("")?,
Container::Div { .. } => self.out.write_str("")?,
Container::Paragraph => {
if let Some(num) = self.footnote_number {
if matches!(
self.events.peek(),
Some(Event::End(Container::Footnote { .. }))
) {
write!(
self.out,
r##"↩︎︎"##,
num
)?;
self.footnote_backlink_written = true;
}
}
self.out.write_str("
")?;
}
Container::Heading { level } => write!(self.out, "", level)?,
Container::TableCell => self.out.write_str("")?,
Container::DescriptionTerm => self.out.write_str("")?,
Container::CodeBlock { .. } => self.out.write_str("")?,
Container::Span => self.out.write_str("")?,
Container::Link(..) => self.out.write_str("")?,
Container::Image(src, ..) => {
self.text_only = false;
if src.is_empty() {
self.out.write_str(r#"">"#)?;
} else {
write!(self.out, r#"" src="{}">"#, src)?;
}
}
Container::Verbatim => self.out.write_str("")?,
Container::Math { display } => {
self.out.write_str(if display {
r#"\]"#
} else {
r#"\)"#
})?;
}
Container::RawBlock { .. } | Container::RawInline { .. } => {
self.raw = Raw::None;
}
Container::Subscript => self.out.write_str("")?,
Container::Superscript => self.out.write_str("")?,
Container::Insert => self.out.write_str("")?,
Container::Delete => self.out.write_str("")?,
Container::Strong => self.out.write_str("")?,
Container::Emphasis => self.out.write_str("")?,
Container::Mark => self.out.write_str("")?,
Container::SingleQuoted => self.out.write_str("’")?,
Container::DoubleQuoted => self.out.write_str("”")?,
}
}
Event::Str(s) => {
let mut s: &str = s.as_ref();
match self.raw {
Raw::None => {
let mut ent = "";
while let Some(i) = s.chars().position(|c| {
if let Some(s) = match c {
'<' => Some("<"),
'>' => Some(">"),
'&' => Some("&"),
'"' => Some("""),
_ => None,
} {
ent = s;
true
} else {
false
}
}) {
self.out.write_str(&s[..i])?;
self.out.write_str(ent)?;
s = &s[i + 1..];
}
self.out.write_str(s)?;
}
Raw::Html => {
self.out.write_str(s)?;
}
Raw::Other => {}
}
}
Event::Atom(a) => match a {
Atom::FootnoteReference(_tag, number) => {
write!(
self.out,
r##"{}"##,
number, number, number
)?;
}
Atom::Ellipsis => self.out.write_str("…")?,
Atom::EnDash => self.out.write_str("–")?,
Atom::EmDash => self.out.write_str("—")?,
Atom::ThematicBreak => self.out.write_str("\n
")?,
Atom::NonBreakingSpace => self.out.write_str(" ")?,
Atom::Hardbreak => self.out.write_str("
\n")?,
Atom::Softbreak => self.out.write_char('\n')?,
Atom::Blankline | Atom::Escape => {}
},
}
}
if self.encountered_footnote {
self.out.write_str("\n\n")?;
}
self.out.write_char('\n')?;
Ok(())
}
}