diff options
author | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2023-10-10 07:08:21 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers+gitlab@digital-scurf.org> | 2023-10-10 07:08:21 +0000 |
commit | 58350fed3216b3ec2875503f6036e1a7f982088e (patch) | |
tree | 9fb29d0b107d5e1edb78954e68e35270b5fc391c | |
parent | 478147c59de2547cb1e3e0a0cd5309c25f4453dc (diff) | |
parent | 76838e218f8c0943de43befce434fa8845a68e86 (diff) | |
download | subplot-58350fed3216b3ec2875503f6036e1a7f982088e.tar.gz |
Merge branch 'liw/html-correctness' into 'main'
Various HTML output fixes
Closes #336
See merge request subplot/subplot!355
-rwxr-xr-x | check | 5 | ||||
-rw-r--r-- | src/html.rs | 63 | ||||
-rw-r--r-- | src/md.rs | 23 |
3 files changed, 73 insertions, 18 deletions
@@ -320,7 +320,9 @@ def check_subplots(r): base = os.path.basename(subplot) base, _ = os.path.splitext(subplot) base = os.path.join(output, base) - r.docgen(subplot, doc_template, base + ".html", cwd=dirname) + html = base + ".html" + r.docgen(subplot, doc_template, html, cwd=dirname) + r.runcmd(["tidy", "-errors", html], cwd=dirname) def tail(filename, numlines=100): @@ -344,6 +346,7 @@ def check_tooling(r): "plantuml", "rustc", "rustfmt", + "tidy", ] for command in commands: if not r.got_command(command): diff --git a/src/html.rs b/src/html.rs index f863727..1e0dafa 100644 --- a/src/html.rs +++ b/src/html.rs @@ -7,10 +7,13 @@ use line_col::LineColLookup; use log::{debug, trace}; use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fmt::Write as _; use std::io::Write; use std::path::{Path, PathBuf}; +const DOCTYPE: &str = "<!DOCTYPE html>"; + /// A HTML page, consisting of a head and a body. #[derive(Debug)] pub struct HtmlPage { @@ -47,8 +50,11 @@ impl HtmlPage { 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() + let mut body = Element::new(ElementTag::Body); + body.push_child(Content::Elt(self.body.clone())); + html.push_child(Content::Elt(body)); + let html = html.serialize()?; + Ok(format!("{}\n{}", DOCTYPE, html)) } /// Try to write an HTML page as text into a file. @@ -82,7 +88,7 @@ pub fn parse(filename: &Path, markdown: &str) -> Result<Element, HtmlError> { 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)); + stack.push(Element::new(ElementTag::Div)); for (event, loc) in p { trace!("event {:?}", event); let (line, col) = linecol.get(loc.start); @@ -150,6 +156,7 @@ pub fn parse(filename: &Path, markdown: &str) -> Result<Element, HtmlError> { Tag::Image(_, url, title) => { let mut e = Element::new(ElementTag::Img); e.push_attribute(Attribute::new("src", url.as_ref())); + e.push_attribute(Attribute::new("alt", title.as_ref())); if !title.is_empty() { e.push_attribute(Attribute::new("title", title.as_ref())); } @@ -275,6 +282,13 @@ impl Element { self.attrs.push(attr); } + /// Drop all attributes with a given name. + pub fn drop_attributes(&mut self, unwanted: &[&str]) { + for uw in unwanted { + self.attrs.retain(|a| a.name() != *uw); + } + } + /// Append a new child to the element. pub fn push_child(&mut self, child: Content) { self.children.push(child); @@ -321,9 +335,11 @@ impl Element { 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(); + if !self.attrs.iter().any(|a| a.name() == "alt") { + 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 { @@ -376,9 +392,14 @@ impl Element { } fn serialize_attrs_to_buf(&self, buf: &mut String) -> Result<(), std::fmt::Error> { + let mut attrs = Attributes::default(); for attr in self.attrs.iter() { - write!(buf, " {}", attr.name())?; - if let Some(value) = attr.value() { + attrs.push(attr); + } + + for (name, value) in attrs.iter() { + write!(buf, " {}", name)?; + if !value.is_empty() { write!(buf, "=\"{}\"", encode_double_quoted_attribute(value))?; } } @@ -487,6 +508,32 @@ impl ElementTag { } } +#[derive(Debug, Default, Clone)] +struct Attributes { + attrs: HashMap<String, String>, +} + +impl Attributes { + fn push(&mut self, attr: &Attribute) { + if let Some(new_value) = attr.value() { + if let Some(old_value) = self.attrs.get_mut(attr.name()) { + assert!(!old_value.is_empty()); + old_value.push(' '); + old_value.push_str(new_value); + } else { + self.attrs.insert(attr.name().into(), new_value.into()); + } + } else { + assert!(!self.attrs.contains_key(attr.name())); + self.attrs.insert(attr.name().into(), "".into()); + } + } + + fn iter(&self) -> impl Iterator<Item = (&String, &String)> { + self.attrs.iter() + } +} + /// An attribute of an HTML element. #[derive(Clone, Debug)] pub struct Attribute { @@ -324,6 +324,7 @@ fn extract_scenario(e: &[StructureElement]) -> Result<(Option<Scenario>, usize), } mod typeset { + const UNWANTED_ATTRS: &[&str] = &["add-newline"]; use crate::html::{Attribute, Content, Element, ElementTag}; // use crate::parser::parse_scenario_snippet; @@ -338,7 +339,7 @@ mod typeset { use base64::prelude::{Engine as _, BASE64_STANDARD}; pub(crate) fn typeset_element(e: &Element) -> Result<Element, SubplotError> { - match e.tag() { + let new = match e.tag() { ElementTag::Pre if e.has_attr("class", "scenario") => typeset_scenario(e), ElementTag::Pre if e.has_attr("class", "file") => typeset_file(e), ElementTag::Pre if e.has_attr("class", "example") => typeset_example(e), @@ -360,7 +361,10 @@ mod typeset { } Ok(new) } - } + }; + let mut new = new?; + new.drop_attributes(UNWANTED_ATTRS); + Ok(new) } fn typeset_scenario(e: &Element) -> Result<Element, SubplotError> { @@ -378,19 +382,19 @@ mod typeset { fn typeset_dot(e: &Element) -> Result<Element, SubplotError> { let dot = e.content(); let svg = DotMarkup::new(&dot).as_svg()?; - Ok(svg_to_element(svg)) + Ok(svg_to_element(svg, "Dot diagram")) } fn typeset_plantuml(e: &Element) -> Result<Element, SubplotError> { let markup = e.content(); let svg = PlantumlMarkup::new(&markup).as_svg()?; - Ok(svg_to_element(svg)) + Ok(svg_to_element(svg, "UML diagram")) } fn typeset_pikchr(e: &Element) -> Result<Element, SubplotError> { let markup = e.content(); let svg = PikchrMarkup::new(&markup, None).as_svg()?; - Ok(svg_to_element(svg)) + Ok(svg_to_element(svg, "Pikchr diagram")) } fn typeset_roadmap(e: &Element) -> Result<Element, SubplotError> { @@ -400,12 +404,12 @@ mod typeset { let roadmap = roadmap::from_yaml(&yaml)?; let dot = roadmap.format_as_dot(WIDTH)?; let svg = DotMarkup::new(&dot).as_svg()?; - Ok(svg_to_element(svg)) + Ok(svg_to_element(svg, "Road map")) } - fn svg_to_element(svg: Svg) -> Element { + fn svg_to_element(svg: Svg, alt: &str) -> Element { let url = svg_as_data_url(svg); - let img = html_img(&url); + let img = html_img(&url, alt); html_p(vec![Content::Elt(img)]) } @@ -422,9 +426,10 @@ mod typeset { new } - fn html_img(src: &str) -> Element { + fn html_img(src: &str, alt: &str) -> Element { let mut new = Element::new(ElementTag::Img); new.push_attribute(Attribute::new("src", src)); + new.push_attribute(Attribute::new("alt", alt)); new } } |