Index: .efiles ================================================================== --- .efiles +++ .efiles @@ -1,2 +1,6 @@ Cargo.toml +README.md +www/index.md +www/changelog.md src/lib.rs +tests/gentests.rs Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,20 +1,36 @@ [package] name = "sidoc-html5" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "0BSD" -categories = [ "web-programming" ] +# https://crates.io/category_slugs +categories = [ "template-engine", "web-programming" ] keywords = [ "sidoc", "html" ] repository = "https://repos.qrnch.tech/pub/sidoc-html5" description = "Helper functions for generating HTML5 documents for sidoc." exclude = [ ".efiles", ".fossil-settings", ".fslckout", + "bacon.toml", "rustfmt.toml" ] +# https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section +[badges] +maintenance = { status = "passively-maintained" } + +[features] +extra-validation = [ "lazy_static" ] + [dependencies] -html-escape = { version = "0.2.11" } -sidoc = { version = "0.1.0" } +html-escape = { version = "0.2.13" } +lazy_static = { version = "1.5.0", optional = true } +sidoc = { version = "0.1.1" } + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } ADDED README.md Index: README.md ================================================================== --- /dev/null +++ README.md @@ -0,0 +1,4 @@ +# sidoc-html5 + +HTML5 generation wrapper for sidoc. + ADDED bacon.toml Index: bacon.toml ================================================================== --- /dev/null +++ bacon.toml @@ -0,0 +1,126 @@ +# This is a configuration file for the bacon tool +# +# Complete help on configuration: https://dystroy.org/bacon/config/ +# +# You may check the current default at +# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml + +default_job = "clippy-all" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = [ + "cargo", "clippy", + "--color", "always", +] +need_stdout = false + +# Run clippy on all targets +# To disable some lints, you may change the job this way: +# [jobs.clippy-all] +# command = [ +# "cargo", "clippy", +# "--all-targets", +# "--color", "always", +# "--", +# "-A", "clippy::bool_to_int_with_if", +# "-A", "clippy::collapsible_if", +# "-A", "clippy::derive_partial_eq_without_eq", +# ] +# need_stdout = false +[jobs.clippy-all] +command = [ + "cargo", "clippy", + "--all-targets", + "--color", "always", +] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = [ + "cargo", "test", "--color", "always", + "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 +] +need_stdout = true + +[jobs.nextest] +command = [ + "cargo", "nextest", "run", + "--color", "always", + "--hide-progress-bar", "--failure-output", "final" +] +need_stdout = true +analyzer = "nextest" + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# if it makes sense for this crate. +# Don't forget the `--color always` part or the errors won't be +# properly parsed. +[jobs.run] +command = [ + "cargo", "run", + "--color", "always", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# Run your long-running application (eg server) and have the result displayed in bacon. +# For programs that never stop (eg a server), `background` is set to false +# to have the cargo run output immediately displayed instead of waiting for +# program's end. +# 'on_change_strategy' is set to `kill_then_restart` to have your program restart +# on every change (an alternative would be to use the 'F5' key manually in bacon). +# If you often use this job, it makes sense to override the 'r' key by adding +# a binding `r = job:run-long` at the end of this file . +[jobs.run-long] +command = [ + "cargo", "run", + "--color", "always", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--color", "always", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -1,6 +1,19 @@ use std::borrow::Cow; + +#[cfg(feature = "extra-validation")] +use std::collections::HashSet; + +#[cfg(feature = "extra-validation")] +lazy_static::lazy_static! { + static ref VOID_ELEMENTS: HashSet<&'static str> = { + HashSet::from([ + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", + "meta", "param", "source", "track", "wbr" + ]) + }; +} enum AttrType<'a> { KV(&'a str, String), Bool(&'a str), Data(&'a str, String), @@ -12,11 +25,12 @@ classes: Vec<&'a str>, alst: Vec> } impl<'a> Element<'a> { - pub fn new(tag: &'a str) -> Self { + #[must_use] + pub const fn new(tag: &'a str) -> Self { Element { tag, classes: Vec::new(), alst: Vec::new() } @@ -28,14 +42,28 @@ /// use sidoc_html5::Element; /// let element = Element::new("p") /// .class("warning"); /// ``` #[inline] + #[must_use] pub fn class(mut self, cls: &'a str) -> Self { self.classes.push(cls); self } + + /// Assign a class to this element in-place. + /// + /// ``` + /// use sidoc_html5::Element; + /// let mut element = Element::new("p"); + /// element.class_r("warning"); + /// ``` + #[inline] + pub fn class_r(&mut self, cls: &'a str) -> &mut Self { + self.classes.push(cls); + self + } /// Assign a "flag attribute" to the element. /// /// Flag attributes do not have (explicit) values, and are used to mark /// elements as `selected` or `checked` and such. @@ -45,14 +73,21 @@ /// let element = Element::new("input") /// .raw_attr("type", "checkbox") /// .flag("checked"); /// ``` #[inline] + #[must_use] pub fn flag(mut self, key: &'a str) -> Self { self.alst.push(AttrType::Bool(key)); self } + + #[inline] + pub fn flag_r(&mut self, key: &'a str) -> &mut Self { + self.alst.push(AttrType::Bool(key)); + self + } /// Conditionally add a flag attribute. /// /// Flag attributes do not have (explicit) values, and are used to mark /// elements as `selected` or `checked` and such. @@ -63,10 +98,14 @@ /// let element = Element::new("option") /// .flag_if(*id == 3, "selected"); /// } /// ``` #[inline] + /* + #[deprecated(since = "0.1.2", note = "Use .mod_if() with .flag() instead.")] + */ + #[must_use] pub fn flag_if(mut self, f: bool, key: &'a str) -> Self { if f { self.alst.push(AttrType::Bool(key)); } self @@ -83,10 +122,11 @@ /// use sidoc_html5::Element; /// let elem = Element::new("input") /// .attr("name", "foo"); /// ``` #[inline] + #[must_use] pub fn attr(mut self, key: &'a str, value: impl AsRef) -> Self { debug_assert!( key != "class", "Use the dedicated .class() method to add classes to elements" ); @@ -99,10 +139,14 @@ /// Conditionally add an attribute. /// /// The value is escaped as needed. #[inline] + /* + #[deprecated(since = "0.1.2", note = "Use .mod_if() with .attr() instead.")] + */ + #[must_use] pub fn attr_if(self, flag: bool, key: &'a str, value: V) -> Self where V: AsRef { debug_assert!( @@ -121,10 +165,11 @@ /// use sidoc_html5::Element; /// let elem = Element::new("tr") /// .data_attr("name", "foo"); /// ``` #[inline] + #[must_use] pub fn data_attr(mut self, key: &'a str, value: impl AsRef) -> Self { debug_assert!( key != "class", "Use the dedicated .class() method to add classes to elements" ); @@ -132,10 +177,35 @@ key, html_escape::encode_double_quoted_attribute(value.as_ref()).to_string() )); self } + + #[inline] + pub fn data_attr_r( + &mut self, + key: &'a str, + value: impl AsRef + ) -> &mut Self { + debug_assert!( + key != "class", + "Use the dedicated .class() method to add classes to elements" + ); + self.alst.push(AttrType::Data( + key, + html_escape::encode_double_quoted_attribute(value.as_ref()).to_string() + )); + self + } + + + #[inline] + #[must_use] + pub fn data_flag(mut self, key: &'a str) -> Self { + self.alst.push(AttrType::BoolData(key)); + self + } /// Conditionally add a boolean `data-` attribute. /// /// Add a `data-foo=true` attribute to an element: /// ``` @@ -143,10 +213,17 @@ /// let val = 7; /// let elem = Element::new("p") /// .data_flag_if(val > 5, "foo"); /// ``` #[inline] + /* + #[deprecated( + since = "0.1.2", + note = "Use .mod_if() with .data_flag() instead." + )] + */ + #[must_use] pub fn data_flag_if(mut self, flag: bool, key: &'a str) -> Self { if flag { self.alst.push(AttrType::BoolData(key)); } self @@ -166,10 +243,12 @@ /// let elem = Element::new("form") /// .optattr("id", something) /// .optattr("name", nothing); /// ``` #[inline] + #[must_use] + #[allow(clippy::needless_pass_by_value)] pub fn optattr(mut self, key: &'a str, value: Option) -> Self where T: AsRef { if let Some(v) = value.as_ref() { @@ -195,16 +274,26 @@ /// // Have a value -- format it and append "-aboo" to it. /// format!("{}-aboo", v) /// }); /// ``` #[inline] - pub fn optattr_map( - mut self, - key: &'a str, - value: Option, - f: F - ) -> Self + /* + #[deprecated(since = "0.1.2", note = "Use .map_attr() instead.")] + */ + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn optattr_map(self, key: &'a str, value: Option, f: F) -> Self + where + F: Fn(&T) -> String + { + self.map_attr(key, value, f) + } + + #[inline] + #[must_use] + #[allow(clippy::needless_pass_by_value)] + pub fn map_attr(mut self, key: &'a str, value: Option, f: F) -> Self where F: Fn(&T) -> String { if let Some(v) = value.as_ref() { let s = f(v); @@ -217,20 +306,55 @@ } /// If an an optional input value is set, apply a function on the contained /// value. #[inline] + /* + #[deprecated(since = "0.1.2", note = "Use .map() instead.")] + */ + #[must_use] pub fn opt_map(self, value: Option<&'_ T>, f: F) -> Self where F: Fn(Self, &T) -> Self { + self.map(value, f) + } + + #[must_use] + pub fn map(self, value: Option<&'_ T>, f: F) -> Self + where + F: Fn(Self, &T) -> Self + { if let Some(v) = value.as_ref() { f(self, v) } else { self } } + + /// If an an optional input value is set, apply a function on the contained + /// value. + /// + /// ``` + /// use sidoc_html5::Element; + /// let opt_str = Some("enabled"); + /// Element::new("body") + /// .map_opt(opt_str, |this, s| { + /// this.flag(s) + /// }); + /// ``` + #[inline] + #[must_use] + pub fn map_opt(self, o: Option, f: F) -> Self + where + F: FnOnce(Self, T) -> Self + { + match o { + Some(t) => f(self, t), + None => self + } + } /// Conditionally call a function to add an attribute with a generated value. /// /// ``` /// use sidoc_html5::Element; @@ -239,10 +363,11 @@ /// .map_attr_if(!sv.is_empty(), "data-mylist", &sv, |v: &Vec| { /// v.join(",") /// }); /// ``` #[inline] + #[must_use] pub fn map_attr_if( self, flag: bool, key: &'a str, data: &T, @@ -255,10 +380,54 @@ self.attr(key, f(data)) } else { self } } + + + /// Conditionally call a closure to modify `self` if a predicate is true. + /// + /// ``` + /// use sidoc_html5::Element; + /// let someval = 42; + /// Element::new("body") + /// .map_if(someval == 42, |obj| obj.flag("selected")); + /// ``` + #[inline] + #[must_use] + pub fn map_if(self, flag: bool, f: F) -> Self + where + F: FnOnce(Self) -> Self + { + if flag { + f(self) + } else { + self + } + } + + /// Conditionally call a closure to modify `self`, in-place, if a predicate + /// is true. + /// + /// ``` + /// use sidoc_html5::Element; + /// let someval = 42; + /// let mut e = Element::new("body"); + /// e.mod_if(someval == 42, |obj| { + /// obj.flag_r("selected"); + /// }); + /// ``` + #[inline] + pub fn mod_if(&mut self, flag: bool, f: F) -> &mut Self + where + F: FnOnce(&mut Self) + { + if flag { + f(self); + } + self + } } /// Methods that don't transform the input. impl<'a> Element<'a> { /// Add an attribute. @@ -269,10 +438,12 @@ /// use sidoc_html5::Element; /// let elem = Element::new("form") /// .raw_attr("id", "foo"); /// ``` #[inline] + #[must_use] + #[allow(clippy::needless_pass_by_value)] pub fn raw_attr(mut self, key: &'a str, value: impl ToString) -> Self { debug_assert!( key != "class", "Use the dedicated .class() method to add classes to elements" ); @@ -292,10 +463,11 @@ /// let elem = Element::new("form") /// .raw_optattr("id", something) /// .raw_optattr("name", nothing); /// ``` #[inline] + #[must_use] pub fn raw_optattr(mut self, key: &'a str, value: Option<&T>) -> Self where T: ToString { if let Some(v) = value.as_ref() { @@ -306,10 +478,12 @@ /// Add an attribute if a condition is true. /// /// The attribute value is not escaped. #[inline] + #[allow(clippy::needless_pass_by_value)] + #[must_use] pub fn raw_attr_if( self, flag: bool, key: &'a str, value: impl ToString @@ -334,18 +508,18 @@ ret.push(format!(r#"class="{}""#, self.classes.join(" "))); } let it = self.alst.iter().map(|a| match a { AttrType::KV(k, v) => { - format!(r#"{}="{}""#, k, v) + format!(r#"{k}="{v}""#) } - AttrType::Bool(a) => a.to_string(), + AttrType::Bool(a) => (*a).to_string(), AttrType::Data(k, v) => { - format!(r#"data-{}="{}""#, k, v) + format!(r#"data-{k}="{v}""#) } AttrType::BoolData(a) => { - format!("data-{}=true", a) + format!("data-{a}") } }); ret.extend(it); @@ -364,14 +538,21 @@ /// .sub(&mut bldr, |bldr| { /// Element::new("br") /// .add_empty(bldr); /// }); /// ``` + /// + /// # Panics + /// If the `extra-validation` feature is enabled, panic if the tag name is + /// not a known "void" element. pub fn sub(self, bldr: &mut sidoc::Builder, mut f: F) where F: FnMut(&mut sidoc::Builder) { + #[cfg(feature = "extra-validation")] + assert!(!VOID_ELEMENTS.contains(self.tag)); + if let Some(lst) = self.gen_attr_list() { bldr.scope( format!("<{} {}>", self.tag, lst.join(" ")), Some(format!("", self.tag)) ); @@ -384,20 +565,28 @@ f(bldr); bldr.exit(); } } + /// Methods inserting element into a sidoc context. impl<'a> Element<'a> { /// Consume `self` and add a empty tag representation of element to a sidoc /// builder. /// /// An empty/void tag comes is one which does not have a closing tag: /// ``. + /// + /// # Panics + /// If the `extra-validation` feature is enabled, panic if the tag name is + /// not a known "void" element. #[inline] pub fn add_empty(self, bldr: &mut sidoc::Builder) { + #[cfg(feature = "extra-validation")] + assert!(VOID_ELEMENTS.contains(self.tag)); + let line = if let Some(alst) = self.gen_attr_list() { format!("<{} {}>", self.tag, alst.join(" ")) } else { format!("<{}>", self.tag) }; @@ -419,12 +608,19 @@ /// .add_content("This is the text content", &mut bldr); /// ``` /// /// The example above should generate: /// `` + /// + /// # Panics + /// If the `extra-validation` feature is enabled, panic if the tag name is + /// not a known "void" element. #[inline] pub fn add_content(self, text: &str, bldr: &mut sidoc::Builder) { + #[cfg(feature = "extra-validation")] + assert!(!VOID_ELEMENTS.contains(self.tag)); + let line = if let Some(alst) = self.gen_attr_list() { format!( "<{} {}>{}", self.tag, alst.join(" "), @@ -454,38 +650,56 @@ /// .add_raw_content("Do Stuff", &mut bldr); /// ``` /// /// The example above should generate: /// `` + /// + /// # Panics + /// If the `extra-validation` feature is enabled, panic if the tag name is + /// not a known "void" element. #[inline] pub fn add_raw_content(self, text: &str, bldr: &mut sidoc::Builder) { + #[cfg(feature = "extra-validation")] + assert!(!VOID_ELEMENTS.contains(self.tag)); + let line = if let Some(alst) = self.gen_attr_list() { format!("<{} {}>{}", self.tag, alst.join(" "), text, self.tag) } else { format!("<{}>{}", self.tag, text, self.tag) }; bldr.line(line); } + /// # Panics + /// If the `extra-validation` feature is enabled, panic if the tag name is + /// not a known "void" element. pub fn add_opt_content(self, text: &Option, bldr: &mut sidoc::Builder) where T: AsRef { - let t = if let Some(t) = text { - html_escape::encode_text(t) - } else { - Cow::from("") - }; + #[cfg(feature = "extra-validation")] + assert!(!VOID_ELEMENTS.contains(self.tag)); + + let t = text + .as_ref() + .map_or_else(|| Cow::from(""), |t| html_escape::encode_text(t)); let line = if let Some(alst) = self.gen_attr_list() { format!("<{} {}>{}", self.tag, alst.join(" "), t, self.tag) } else { format!("<{}>{}", self.tag, t, self.tag) }; bldr.line(line); } + + /// # Panics + /// If the `extra-validation` feature is enabled, panic if the tag name is + /// not a known "void" element. pub fn add_scope(self, bldr: &mut sidoc::Builder) { + #[cfg(feature = "extra-validation")] + assert!(!VOID_ELEMENTS.contains(self.tag)); + let line = if let Some(alst) = self.gen_attr_list() { format!("<{} {}>", self.tag, alst.join(" ")) } else { format!("<{}>", self.tag) }; ADDED tests/gentests.rs Index: tests/gentests.rs ================================================================== --- /dev/null +++ tests/gentests.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use sidoc::{Builder, RenderContext}; + +use sidoc_html5::Element; + +#[test] +fn data_attr() { + let mut bldr = Builder::new(); + + let e = Element::new("br").data_attr("hello", "world"); + e.add_empty(&mut bldr); + + let mut r = RenderContext::new(); + let doc = bldr.build().unwrap(); + r.doc("root", Arc::new(doc)); + let buf = r.render("root").unwrap(); + + assert_eq!(buf, "
\n"); +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : ADDED www/changelog.md Index: www/changelog.md ================================================================== --- /dev/null +++ www/changelog.md @@ -0,0 +1,22 @@ +# Change Log + +⚠️ indicates a breaking change. + +## [Unreleased] + +[Details](/vdiff?from=sidoc-html5-0.1.0&to=trunk) + +### Added + +- Add `Element::map_opt()`. + +### Changed + +### Removed + +--- + +## [0.1.0] - 2020-09-12 + +Initial release + ADDED www/index.md Index: www/index.md ================================================================== --- /dev/null +++ www/index.md @@ -0,0 +1,17 @@ +# sidoc-html5 + +HTML5 generation wrapper for sidoc. + + +## Change log + +The details of changes can always be found in the timeline, but for a +high-level view of changes between released versions there's a manually +maintained [Change Log](./changelog.md). + + +## Project Status + +sidoc-html5 is passively maintained. It will receive updates and fixes and +needed, but is currently considered to be feature complete. +