summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2023-04-09 14:38:27 +0300
committerLars Wirzenius <liw@liw.fi>2023-04-10 09:56:15 +0300
commited827859f36ff5cc9b2360284b92cb56c5b88d41 (patch)
tree674e238696fb94ececaedb2a2ab7d84c086f9c60
parent87b6778163210748e1c80368cd5ca91afa4a07b9 (diff)
downloadriki-ed827859f36ff5cc9b2360284b92cb56c5b88d41.tar.gz
refactor: use html-page crate to represent HTML
Sponsored-by: author
-rw-r--r--Cargo.lock15
-rw-r--r--Cargo.toml1
-rw-r--r--build.rs4
-rw-r--r--riki.md23
-rw-r--r--src/bin/riki.rs6
-rw-r--r--src/directive/toc.rs116
-rw-r--r--src/error.rs4
-rw-r--r--src/html.rs485
-rw-r--r--src/page.rs26
-rw-r--r--src/util.rs26
10 files changed, 228 insertions, 478 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c9d2409..9b801a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -526,14 +526,24 @@ dependencies = [
[[package]]
name = "html-escape"
-version = "0.2.11"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
"utf8-width",
]
[[package]]
+name = "html-page"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f8314b0ea57e9e3fc648213a02315e8a16154bb86da7516fec7a09ec4d7417c"
+dependencies = [
+ "html-escape",
+ "line-col",
+]
+
+[[package]]
name = "humansize"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1136,6 +1146,7 @@ dependencies = [
"fehler",
"git-testament",
"html-escape",
+ "html-page",
"lalrpop",
"lalrpop-util",
"libc",
diff --git a/Cargo.toml b/Cargo.toml
index bcb118f..be306dc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ clap = { version = "3.2.10", features = ["derive"] }
env_logger = "0.9.1"
git-testament = "0.2.1"
html-escape = "0.2.11"
+html-page = "0.1.0"
lalrpop = "0.19.8"
lalrpop-util = "0.19.8"
libc = "0.2.126"
diff --git a/build.rs b/build.rs
index 52d9f76..9a45016 100644
--- a/build.rs
+++ b/build.rs
@@ -1,5 +1,9 @@
fn main() {
println!("cargo:rerun-if-changed=src/pagespec.lalrpop");
+ println!("building parser with larlpop");
lalrpop::process_root().unwrap();
+ println!("built parser with larlpop successfully");
+ println!("generating code with Subplot");
subplot_build::codegen("riki.subplot").expect("failed to generate code with Subplot");
+ println!("generated code with Subplot successfully");
}
diff --git a/riki.md b/riki.md
index a64b8ac..9938e72 100644
--- a/riki.md
+++ b/riki.md
@@ -623,7 +623,7 @@ _Requirement: the `meta title` directive sets page title._
given an installed riki
given file site/index.mdwn from meta
when I run riki build site output
-then file output/index.html contains "<title>Yo</title>"
+then file output/index.html contains "<TITLE>Yo</TITLE>"
~~~
~~~{#meta .file .markdown}
@@ -640,7 +640,8 @@ given an installed riki
given file site/a.mdwn from use_shortcut
given file site/b.mdwn from define_shortcut
when I run riki build site output
-then file output/a/index.html contains "<a href="https://example.com/foo/123">foo!123</a>"
+when I run cat output/a/index.html
+then file output/a/index.html contains "<A href="https://example.com/foo/123">foo!123</A>"
~~~
~~~{#use_shortcut .file .markdown}
@@ -660,13 +661,13 @@ given an installed riki
given file site/index.mdwn from table
when I run riki build site output
when I run cat output/index.html
-then file output/index.html contains "<table>"
-then file output/index.html contains "<th><td>Greeting</td>"
-then file output/index.html contains "<td>Greetee</td>"
-then file output/index.html contains "<tr><td>hello</td>"
-then file output/index.html contains "<td>world</td>"
-then file output/index.html contains "<tr><td>goodbye</td>"
-then file output/index.html contains "<td>cruel world</td>"
+then file output/index.html contains "<TABLE>"
+then file output/index.html contains "<TH><TD>Greeting</TD>"
+then file output/index.html contains "<TD>Greetee</TD>"
+then file output/index.html contains "<TR><TD>hello</TD>"
+then file output/index.html contains "<TD>world</TD>"
+then file output/index.html contains "<TR><TD>goodbye</TD>"
+then file output/index.html contains "<TD>cruel world</TD>"
~~~
~~~{#table .file .markdown}
@@ -686,8 +687,8 @@ given an installed riki
given file site/index.mdwn from toc
when I run riki build site output
when I run cat output/index.html
-then file output/index.html contains "<li>Introduction</li>"
-then file output/index.html contains "<li>Acknowledgements</li>"
+then file output/index.html contains "<LI>Introduction</LI>"
+then file output/index.html contains "<LI>Acknowledgements</LI>"
~~~
~~~{#toc .file .markdown}
diff --git a/src/bin/riki.rs b/src/bin/riki.rs
index 131326b..d219803 100644
--- a/src/bin/riki.rs
+++ b/src/bin/riki.rs
@@ -7,7 +7,7 @@ use riki::name::Name;
use riki::pagespec;
use riki::site::Site;
use riki::time::parse_timestamp;
-use riki::util::{canonicalize, copy_file_from_source, get_mtime, mkdir, set_mtime};
+use riki::util::{canonicalize, copy_file_from_source, get_mtime, mkdir, set_mtime, write};
use riki::version::Version;
use std::error::Error;
use std::path::{Path, PathBuf};
@@ -119,7 +119,7 @@ impl Build {
};
let output = page.meta().destination_filename();
debug!("writing: {}", output.display());
- htmlpage.write(&output)?;
+ write(&output, &format!("{}", htmlpage))?;
set_mtime(&output, page.meta().mtime())?;
}
@@ -168,10 +168,8 @@ impl Timestamps {
names.sort_by_cached_key(|name| name.page_path());
let whatchanged = git_whatchanged(&srcdir)?;
- eprintln!("whatchanged: {:#?}", whatchanged);
let dirty = git_dirty(&srcdir)?;
- eprintln!("dirty: {:#?}", dirty);
println!();
for name in names {
diff --git a/src/directive/toc.rs b/src/directive/toc.rs
index fa115b6..c4ffc9e 100644
--- a/src/directive/toc.rs
+++ b/src/directive/toc.rs
@@ -1,8 +1,41 @@
use crate::directive::{DirectiveError, DirectiveImplementation, Processed};
-use crate::html::{Content, Element, ElementTag};
use crate::page::PageMeta;
use crate::site::Site;
use crate::wikitext::ParsedDirective;
+use html_page::{Element, Tag, Visitor};
+use std::cmp::Ordering;
+
+#[derive(Debug, Default)]
+struct Headings {
+ headings: Vec<(usize, Element)>,
+}
+
+impl Visitor for Headings {
+ fn visit_text(&mut self, _: &str) {}
+ fn visit_element(&mut self, e: &Element) {
+ match e.tag() {
+ Tag::H1 => self.headings.push((1, e.clone())),
+ Tag::H2 => self.headings.push((2, e.clone())),
+ Tag::H3 => self.headings.push((3, e.clone())),
+ Tag::H4 => self.headings.push((4, e.clone())),
+ Tag::H5 => self.headings.push((5, e.clone())),
+ Tag::H6 => self.headings.push((6, e.clone())),
+ _ => (),
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+struct Text {
+ text: String,
+}
+
+impl Visitor for Text {
+ fn visit_text(&mut self, s: &str) {
+ self.text.push_str(s);
+ }
+ fn visit_element(&mut self, _: &Element) {}
+}
#[derive(Debug, Default, Eq, PartialEq)]
pub struct Toc {
@@ -35,51 +68,54 @@ impl Toc {
}
pub fn post_process(html: &Element, levels: usize) -> String {
- let headings: Vec<(usize, &[Content])> = html
- .children()
- .iter()
- .filter_map(|c| match c {
- Content::Elt(e) => Some(e),
- _ => None,
- })
- .filter_map(|e| match e.tag() {
- ElementTag::H1 => Some((1, e.children())),
- ElementTag::H2 => Some((2, e.children())),
- ElementTag::H3 => Some((3, e.children())),
- ElementTag::H4 => Some((4, e.children())),
- ElementTag::H5 => Some((5, e.children())),
- ElementTag::H6 => Some((6, e.children())),
- _ => None,
- })
- .collect();
- let mut html = String::new();
+ let mut headings = Headings::default();
+ headings.visit(html);
+
+ let mut stack = vec![];
let mut prev_level: usize = 0;
- for (level, text) in headings.iter() {
- if *level > levels {
- continue;
- } else if *level > prev_level {
- html.push_str("<ol>");
- } else if *level < prev_level {
- html.push_str("</ol>\n");
+ for (level, heading) in headings
+ .headings
+ .iter()
+ .filter(|(level, _)| *level < levels)
+ {
+ match level.cmp(&prev_level) {
+ Ordering::Greater => {
+ let list = Element::new(Tag::Ul);
+ stack.push(list);
+ }
+ Ordering::Less => {
+ assert!(!stack.is_empty());
+ let ending = stack.pop().unwrap();
+
+ assert!(!stack.is_empty());
+ let parent = stack.last_mut().unwrap();
+ parent.push_child(&ending);
+ }
+ Ordering::Equal => (),
+ }
+ if let Some(ol) = stack.last_mut() {
+ let mut text = Text::default();
+ text.visit(heading);
+ let li = Element::new(Tag::Li).with_text(&text.text);
+ ol.push_child(&li);
}
- html.push_str("<li>");
- Self::stringify(&mut html, text);
- html.push_str("</li>\n");
prev_level = *level;
}
- for _ in 0..prev_level {
- html.push_str("</ol>\n");
+
+ while stack.len() > 1 {
+ assert!(!stack.is_empty());
+ let ending = stack.pop().unwrap();
+
+ assert!(!stack.is_empty());
+ let parent = stack.last_mut().unwrap();
+ parent.push_child(&ending);
}
- html
- }
- fn stringify(buf: &mut String, bits: &[Content]) {
- for c in bits.iter() {
- match c {
- Content::Text(s) => buf.push_str(s),
- Content::Elt(e) => Self::stringify(buf, e.children()),
- Content::Html(h) => buf.push_str(h),
- }
+ let mut toc =
+ Element::new(Tag::Div).with_child(Element::new(Tag::H1).with_text("Contents"));
+ if let Some(e) = stack.get(0) {
+ toc.push_child(e);
}
+ format!("{}", toc)
}
}
diff --git a/src/error.rs b/src/error.rs
index d38c1e9..8f54a0f 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,4 +1,5 @@
use crate::html::HtmlError;
+use std::path::PathBuf;
#[derive(Debug, thiserror::Error)]
pub enum RikiError {
@@ -37,4 +38,7 @@ pub enum RikiError {
#[error(transparent)]
Site(#[from] crate::site::SiteError),
+
+ #[error("failed to write file {0}")]
+ WriteFile(PathBuf, #[source] std::io::Error),
}
diff --git a/src/html.rs b/src/html.rs
index e550ba7..adba870 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -2,74 +2,10 @@
#![deny(missing_docs)]
-use html_escape::{encode_double_quoted_attribute, encode_text};
+use html_page::Element;
use line_col::LineColLookup;
-use log::{debug, trace};
+use log::trace;
use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag};
-use std::fmt::Write as _;
-use std::io::Write;
-use std::path::{Path, PathBuf};
-
-/// A HTML page, consisting of a head and a body.
-#[derive(Debug)]
-pub struct HtmlPage {
- head: Element,
- body: Element,
-}
-
-impl Default for HtmlPage {
- fn default() -> Self {
- Self {
- head: Element::new(ElementTag::Head),
- body: Element::new(ElementTag::Body),
- }
- }
-}
-
-impl HtmlPage {
- /// Create a new HTML page from a head and a body element.
- pub fn new(head: Element, body: Element) -> Self {
- Self { head, body }
- }
-
- /// Return the page's head element.
- pub fn head(&self) -> &Element {
- &self.head
- }
-
- /// Return the page's body element.
- pub fn body(&self) -> &Element {
- &self.body
- }
-
- /// Try to serialize an HTML page into HTML text.
- pub fn serialize(&self) -> Result<String, HtmlError> {
- let mut html = Element::new(ElementTag::Html);
- html.push_child(Content::Elt(self.head.clone()));
- html.push_child(Content::Elt(self.body.clone()));
- html.serialize()
- }
-
- /// Try to write an HTML page as text into a file.
- pub fn write(&self, filename: &Path) -> Result<(), HtmlError> {
- if let Some(parent) = filename.parent() {
- trace!("parent: {}", parent.display());
- if !parent.exists() {
- debug!("creating directory {}", parent.display());
- std::fs::create_dir_all(parent)
- .map_err(|e| HtmlError::CreateDir(parent.into(), e))?;
- }
- }
-
- trace!("writing HTML: {}", filename.display());
- let mut f = std::fs::File::create(filename)
- .map_err(|e| HtmlError::CreateFile(filename.into(), e))?;
- let html = self.serialize()?;
- f.write_all(html.as_bytes())
- .map_err(|e| HtmlError::FileWrite(filename.into(), e))?;
- Ok(())
- }
-}
/// Parse Markdown text into an HTML element.
pub fn parse(markdown: &str) -> Result<Element, HtmlError> {
@@ -80,27 +16,26 @@ pub fn parse(markdown: &str) -> Result<Element, HtmlError> {
options.insert(Options::ENABLE_TASKLISTS);
let p = Parser::new_ext(markdown, options).into_offset_iter();
let linecol = LineColLookup::new(markdown);
- let mut stack = Stack::new();
- stack.push(Element::new(ElementTag::Body));
+ let mut stack = Stack::default();
+ stack.push(Element::new(html_page::Tag::Body));
for (event, loc) in p {
trace!("event {:?}", event);
let (line, col) = linecol.get(loc.start);
- let loc = Location::new(line, col);
match event {
Event::Start(tag) => match tag {
- Tag::Paragraph => stack.push_tag(ElementTag::P, loc),
+ Tag::Paragraph => stack.push_tag(html_page::Tag::P, line, col),
Tag::Heading(level, id, classes) => {
let tag = match level {
- HeadingLevel::H1 => ElementTag::H1,
- HeadingLevel::H2 => ElementTag::H2,
- HeadingLevel::H3 => ElementTag::H3,
- HeadingLevel::H4 => ElementTag::H4,
- HeadingLevel::H5 => ElementTag::H5,
- HeadingLevel::H6 => ElementTag::H6,
+ HeadingLevel::H1 => html_page::Tag::H1,
+ HeadingLevel::H2 => html_page::Tag::H2,
+ HeadingLevel::H3 => html_page::Tag::H3,
+ HeadingLevel::H4 => html_page::Tag::H4,
+ HeadingLevel::H5 => html_page::Tag::H5,
+ HeadingLevel::H6 => html_page::Tag::H6,
};
let mut h = Element::new(tag);
if let Some(id) = id {
- h.push_attribute(Attribute::new("id", id));
+ h.set_attribute("id", id);
}
if !classes.is_empty() {
let mut names = String::new();
@@ -110,40 +45,40 @@ pub fn parse(markdown: &str) -> Result<Element, HtmlError> {
}
names.push_str(c);
}
- h.push_attribute(Attribute::new("class", &names));
+ h.set_attribute("class", &names);
}
stack.push(h);
}
- Tag::BlockQuote => stack.push_tag(ElementTag::Blockquote, loc),
- Tag::CodeBlock(_) => stack.push_tag(ElementTag::Pre, loc),
- Tag::List(None) => stack.push_tag(ElementTag::Ul, loc),
+ Tag::BlockQuote => stack.push_tag(html_page::Tag::Blockquote, line, col),
+ Tag::CodeBlock(_) => stack.push_tag(html_page::Tag::Pre, line, col),
+ Tag::List(None) => stack.push_tag(html_page::Tag::Ul, line, col),
Tag::List(Some(start)) => {
- let mut e = Element::new(ElementTag::Ol).with_location(loc);
- e.push_attribute(Attribute::new("start", &format!("{}", start)));
+ let mut e = Element::new(html_page::Tag::Ol).with_location(line, col);
+ e.set_attribute("start", &format!("{}", start));
stack.push(e);
}
- Tag::Item => stack.push_tag(ElementTag::Li, loc),
+ Tag::Item => stack.push_tag(html_page::Tag::Li, line, col),
Tag::FootnoteDefinition(_) => unreachable!("{:?}", tag),
- Tag::Table(_) => stack.push_tag(ElementTag::Table, loc),
- Tag::TableHead => stack.push_tag(ElementTag::Th, loc),
- Tag::TableRow => stack.push_tag(ElementTag::Tr, loc),
- Tag::TableCell => stack.push_tag(ElementTag::Td, loc),
- Tag::Emphasis => stack.push_tag(ElementTag::Em, loc),
- Tag::Strong => stack.push_tag(ElementTag::Strong, loc),
- Tag::Strikethrough => stack.push_tag(ElementTag::Del, loc),
+ Tag::Table(_) => stack.push_tag(html_page::Tag::Table, line, col),
+ Tag::TableHead => stack.push_tag(html_page::Tag::Th, line, col),
+ Tag::TableRow => stack.push_tag(html_page::Tag::Tr, line, col),
+ Tag::TableCell => stack.push_tag(html_page::Tag::Td, line, col),
+ Tag::Emphasis => stack.push_tag(html_page::Tag::Em, line, col),
+ Tag::Strong => stack.push_tag(html_page::Tag::Strong, line, col),
+ Tag::Strikethrough => stack.push_tag(html_page::Tag::Del, line, col),
Tag::Link(_, url, title) => {
- let mut link = Element::new(ElementTag::A);
- link.push_attribute(Attribute::new("href", url.as_ref()));
+ let mut link = Element::new(html_page::Tag::A);
+ link.set_attribute("href", url.as_ref());
if !title.is_empty() {
- link.push_attribute(Attribute::new("title", title.as_ref()));
+ link.set_attribute("title", title.as_ref());
}
stack.push(link);
}
Tag::Image(_, url, title) => {
- let mut e = Element::new(ElementTag::Img);
- e.push_attribute(Attribute::new("src", url.as_ref()));
+ let mut e = Element::new(html_page::Tag::Img);
+ e.set_attribute("src", url.as_ref());
if !title.is_empty() {
- e.push_attribute(Attribute::new("title", title.as_ref()));
+ e.set_attribute("title", title.as_ref());
}
stack.push(e);
}
@@ -152,18 +87,32 @@ pub fn parse(markdown: &str) -> Result<Element, HtmlError> {
Tag::Paragraph => {
trace!("at end of paragraph, looking for definition list use");
let e = stack.pop();
- let s = as_plain_text(e.children());
+ let s = e.plain_text();
trace!("paragraph text: {:?}", s);
if s.starts_with(": ") || s.contains("\n: ") {
- return Err(HtmlError::DefinitionList(loc.line, loc.col));
+ return Err(HtmlError::DefinitionList(line, col));
}
- stack.append_child(Content::Elt(e));
+ stack.append_child(e);
+ }
+ Tag::Image(_, _, _) => {
+ // The way pulldown_cmark feeds us events, the alt
+ // text of an image ends up being the content of
+ // the img element. That's wrong for HTML, so we
+ // remove the content, and use it as the alt
+ // attribute instead.
+ let mut img = stack.pop();
+ eprintln!("IMAGE: {img:#?}");
+ assert_eq!(img.tag(), html_page::Tag::Img);
+ let alt_text = img.plain_text();
+ img.clear_children();
+ img.set_attribute("alt", &alt_text);
+ eprintln!("IMAGE after: {img:#?}");
+ stack.append_child(img);
}
Tag::Heading(_, _, _)
| Tag::List(_)
| Tag::Item
| Tag::Link(_, _, _)
- | Tag::Image(_, _, _)
| Tag::Emphasis
| Tag::Table(_)
| Tag::TableHead
@@ -174,313 +123,45 @@ pub fn parse(markdown: &str) -> Result<Element, HtmlError> {
| Tag::BlockQuote
| Tag::CodeBlock(_) => {
let e = stack.pop();
- stack.append_child(Content::Elt(e));
+ stack.append_child(e);
}
Tag::FootnoteDefinition(_) => unreachable!("{:?}", tag),
},
- Event::Text(s) => stack.append_str(s.as_ref()),
+ Event::Text(s) => stack.append_text(s.as_ref()),
Event::Code(s) => {
- let mut code = Element::new(ElementTag::Code);
- code.push_child(Content::Text(s.to_string()));
- stack.append_element(code);
+ let mut code = Element::new(html_page::Tag::Code);
+ code.push_text(s.to_string().as_ref());
+ stack.append_child(code);
}
- Event::Html(s) => stack.append_child(Content::Html(s.to_string())),
+ Event::Html(s) => stack.append_html(s.as_ref()),
Event::FootnoteReference(s) => trace!("footnote ref {:?}", s),
- Event::SoftBreak => stack.append_str("\n"),
- Event::HardBreak => stack.append_element(Element::new(ElementTag::Br)),
- Event::Rule => stack.append_element(Element::new(ElementTag::Hr)),
+ Event::SoftBreak => stack.append_text("\n"),
+ Event::HardBreak => stack.append_child(Element::new(html_page::Tag::Br)),
+ Event::Rule => stack.append_child(Element::new(html_page::Tag::Hr)),
Event::TaskListMarker(done) => {
let marker = if done {
"\u{2612} " // Unicode for box with X
} else {
"\u{2610} " // Unicode for empty box
};
- stack.append_str(marker);
+ stack.append_text(marker);
}
}
}
- let mut body = stack.pop();
+ eprintln!("STACK: {stack:#?}");
+ let body = stack.pop();
assert!(stack.is_empty());
- body.fix_up_img_alt();
+ // body.fix_up_img_alt();
Ok(body)
}
-fn as_plain_text(content: &[Content]) -> String {
- let mut buf = String::new();
- for c in content {
- if let Content::Text(s) = c {
- buf.push_str(s);
- }
- }
- buf
-}
-
-/// An HTML element.
-#[derive(Debug, Clone)]
-pub struct Element {
- loc: Option<Location>,
- tag: ElementTag,
- attrs: Vec<Attribute>,
- children: Vec<Content>,
-}
-
-impl Element {
- /// Create a new element.
- pub fn new(tag: ElementTag) -> Self {
- Self {
- loc: None,
- tag,
- attrs: vec![],
- children: vec![],
- }
- }
-
- fn with_location(mut self, loc: Location) -> Self {
- self.loc = Some(loc);
- self
- }
-
- fn push_attribute(&mut self, attr: Attribute) {
- self.attrs.push(attr);
- }
-
- /// Append a new child to the element.
- pub fn push_child(&mut self, child: Content) {
- self.children.push(child);
- }
-
- /// Return an element's tag.
- pub fn tag(&self) -> ElementTag {
- self.tag
- }
-
- /// Return all the children of an element.
- pub fn children(&self) -> &[Content] {
- &self.children
- }
-
- fn fix_up_img_alt(&mut self) {
- if self.tag == ElementTag::Img {
- let alt = as_plain_text(self.children());
- self.push_attribute(Attribute::new("alt", &alt));
- self.children.clear();
- } else {
- for child in self.children.iter_mut() {
- if let Content::Elt(kid) = child {
- kid.fix_up_img_alt();
- }
- }
- }
- }
-
- /// Serialize an element into HTML text.
- pub fn serialize(&self) -> Result<String, HtmlError> {
- let mut buf = String::new();
- self.serialize_to_buf_without_added_newlines(&mut buf)
- .map_err(HtmlError::Format)?;
- Ok(buf)
- }
-
- fn serialize_to_buf_without_added_newlines(
- &self,
- buf: &mut String,
- ) -> Result<(), std::fmt::Error> {
- if self.children.is_empty() {
- write!(buf, "<{}", self.tag.name())?;
- self.serialize_attrs_to_buf(buf)?;
- write!(buf, "/>")?;
- } else {
- write!(buf, "<{}", self.tag.name())?;
- self.serialize_attrs_to_buf(buf)?;
- write!(buf, ">")?;
- for c in self.children() {
- match c {
- Content::Text(s) => buf.push_str(&encode_text(s)),
- Content::Elt(e) => e.serialize_to_buf_adding_block_newline(buf)?,
- Content::Html(s) => buf.push_str(s),
- }
- }
- write!(buf, "</{}>", self.tag.name())?;
- }
- Ok(())
- }
-
- fn serialize_to_buf_adding_block_newline(
- &self,
- buf: &mut String,
- ) -> Result<(), std::fmt::Error> {
- if self.tag.is_block() {
- writeln!(buf)?;
- }
- self.serialize_to_buf_without_added_newlines(buf)
- }
-
- fn serialize_attrs_to_buf(&self, buf: &mut String) -> Result<(), std::fmt::Error> {
- for attr in self.attrs.iter() {
- write!(buf, " {}", attr.name())?;
- if let Some(value) = attr.value() {
- write!(buf, "=\"{}\"", encode_double_quoted_attribute(value))?;
- }
- }
- Ok(())
- }
-}
-
-/// The tag of an HTML element.
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-#[allow(missing_docs)]
-pub enum ElementTag {
- Html,
- Head,
- Body,
- H1,
- H2,
- H3,
- H4,
- H5,
- H6,
- P,
- Ol,
- Ul,
- Li,
- Blockquote,
- Pre,
- Em,
- Strong,
- Del,
- A,
- Img,
- Table,
- Title,
- Th,
- Tr,
- Td,
- Br,
- Hr,
- Code,
-}
-
-impl ElementTag {
- fn name(&self) -> &str {
- match self {
- Self::Html => "html",
- Self::Head => "head",
- Self::Body => "body",
- Self::H1 => "h1",
- Self::H2 => "h2",
- Self::H3 => "h3",
- Self::H4 => "h4",
- Self::H5 => "h5",
- Self::H6 => "h6",
- Self::P => "p",
- Self::Ol => "ol",
- Self::Ul => "ul",
- Self::Li => "li",
- Self::Blockquote => "blockquote",
- Self::Pre => "pre",
- Self::Em => "em",
- Self::Strong => "strong",
- Self::Del => "del",
- Self::A => "a",
- Self::Img => "img",
- Self::Table => "table",
- Self::Th => "th",
- Self::Title => "title",
- Self::Tr => "tr",
- Self::Td => "td",
- Self::Br => "br",
- Self::Hr => "hr",
- Self::Code => "code",
- }
- }
-
- fn is_block(&self) -> bool {
- matches!(
- self,
- Self::Html
- | Self::Head
- | Self::Body
- | Self::H1
- | Self::H2
- | Self::H3
- | Self::H4
- | Self::H5
- | Self::H6
- | Self::P
- | Self::Ol
- | Self::Ul
- | Self::Li
- | Self::Blockquote
- | Self::Table
- | Self::Th
- | Self::Tr
- | Self::Br
- | Self::Hr
- )
- }
-}
-
-/// An attribute of an HTML element.
-#[derive(Clone, Debug)]
-pub struct Attribute {
- name: String,
- value: Option<String>,
-}
-
-impl Attribute {
- fn new(name: &str, value: &str) -> Self {
- Self {
- name: name.into(),
- value: Some(value.into()),
- }
- }
-
- /// Return the name of the attribute.
- pub fn name(&self) -> &str {
- &self.name
- }
-
- /// Return the value of the attribute, if any.
- pub fn value(&self) -> Option<&str> {
- self.value.as_deref()
- }
-}
-
-/// Content in HTML.
-#[derive(Clone, Debug)]
-pub enum Content {
- /// Arbitrary text.
- Text(String),
-
- /// An HTML element.
- Elt(Element),
-
- /// Arbitrary HTML text.
- Html(String),
-}
-
-#[derive(Debug, Clone, Copy)]
-struct Location {
- line: usize,
- col: usize,
-}
-
-impl Location {
- fn new(line: usize, col: usize) -> Self {
- Self { line, col }
- }
-}
-
+#[derive(Debug, Default)]
struct Stack {
stack: Vec<Element>,
}
impl Stack {
- fn new() -> Self {
- Self { stack: vec![] }
- }
-
fn is_empty(&self) -> bool {
self.stack.is_empty()
}
@@ -490,8 +171,8 @@ impl Stack {
self.stack.push(e);
}
- fn push_tag(&mut self, tag: ElementTag, loc: Location) {
- self.push(Element::new(tag).with_location(loc));
+ fn push_tag(&mut self, tag: html_page::Tag, line: usize, col: usize) {
+ self.push(Element::new(tag).with_location(line, col));
}
fn pop(&mut self) -> Element {
@@ -500,43 +181,33 @@ impl Stack {
e
}
- fn append_child(&mut self, child: Content) {
+ fn append_child(&mut self, child: Element) {
trace!("appended {:?}", child);
let mut parent = self.stack.pop().unwrap();
- parent.push_child(child);
+ parent.push_child(&child);
self.stack.push(parent);
}
- fn append_str(&mut self, text: &str) {
- self.append_child(Content::Text(text.into()));
+ fn append_text(&mut self, child: &str) {
+ trace!("appended {:?}", child);
+ let mut parent = self.stack.pop().unwrap();
+ parent.push_text(child);
+ self.stack.push(parent);
}
- fn append_element(&mut self, e: Element) {
- self.append_child(Content::Elt(e));
+ fn append_html(&mut self, child: &str) {
+ trace!("appended {:?}", child);
+ let mut parent = self.stack.pop().unwrap();
+ parent.push_html(child);
+ self.stack.push(parent);
}
}
/// Errors from the `html` module.
#[derive(Debug, thiserror::Error)]
pub enum HtmlError {
- /// Failed to create a directory.
- #[error("failed to create directory {0}")]
- CreateDir(PathBuf, #[source] std::io::Error),
-
- /// Failed to create a file.
- #[error("failed to create file {0}")]
- CreateFile(PathBuf, #[source] std::io::Error),
-
- /// Failed to write to a file.
- #[error("failed to write to file {0}")]
- FileWrite(PathBuf, #[source] std::io::Error),
-
/// Input contains an attempt to use a definition list in
/// Markdown.
#[error("attempt to use definition lists in Markdown: line {0}, column {1}")]
DefinitionList(usize, usize),
-
- /// String formatting error. This is likely a programming error.
- #[error("string formatting error: {0}")]
- Format(#[source] std::fmt::Error),
}
diff --git a/src/page.rs b/src/page.rs
index 453c29b..485632d 100644
--- a/src/page.rs
+++ b/src/page.rs
@@ -1,10 +1,11 @@
use crate::directive::{Processed, Toc};
-use crate::html::{parse, Content, Element, ElementTag, HtmlPage};
+use crate::html::parse;
use crate::name::Name;
use crate::parser::WikitextParser;
use crate::site::Site;
use crate::util::get_mtime;
use crate::wikitext::Snippet;
+use html_page::{Document, Element, Tag};
use log::{info, trace};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
@@ -169,23 +170,20 @@ impl MarkdownPage {
&self.meta
}
- pub fn body_to_html(&self) -> Result<HtmlPage, PageError> {
- let head = Element::new(ElementTag::Head);
- let body = parse(self.markdown())?;
- Ok(HtmlPage::new(head, body))
+ pub fn body_to_html(&self) -> Result<Document, PageError> {
+ let mut html = Document::default();
+ html.push_children(&parse(self.markdown())?);
+ Ok(html)
}
- pub fn to_html(&self) -> Result<HtmlPage, PageError> {
- let mut title = Element::new(ElementTag::Title);
- title.push_child(Content::Text(self.meta.title().into()));
+ pub fn to_html(&self) -> Result<Document, PageError> {
+ let mut html = Document::default();
- let mut head = Element::new(ElementTag::Head);
- head.push_child(Content::Elt(title));
+ let title = Element::new(Tag::Title).with_text(self.meta.title());
+ html.push_to_head(&title);
+ html.push_children(&parse(self.markdown())?);
- let body = parse(self.markdown())?;
-
- trace!("MarkdownPage::to_html: head={:?}", head);
- Ok(HtmlPage::new(head, body))
+ Ok(html)
}
}
diff --git a/src/util.rs b/src/util.rs
index eac71f1..227f251 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -1,6 +1,7 @@
use libc::{timespec, utimensat, AT_FDCWD, AT_SYMLINK_NOFOLLOW};
use log::{debug, error, trace};
use std::ffi::CString;
+use std::io::Write;
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -19,6 +20,14 @@ pub enum UtilError {
#[error("failed to copy {0} to {1}")]
CopyFile(PathBuf, PathBuf, #[source] std::io::Error),
+ /// Failed to create a file.
+ #[error("failed to create file {0}")]
+ CreateFile(PathBuf, #[source] std::io::Error),
+
+ /// Failed to write to a file.
+ #[error("failed to write to file {0}")]
+ FileWrite(PathBuf, #[source] std::io::Error),
+
#[error("failed to get file metadata: {0}")]
FileMetadata(PathBuf, #[source] std::io::Error),
@@ -48,6 +57,23 @@ pub fn copy(src: &Path, dest: &Path) -> Result<(), UtilError> {
Ok(())
}
+pub fn write(filename: &Path, content: &str) -> Result<(), UtilError> {
+ if let Some(parent) = filename.parent() {
+ trace!("parent: {}", parent.display());
+ if !parent.exists() {
+ debug!("creating directory {}", parent.display());
+ std::fs::create_dir_all(parent).map_err(|e| UtilError::CreateDir(parent.into(), e))?;
+ }
+ }
+
+ trace!("writing HTML: {}", filename.display());
+ let mut f =
+ std::fs::File::create(filename).map_err(|e| UtilError::CreateFile(filename.into(), e))?;
+ f.write_all(content.as_bytes())
+ .map_err(|e| UtilError::FileWrite(filename.into(), e))?;
+ Ok(())
+}
+
pub fn get_mtime(src: &Path) -> Result<SystemTime, UtilError> {
let metadata = std::fs::metadata(src).map_err(|e| UtilError::FileMetadata(src.into(), e))?;
let mtime = metadata