html: extract Writer::render_{event, epilogue}

This commit is contained in:
Noah Hellman 2023-03-19 18:35:41 +01:00
parent 3d1b5f2115
commit 8eafdf073b

View file

@ -74,363 +74,378 @@ impl Default for Writer {
} }
impl Writer { impl Writer {
fn write<'s>( fn write<'s, I, W>(&mut self, mut events: I, mut out: W) -> std::fmt::Result
&mut self, where
events: impl Iterator<Item = Event<'s>>, I: Iterator<Item = Event<'s>>,
mut out: impl std::fmt::Write, W: std::fmt::Write,
) -> std::fmt::Result { {
for e in events { events.try_for_each(|e| self.render_event(&e, &mut out))?;
if matches!(&e, Event::Blankline | Event::Escape) { self.render_epilogue(&mut out)
continue; }
}
let close_para = self.close_para; fn render_event<'s, W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result
if close_para { where
self.close_para = false; W: std::fmt::Write,
if !matches!(&e, Event::End(Container::Footnote { .. })) { {
// no need to add href before para close if matches!(&e, Event::Blankline | Event::Escape) {
out.write_str("</p>")?; return Ok(());
}
let close_para = self.close_para;
if close_para {
self.close_para = false;
if !matches!(&e, Event::End(Container::Footnote { .. })) {
// no need to add href before para close
out.write_str("</p>")?;
}
}
match e {
Event::Start(c, attrs) => {
if c.is_block() && !self.first_line {
out.write_char('\n')?;
} }
} if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
return Ok(());
match e { }
Event::Start(c, attrs) => { match &c {
if c.is_block() && !self.first_line { Container::Blockquote => out.write_str("<blockquote")?,
out.write_char('\n')?; Container::List { kind, tight } => {
} self.list_tightness.push(*tight);
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { match kind {
continue; ListKind::Unordered | ListKind::Task => out.write_str("<ul")?,
} ListKind::Ordered {
match &c { numbering, start, ..
Container::Blockquote => out.write_str("<blockquote")?, } => {
Container::List { kind, tight } => { out.write_str("<ol")?;
self.list_tightness.push(*tight); if *start > 1 {
match kind { write!(out, r#" start="{}""#, start)?;
ListKind::Unordered | ListKind::Task => out.write_str("<ul")?, }
ListKind::Ordered { if let Some(ty) = match numbering {
numbering, start, .. Decimal => None,
} => { AlphaLower => Some('a'),
out.write_str("<ol")?; AlphaUpper => Some('A'),
if *start > 1 { RomanLower => Some('i'),
write!(out, r#" start="{}""#, start)?; RomanUpper => Some('I'),
} } {
if let Some(ty) = match numbering { write!(out, r#" type="{}""#, ty)?;
Decimal => None,
AlphaLower => Some('a'),
AlphaUpper => Some('A'),
RomanLower => Some('i'),
RomanUpper => Some('I'),
} {
write!(out, r#" type="{}""#, ty)?;
}
} }
} }
} }
Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("<li")?;
}
Container::DescriptionList => out.write_str("<dl")?,
Container::DescriptionDetails => out.write_str("<dd")?,
Container::Footnote { number, .. } => {
assert!(self.footnote_number.is_none());
self.footnote_number = Some((*number).try_into().unwrap());
if !self.encountered_footnote {
self.encountered_footnote = true;
out.write_str("<section role=\"doc-endnotes\">\n<hr>\n<ol>\n")?;
}
write!(out, "<li id=\"fn{}\">", number)?;
continue;
}
Container::Table => out.write_str("<table")?,
Container::TableRow { .. } => out.write_str("<tr")?,
Container::Section { .. } => out.write_str("<section")?,
Container::Div { .. } => out.write_str("<div")?,
Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) {
continue;
}
out.write_str("<p")?;
}
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
Container::TableCell { head: false, .. } => out.write_str("<td")?,
Container::TableCell { head: true, .. } => out.write_str("<th")?,
Container::Caption => out.write_str("<caption")?,
Container::DescriptionTerm => out.write_str("<dt")?,
Container::CodeBlock { .. } => out.write_str("<pre")?,
Container::Span | Container::Math { .. } => out.write_str("<span")?,
Container::Link(dst, ty) => {
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
out.write_str("<a")?;
} else {
out.write_str(r#"<a href=""#)?;
if matches!(ty, LinkType::Email) {
out.write_str("mailto:")?;
}
write_attr(dst, &mut out)?;
out.write_char('"')?;
}
}
Container::Image(..) => {
self.img_alt_text += 1;
if self.img_alt_text == 1 {
out.write_str("<img")?;
} else {
continue;
}
}
Container::Verbatim => out.write_str("<code")?,
Container::RawBlock { format } | Container::RawInline { format } => {
self.raw = if format == &"html" {
Raw::Html
} else {
Raw::Other
};
continue;
}
Container::Subscript => out.write_str("<sub")?,
Container::Superscript => out.write_str("<sup")?,
Container::Insert => out.write_str("<ins")?,
Container::Delete => out.write_str("<del")?,
Container::Strong => out.write_str("<strong")?,
Container::Emphasis => out.write_str("<em")?,
Container::Mark => out.write_str("<mark")?,
} }
Container::ListItem | Container::TaskListItem { .. } => {
for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") { out.write_str("<li")?;
write!(out, r#" {}=""#, a)?;
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
out.write_char('"')?;
} }
Container::DescriptionList => out.write_str("<dl")?,
if let Container::Heading { Container::DescriptionDetails => out.write_str("<dd")?,
id, Container::Footnote { number, .. } => {
has_section: false, assert!(self.footnote_number.is_none());
.. self.footnote_number = Some((*number).try_into().unwrap());
if !self.encountered_footnote {
self.encountered_footnote = true;
out.write_str("<section role=\"doc-endnotes\">\n<hr>\n<ol>\n")?;
}
write!(out, "<li id=\"fn{}\">", number)?;
return Ok(());
} }
| Container::Section { id } = &c Container::Table => out.write_str("<table")?,
{ Container::TableRow { .. } => out.write_str("<tr")?,
if !attrs.iter().any(|(a, _)| a == "id") { Container::Section { .. } => out.write_str("<section")?,
out.write_str(r#" id=""#)?; Container::Div { .. } => out.write_str("<div")?,
write_attr(id, &mut out)?; Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) {
return Ok(());
}
out.write_str("<p")?;
}
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
Container::TableCell { head: false, .. } => out.write_str("<td")?,
Container::TableCell { head: true, .. } => out.write_str("<th")?,
Container::Caption => out.write_str("<caption")?,
Container::DescriptionTerm => out.write_str("<dt")?,
Container::CodeBlock { .. } => out.write_str("<pre")?,
Container::Span | Container::Math { .. } => out.write_str("<span")?,
Container::Link(dst, ty) => {
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
out.write_str("<a")?;
} else {
out.write_str(r#"<a href=""#)?;
if matches!(ty, LinkType::Email) {
out.write_str("mailto:")?;
}
write_attr(dst, &mut out)?;
out.write_char('"')?; out.write_char('"')?;
} }
} }
Container::Image(..) => {
self.img_alt_text += 1;
if self.img_alt_text == 1 {
out.write_str("<img")?;
} else {
return Ok(());
}
}
Container::Verbatim => out.write_str("<code")?,
Container::RawBlock { format } | Container::RawInline { format } => {
self.raw = if format == &"html" {
Raw::Html
} else {
Raw::Other
};
return Ok(());
}
Container::Subscript => out.write_str("<sub")?,
Container::Superscript => out.write_str("<sup")?,
Container::Insert => out.write_str("<ins")?,
Container::Delete => out.write_str("<del")?,
Container::Strong => out.write_str("<strong")?,
Container::Emphasis => out.write_str("<em")?,
Container::Mark => out.write_str("<mark")?,
}
if attrs.iter().any(|(a, _)| a == "class") for (a, v) in attrs.iter().filter(|(a, _)| *a != "class") {
|| matches!( write!(out, r#" {}=""#, a)?;
c, v.parts().try_for_each(|part| write_attr(part, &mut out))?;
Container::Div { class: Some(_) } out.write_char('"')?;
| Container::Math { .. } }
| Container::List {
kind: ListKind::Task, if let Container::Heading {
.. id,
} has_section: false,
| Container::TaskListItem { .. } ..
) }
{ | Container::Section { id } = &c
out.write_str(r#" class=""#)?; {
let mut first_written = false; if !attrs.iter().any(|(a, _)| a == "id") {
if let Some(cls) = match c { out.write_str(r#" id=""#)?;
Container::List { write_attr(id, &mut out)?;
out.write_char('"')?;
}
}
if attrs.iter().any(|(a, _)| a == "class")
|| matches!(
c,
Container::Div { class: Some(_) }
| Container::Math { .. }
| Container::List {
kind: ListKind::Task, kind: ListKind::Task,
.. ..
} => Some("task-list"),
Container::TaskListItem { checked: false } => Some("unchecked"),
Container::TaskListItem { checked: true } => Some("checked"),
Container::Math { display: false } => Some("math inline"),
Container::Math { display: true } => Some("math display"),
_ => None,
} {
first_written = true;
out.write_str(cls)?;
}
for cls in attrs
.iter()
.filter(|(a, _)| a == &"class")
.map(|(_, cls)| cls)
{
if first_written {
out.write_char(' ')?;
} }
first_written = true; | Container::TaskListItem { .. }
cls.parts() )
.try_for_each(|part| write_attr(part, &mut out))?; {
} out.write_str(r#" class=""#)?;
// div class goes after classes from attrs let mut first_written = false;
if let Container::Div { class: Some(cls) } = c { if let Some(cls) = match c {
if first_written { Container::List {
out.write_char(' ')?; kind: ListKind::Task,
} ..
out.write_str(cls)?; } => Some("task-list"),
} Container::TaskListItem { checked: false } => Some("unchecked"),
out.write_char('"')?; Container::TaskListItem { checked: true } => Some("checked"),
Container::Math { display: false } => Some("math inline"),
Container::Math { display: true } => Some("math display"),
_ => None,
} {
first_written = true;
out.write_str(cls)?;
} }
for cls in attrs
.iter()
.filter(|(a, _)| a == &"class")
.map(|(_, cls)| cls)
{
if first_written {
out.write_char(' ')?;
}
first_written = true;
cls.parts()
.try_for_each(|part| write_attr(part, &mut out))?;
}
// div class goes after classes from attrs
if let Container::Div { class: Some(cls) } = c {
if first_written {
out.write_char(' ')?;
}
out.write_str(cls)?;
}
out.write_char('"')?;
}
match c { match c {
Container::TableCell { alignment, .. } Container::TableCell { alignment, .. }
if !matches!(alignment, Alignment::Unspecified) => if !matches!(alignment, Alignment::Unspecified) =>
{ {
let a = match alignment { let a = match alignment {
Alignment::Unspecified => unreachable!(), Alignment::Unspecified => unreachable!(),
Alignment::Left => "left", Alignment::Left => "left",
Alignment::Center => "center", Alignment::Center => "center",
Alignment::Right => "right", Alignment::Right => "right",
}; };
write!(out, r#" style="text-align: {};">"#, a)?; write!(out, r#" style="text-align: {};">"#, a)?;
}
Container::CodeBlock { lang } => {
if let Some(l) = lang {
out.write_str(r#"><code class="language-"#)?;
write_attr(l, &mut out)?;
out.write_str(r#"">"#)?;
} else {
out.write_str("><code>")?;
}
}
Container::Image(..) => {
if self.img_alt_text == 1 {
out.write_str(r#" alt=""#)?;
}
}
Container::Math { display } => {
out.write_str(if display { r#">\["# } else { r#">\("# })?;
}
_ => out.write_char('>')?,
} }
} Container::CodeBlock { lang } => {
Event::End(c) => { if let Some(l) = lang {
if c.is_block_container() && !matches!(c, Container::Footnote { .. }) { out.write_str(r#"><code class="language-"#)?;
out.write_char('\n')?; write_attr(l, &mut out)?;
out.write_str(r#"">"#)?;
} else {
out.write_str("><code>")?;
}
} }
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) { Container::Image(..) => {
continue; if self.img_alt_text == 1 {
out.write_str(r#" alt=""#)?;
}
} }
match c { Container::Math { display } => {
Container::Blockquote => out.write_str("</blockquote>")?, out.write_str(if *display { r#">\["# } else { r#">\("# })?;
Container::List {
kind: ListKind::Unordered | ListKind::Task,
..
} => {
self.list_tightness.pop();
out.write_str("</ul>")?;
}
Container::List {
kind: ListKind::Ordered { .. },
..
} => out.write_str("</ol>")?,
Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("</li>")?;
}
Container::DescriptionList => out.write_str("</dl>")?,
Container::DescriptionDetails => out.write_str("</dd>")?,
Container::Footnote { number, .. } => {
if !close_para {
// create a new paragraph
out.write_str("\n<p>")?;
}
write!(
out,
r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a></p>"##,
number,
)?;
out.write_str("\n</li>")?;
self.footnote_number = None;
}
Container::Table => out.write_str("</table>")?,
Container::TableRow { .. } => out.write_str("</tr>")?,
Container::Section { .. } => out.write_str("</section>")?,
Container::Div { .. } => out.write_str("</div>")?,
Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) {
continue;
}
if self.footnote_number.is_none() {
out.write_str("</p>")?;
} else {
self.close_para = true;
}
}
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
Container::TableCell { head: true, .. } => out.write_str("</th>")?,
Container::Caption => out.write_str("</caption>")?,
Container::DescriptionTerm => out.write_str("</dt>")?,
Container::CodeBlock { .. } => out.write_str("</code></pre>")?,
Container::Span => out.write_str("</span>")?,
Container::Link(..) => out.write_str("</a>")?,
Container::Image(src, ..) => {
if self.img_alt_text == 1 {
if !src.is_empty() {
out.write_str(r#"" src=""#)?;
write_attr(&src, &mut out)?;
}
out.write_str(r#"">"#)?;
}
self.img_alt_text -= 1;
}
Container::Verbatim => out.write_str("</code>")?,
Container::Math { display } => {
out.write_str(if display {
r#"\]</span>"#
} else {
r#"\)</span>"#
})?;
}
Container::RawBlock { .. } | Container::RawInline { .. } => {
self.raw = Raw::None;
}
Container::Subscript => out.write_str("</sub>")?,
Container::Superscript => out.write_str("</sup>")?,
Container::Insert => out.write_str("</ins>")?,
Container::Delete => out.write_str("</del>")?,
Container::Strong => out.write_str("</strong>")?,
Container::Emphasis => out.write_str("</em>")?,
Container::Mark => out.write_str("</mark>")?,
} }
} _ => out.write_char('>')?,
Event::Str(s) => match self.raw {
Raw::None if self.img_alt_text > 0 => write_attr(&s, &mut out)?,
Raw::None => write_text(&s, &mut out)?,
Raw::Html => out.write_str(&s)?,
Raw::Other => {}
},
Event::FootnoteReference(_tag, number) => {
if self.img_alt_text == 0 {
write!(
out,
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
number, number, number
)?;
}
}
Event::Symbol(sym) => write!(out, ":{}:", sym)?,
Event::LeftSingleQuote => out.write_str("&lsquo;")?,
Event::RightSingleQuote => out.write_str("&rsquo;")?,
Event::LeftDoubleQuote => out.write_str("&ldquo;")?,
Event::RightDoubleQuote => out.write_str("&rdquo;")?,
Event::Ellipsis => out.write_str("&hellip;")?,
Event::EnDash => out.write_str("&ndash;")?,
Event::EmDash => out.write_str("&mdash;")?,
Event::NonBreakingSpace => out.write_str("&nbsp;")?,
Event::Hardbreak => out.write_str("<br>\n")?,
Event::Softbreak => out.write_char('\n')?,
Event::Escape | Event::Blankline => unreachable!("filtered out"),
Event::ThematicBreak(attrs) => {
out.write_str("\n<hr")?;
for (a, v) in attrs.iter() {
write!(out, r#" {}=""#, a)?;
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
out.write_char('"')?;
}
out.write_str(">")?;
} }
} }
self.first_line = false; Event::End(c) => {
if c.is_block_container() && !matches!(c, Container::Footnote { .. }) {
out.write_char('\n')?;
}
if self.img_alt_text > 0 && !matches!(c, Container::Image(..)) {
return Ok(());
}
match c {
Container::Blockquote => out.write_str("</blockquote>")?,
Container::List {
kind: ListKind::Unordered | ListKind::Task,
..
} => {
self.list_tightness.pop();
out.write_str("</ul>")?;
}
Container::List {
kind: ListKind::Ordered { .. },
..
} => out.write_str("</ol>")?,
Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("</li>")?;
}
Container::DescriptionList => out.write_str("</dl>")?,
Container::DescriptionDetails => out.write_str("</dd>")?,
Container::Footnote { number, .. } => {
if !close_para {
// create a new paragraph
out.write_str("\n<p>")?;
}
write!(
out,
r##"<a href="#fnref{}" role="doc-backlink">↩︎︎</a></p>"##,
number,
)?;
out.write_str("\n</li>")?;
self.footnote_number = None;
}
Container::Table => out.write_str("</table>")?,
Container::TableRow { .. } => out.write_str("</tr>")?,
Container::Section { .. } => out.write_str("</section>")?,
Container::Div { .. } => out.write_str("</div>")?,
Container::Paragraph => {
if matches!(self.list_tightness.last(), Some(true)) {
return Ok(());
}
if self.footnote_number.is_none() {
out.write_str("</p>")?;
} else {
self.close_para = true;
}
}
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
Container::TableCell { head: true, .. } => out.write_str("</th>")?,
Container::Caption => out.write_str("</caption>")?,
Container::DescriptionTerm => out.write_str("</dt>")?,
Container::CodeBlock { .. } => out.write_str("</code></pre>")?,
Container::Span => out.write_str("</span>")?,
Container::Link(..) => out.write_str("</a>")?,
Container::Image(src, ..) => {
if self.img_alt_text == 1 {
if !src.is_empty() {
out.write_str(r#"" src=""#)?;
write_attr(src, &mut out)?;
}
out.write_str(r#"">"#)?;
}
self.img_alt_text -= 1;
}
Container::Verbatim => out.write_str("</code>")?,
Container::Math { display } => {
out.write_str(if *display {
r#"\]</span>"#
} else {
r#"\)</span>"#
})?;
}
Container::RawBlock { .. } | Container::RawInline { .. } => {
self.raw = Raw::None;
}
Container::Subscript => out.write_str("</sub>")?,
Container::Superscript => out.write_str("</sup>")?,
Container::Insert => out.write_str("</ins>")?,
Container::Delete => out.write_str("</del>")?,
Container::Strong => out.write_str("</strong>")?,
Container::Emphasis => out.write_str("</em>")?,
Container::Mark => out.write_str("</mark>")?,
}
}
Event::Str(s) => match self.raw {
Raw::None if self.img_alt_text > 0 => write_attr(s, &mut out)?,
Raw::None => write_text(s, &mut out)?,
Raw::Html => out.write_str(s)?,
Raw::Other => {}
},
Event::FootnoteReference(_tag, number) => {
if self.img_alt_text == 0 {
write!(
out,
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
number, number, number
)?;
}
}
Event::Symbol(sym) => write!(out, ":{}:", sym)?,
Event::LeftSingleQuote => out.write_str("&lsquo;")?,
Event::RightSingleQuote => out.write_str("&rsquo;")?,
Event::LeftDoubleQuote => out.write_str("&ldquo;")?,
Event::RightDoubleQuote => out.write_str("&rdquo;")?,
Event::Ellipsis => out.write_str("&hellip;")?,
Event::EnDash => out.write_str("&ndash;")?,
Event::EmDash => out.write_str("&mdash;")?,
Event::NonBreakingSpace => out.write_str("&nbsp;")?,
Event::Hardbreak => out.write_str("<br>\n")?,
Event::Softbreak => out.write_char('\n')?,
Event::Escape | Event::Blankline => unreachable!("filtered out"),
Event::ThematicBreak(attrs) => {
out.write_str("\n<hr")?;
for (a, v) in attrs.iter() {
write!(out, r#" {}=""#, a)?;
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
out.write_char('"')?;
}
out.write_str(">")?;
}
} }
self.first_line = false;
Ok(())
}
fn render_epilogue<W>(&mut self, mut out: W) -> std::fmt::Result
where
W: std::fmt::Write,
{
if self.encountered_footnote { if self.encountered_footnote {
out.write_str("\n</ol>\n</section>")?; out.write_str("\n</ol>\n</section>")?;
} }
out.write_char('\n')?; out.write_char('\n')?;
Ok(()) Ok(())
} }
} }