sidoc_html5/
lib.rs

1pub mod owned;
2
3use std::borrow::Cow;
4
5#[cfg(feature = "extra-validation")]
6use std::collections::HashSet;
7
8#[cfg(feature = "extra-validation")]
9lazy_static::lazy_static! {
10  static ref VOID_ELEMENTS: HashSet<&'static str> = {
11    HashSet::from([
12      "area", "base", "br", "col", "embed", "hr", "img", "input", "link",
13      "meta", "param", "source", "track", "wbr"
14    ])
15  };
16}
17
18enum AttrType<'a> {
19  KV(&'a str, String),
20  Bool(&'a str),
21  Data(&'a str, String),
22  BoolData(&'a str)
23}
24
25pub struct Element<'a> {
26  tag: &'a str,
27  classes: Vec<&'a str>,
28  alst: Vec<AttrType<'a>>
29}
30
31impl<'a> Element<'a> {
32  #[must_use]
33  pub const fn new(tag: &'a str) -> Self {
34    Element {
35      tag,
36      classes: Vec::new(),
37      alst: Vec::new()
38    }
39  }
40
41  /// Assign a class to this element.
42  ///
43  /// ```
44  /// use sidoc_html5::Element;
45  /// let element = Element::new("p")
46  ///   .class("warning");
47  /// ```
48  #[inline]
49  #[must_use]
50  pub fn class(mut self, cls: &'a str) -> Self {
51    self.classes.push(cls);
52    self
53  }
54
55  /// Assign a class to this element in-place.
56  ///
57  /// ```
58  /// use sidoc_html5::Element;
59  /// let mut element = Element::new("p");
60  /// element.class_r("warning");
61  /// ```
62  #[inline]
63  pub fn class_r(&mut self, cls: &'a str) -> &mut Self {
64    self.classes.push(cls);
65    self
66  }
67
68  /// Assign a "flag attribute" to the element.
69  ///
70  /// Flag attributes do not have (explicit) values, and are used to mark
71  /// elements as `selected` or `checked` and such.
72  ///
73  /// ```
74  /// use sidoc_html5::Element;
75  /// let element = Element::new("input")
76  ///   .raw_attr("type", "checkbox")
77  ///   .flag("checked");
78  /// ```
79  #[inline]
80  #[must_use]
81  pub fn flag(mut self, key: &'a str) -> Self {
82    self.alst.push(AttrType::Bool(key));
83    self
84  }
85
86  #[inline]
87  pub fn flag_r(&mut self, key: &'a str) -> &mut Self {
88    self.alst.push(AttrType::Bool(key));
89    self
90  }
91
92  /// Conditionally add a flag attribute.
93  ///
94  /// Flag attributes do not have (explicit) values, and are used to mark
95  /// elements as `selected` or `checked` and such.
96  ///
97  /// ```
98  /// use sidoc_html5::Element;
99  /// for id in &[1, 2, 3] {
100  ///   let element = Element::new("option")
101  ///     .flag_if(*id == 3, "selected");
102  /// }
103  /// ```
104  #[inline]
105  /*
106  #[deprecated(since = "0.1.2", note = "Use .mod_if() with .flag() instead.")]
107  */
108  #[must_use]
109  pub fn flag_if(mut self, f: bool, key: &'a str) -> Self {
110    if f {
111      self.alst.push(AttrType::Bool(key));
112    }
113    self
114  }
115
116  /// Add an attribute.
117  ///
118  /// The attribute value is escaped as needed.
119  ///
120  /// Note: If the value is guaranteed not to require escaping then prefer the
121  /// [`Element::raw_attr()`] method instead.
122  ///
123  /// ```
124  /// use sidoc_html5::Element;
125  /// let elem = Element::new("input")
126  ///   .attr("name", "foo");
127  /// ```
128  #[inline]
129  #[must_use]
130  pub fn attr(mut self, key: &'a str, value: impl AsRef<str>) -> Self {
131    debug_assert!(
132      key != "class",
133      "Use the dedicated .class() method to add classes to elements"
134    );
135    self.alst.push(AttrType::KV(
136      key,
137      html_escape::encode_double_quoted_attribute(value.as_ref()).to_string()
138    ));
139    self
140  }
141
142  /// Conditionally add an attribute.
143  ///
144  /// The value is escaped as needed.
145  #[inline]
146  /*
147  #[deprecated(since = "0.1.2", note = "Use .mod_if() with .attr() instead.")]
148  */
149  #[must_use]
150  pub fn attr_if<V>(self, flag: bool, key: &'a str, value: V) -> Self
151  where
152    V: AsRef<str>
153  {
154    debug_assert!(
155      key != "class",
156      "Use the dedicated .class() method to add classes to elements"
157    );
158
159    self.optattr(key, flag.then_some(value))
160  }
161
162  /// Add a `data-`-prefixed attribute.
163  ///
164  /// The attribute value is escaped as needed.
165  ///
166  /// ```
167  /// use sidoc_html5::Element;
168  /// let elem = Element::new("tr")
169  ///   .data_attr("name", "foo");
170  /// ```
171  #[inline]
172  #[must_use]
173  pub fn data_attr(mut self, key: &'a str, value: impl AsRef<str>) -> Self {
174    debug_assert!(
175      key != "class",
176      "Use the dedicated .class() method to add classes to elements"
177    );
178    self.alst.push(AttrType::Data(
179      key,
180      html_escape::encode_double_quoted_attribute(value.as_ref()).to_string()
181    ));
182    self
183  }
184
185  #[inline]
186  pub fn data_attr_r(
187    &mut self,
188    key: &'a str,
189    value: impl AsRef<str>
190  ) -> &mut Self {
191    debug_assert!(
192      key != "class",
193      "Use the dedicated .class() method to add classes to elements"
194    );
195    self.alst.push(AttrType::Data(
196      key,
197      html_escape::encode_double_quoted_attribute(value.as_ref()).to_string()
198    ));
199    self
200  }
201
202
203  #[inline]
204  #[must_use]
205  pub fn data_flag(mut self, key: &'a str) -> Self {
206    self.alst.push(AttrType::BoolData(key));
207    self
208  }
209
210  /// Conditionally add a boolean `data-` attribute.
211  ///
212  /// Add a `data-foo=true` attribute to an element:
213  /// ```
214  /// use sidoc_html5::Element;
215  /// let val = 7;
216  /// let elem = Element::new("p")
217  ///   .data_flag_if(val > 5, "foo");
218  /// ```
219  #[inline]
220  /*
221  #[deprecated(
222    since = "0.1.2",
223    note = "Use .mod_if() with .data_flag() instead."
224  )]
225  */
226  #[must_use]
227  pub fn data_flag_if(mut self, flag: bool, key: &'a str) -> Self {
228    if flag {
229      self.alst.push(AttrType::BoolData(key));
230    }
231    self
232  }
233
234  /// Add an attribute if its optional value is `Some()`; ignore it otherwise.
235  ///
236  /// The value is escaped as needed.
237  ///
238  /// Note: If the value is guaranteed to not needing escaping then prefer the
239  /// [`Element::raw_optattr()`] method instead.
240  ///
241  /// ```
242  /// use sidoc_html5::Element;
243  /// let something = Some("something");
244  /// let nothing: Option<&str> = None;
245  /// let elem = Element::new("form")
246  ///   .optattr("id", something)
247  ///   .optattr("name", nothing);
248  /// ```
249  #[inline]
250  #[must_use]
251  #[allow(clippy::needless_pass_by_value)]
252  pub fn optattr<T>(mut self, key: &'a str, value: Option<T>) -> Self
253  where
254    T: AsRef<str>
255  {
256    if let Some(v) = value.as_ref() {
257      self.alst.push(AttrType::KV(
258        key,
259        html_escape::encode_double_quoted_attribute(v).to_string()
260      ));
261    }
262    self
263  }
264
265  /// If an an optional input value is set, apply a function on the contained
266  /// value to allow it to generate the actual attribute value.  Do nothing
267  /// if the optional value is `None`.
268  ///
269  /// The value returned by the closure is escaped as needed.
270  ///
271  /// ```
272  /// use sidoc_html5::Element;
273  /// let something = Some("something");
274  /// let elem = Element::new("p")
275  ///   .optattr_map("id", something, |v| {
276  ///     // Have a value -- format it and append "-aboo" to it.
277  ///     format!("{}-aboo", v)
278  ///   });
279  /// ```
280  #[inline]
281  /*
282  #[deprecated(since = "0.1.2", note = "Use .map_attr() instead.")]
283  */
284  #[must_use]
285  #[allow(clippy::needless_pass_by_value)]
286  pub fn optattr_map<T, F>(self, key: &'a str, value: Option<T>, f: F) -> Self
287  where
288    F: FnOnce(&T) -> String
289  {
290    self.map_attr(key, value, f)
291  }
292
293  #[inline]
294  #[must_use]
295  #[allow(clippy::needless_pass_by_value)]
296  pub fn map_attr<T, F>(mut self, key: &'a str, value: Option<T>, f: F) -> Self
297  where
298    F: FnOnce(&T) -> String
299  {
300    if let Some(v) = value.as_ref() {
301      let s = f(v);
302      self.alst.push(AttrType::KV(
303        key,
304        html_escape::encode_double_quoted_attribute(&s).to_string()
305      ));
306    }
307    self
308  }
309
310  /// If an an optional input value is set, apply a function on the contained
311  /// value.
312  #[inline]
313  /*
314  #[deprecated(since = "0.1.2", note = "Use .map() instead.")]
315  */
316  #[must_use]
317  pub fn opt_map<T, F>(self, value: Option<&'_ T>, f: F) -> Self
318  where
319    F: FnOnce(Self, &T) -> Self
320  {
321    self.map(value, f)
322  }
323
324  #[must_use]
325  pub fn map<T, F>(self, value: Option<&'_ T>, f: F) -> Self
326  where
327    F: FnOnce(Self, &T) -> Self
328  {
329    if let Some(v) = value.as_ref() {
330      f(self, v)
331    } else {
332      self
333    }
334  }
335
336  /// If an an optional input value is set, apply a function on the contained
337  /// value.
338  ///
339  /// ```
340  /// use sidoc_html5::Element;
341  /// let opt_str = Some("enabled");
342  /// Element::new("body")
343  ///   .map_opt(opt_str, |this, s| {
344  ///     this.flag(s)
345  ///   });
346  /// ```
347  #[inline]
348  #[must_use]
349  pub fn map_opt<T, F>(self, o: Option<T>, f: F) -> Self
350  where
351    F: FnOnce(Self, T) -> Self
352  {
353    match o {
354      Some(t) => f(self, t),
355      None => self
356    }
357  }
358
359  /// Conditionally call a function to add an attribute with a generated value.
360  ///
361  /// ```
362  /// use sidoc_html5::Element;
363  /// let sv = vec!["foo".to_string(), "bar".to_string()];
364  /// Element::new("body")
365  ///   .map_attr_if(!sv.is_empty(), "data-mylist", &sv, |v: &Vec<String>| {
366  ///     v.join(",")
367  ///   });
368  /// ```
369  #[inline]
370  #[must_use]
371  pub fn map_attr_if<T, F>(
372    self,
373    flag: bool,
374    key: &'a str,
375    data: &T,
376    f: F
377  ) -> Self
378  where
379    F: FnOnce(&T) -> String
380  {
381    if flag {
382      self.attr(key, f(data))
383    } else {
384      self
385    }
386  }
387
388
389  /// Conditionally call a closure to modify `self` if a predicate is true.
390  ///
391  /// ```
392  /// use sidoc_html5::Element;
393  /// let someval = 42;
394  /// Element::new("body")
395  ///   .map_if(someval == 42, |obj| obj.flag("selected"));
396  /// ```
397  #[inline]
398  #[must_use]
399  pub fn map_if<F>(self, flag: bool, f: F) -> Self
400  where
401    F: FnOnce(Self) -> Self
402  {
403    if flag {
404      f(self)
405    } else {
406      self
407    }
408  }
409
410  /// Conditionally call a closure to modify `self`, in-place, if a predicate
411  /// is true.
412  ///
413  /// ```
414  /// use sidoc_html5::Element;
415  /// let someval = 42;
416  /// let mut e = Element::new("body");
417  /// e.mod_if(someval == 42, |obj| {
418  ///   obj.flag_r("selected");
419  /// });
420  /// ```
421  #[inline]
422  pub fn mod_if<F>(&mut self, flag: bool, f: F) -> &mut Self
423  where
424    F: FnOnce(&mut Self)
425  {
426    if flag {
427      f(self);
428    }
429    self
430  }
431}
432
433/// Methods that don't transform the input.
434impl<'a> Element<'a> {
435  /// Add an attribute.
436  ///
437  /// The attribute value is not escaped.
438  ///
439  /// ```
440  /// use sidoc_html5::Element;
441  /// let elem = Element::new("form")
442  ///   .raw_attr("id", "foo");
443  /// ```
444  #[inline]
445  #[must_use]
446  #[allow(clippy::needless_pass_by_value)]
447  pub fn raw_attr(mut self, key: &'a str, value: impl ToString) -> Self {
448    debug_assert!(
449      key != "class",
450      "Use the dedicated .class() method to add classes to elements"
451    );
452    self.alst.push(AttrType::KV(key, value.to_string()));
453    self
454  }
455
456  /// Add an attribute if its optional value is `Some()`; ignore it otherwise.
457  ///
458  /// The value is assumed not to require escaping.
459  ///
460  /// ```
461  /// use sidoc_html5::Element;
462  /// let ss = "something".to_string();
463  /// let something = Some(&ss);
464  /// let nothing = None::<&String>;
465  /// let elem = Element::new("form")
466  ///   .raw_optattr("id", something)
467  ///   .raw_optattr("name", nothing);
468  /// ```
469  #[inline]
470  #[must_use]
471  pub fn raw_optattr<T>(mut self, key: &'a str, value: Option<&T>) -> Self
472  where
473    T: ToString
474  {
475    if let Some(v) = value.as_ref() {
476      self.alst.push(AttrType::KV(key, v.to_string()));
477    }
478    self
479  }
480
481  /// Add an attribute if a condition is true.
482  ///
483  /// The attribute value is not escaped.
484  #[inline]
485  #[allow(clippy::needless_pass_by_value)]
486  #[must_use]
487  pub fn raw_attr_if(
488    self,
489    flag: bool,
490    key: &'a str,
491    value: impl ToString
492  ) -> Self {
493    debug_assert!(
494      key != "class",
495      "Use the dedicated .class() method to add classes to elements"
496    );
497    self.raw_optattr(key, flag.then_some(&value))
498  }
499}
500
501impl<'a> Element<'a> {
502  /// Generate a vector of strings representing each attribute.
503  fn gen_attr_list(&self) -> Option<Vec<String>> {
504    if self.alst.is_empty() && self.classes.is_empty() {
505      None
506    } else {
507      let mut ret = Vec::new();
508
509      if !self.classes.is_empty() {
510        ret.push(format!(r#"class="{}""#, self.classes.join(" ")));
511      }
512
513      let it = self.alst.iter().map(|a| match a {
514        AttrType::KV(k, v) => {
515          format!(r#"{k}="{v}""#)
516        }
517        AttrType::Bool(a) => (*a).to_string(),
518        AttrType::Data(k, v) => {
519          format!(r#"data-{k}="{v}""#)
520        }
521        AttrType::BoolData(a) => {
522          format!("data-{a}")
523        }
524      });
525
526      ret.extend(it);
527
528      Some(ret)
529    }
530  }
531}
532
533impl<'a> Element<'a> {
534  /// Call a closure for adding child nodes.
535  ///
536  /// ```
537  /// use sidoc_html5::Element;
538  /// let mut bldr = sidoc::Builder::new();
539  /// Element::new("div")
540  ///   .sub(&mut bldr, |bldr| {
541  ///     Element::new("br")
542  ///       .add_empty(bldr);
543  /// });
544  /// ```
545  ///
546  /// # Panics
547  /// If the `extra-validation` feature is enabled, panic if the tag name is
548  /// not a known "void" element.
549  pub fn sub<F>(self, bldr: &mut sidoc::Builder, f: F)
550  where
551    F: FnOnce(&mut sidoc::Builder)
552  {
553    #[cfg(feature = "extra-validation")]
554    assert!(!VOID_ELEMENTS.contains(self.tag));
555
556    if let Some(lst) = self.gen_attr_list() {
557      bldr.scope(
558        format!("<{} {}>", self.tag, lst.join(" ")),
559        Some(format!("</{}>", self.tag))
560      );
561    } else {
562      let stag = format!("<{}>", self.tag);
563      let etag = format!("</{}>", self.tag);
564      bldr.scope(stag, Some(etag));
565    }
566
567    f(bldr);
568
569    bldr.exit();
570  }
571}
572
573
574/// Methods inserting element into a sidoc context.
575impl<'a> Element<'a> {
576  /// Consume `self` and add a empty tag representation of element to a sidoc
577  /// builder.
578  ///
579  /// An empty/void tag comes is one which does not have a closing tag:
580  /// `<tagname foo="bar">`.
581  ///
582  /// # Panics
583  /// If the `extra-validation` feature is enabled, panic if the tag name is
584  /// not a known "void" element.
585  #[inline]
586  pub fn add_empty(self, bldr: &mut sidoc::Builder) {
587    #[cfg(feature = "extra-validation")]
588    assert!(VOID_ELEMENTS.contains(self.tag));
589
590    let line = if let Some(alst) = self.gen_attr_list() {
591      format!("<{} {}>", self.tag, alst.join(" "))
592    } else {
593      format!("<{}>", self.tag)
594    };
595
596    bldr.line(line);
597  }
598
599  /// Consume `self` and add a tag containing text content between the opening
600  /// and closing tag to the supplied sidoc builder.
601  ///
602  /// The supplied text is escaped as needed.
603  ///
604  /// ```
605  /// use sidoc_html5::Element;
606  /// let mut bldr = sidoc::Builder::new();
607  /// let elem = Element::new("textarea")
608  ///   .raw_attr("rows", 8)
609  ///   .raw_attr("cols", 32)
610  ///   .add_content("This is the text content", &mut bldr);
611  /// ```
612  ///
613  /// The example above should generate:
614  /// `<textarea rows="8" cols="32">This is the text content</textarea>`
615  ///
616  /// # Panics
617  /// If the `extra-validation` feature is enabled, panic if the tag name is
618  /// not a known "void" element.
619  #[inline]
620  pub fn add_content(self, text: &str, bldr: &mut sidoc::Builder) {
621    #[cfg(feature = "extra-validation")]
622    assert!(!VOID_ELEMENTS.contains(self.tag));
623
624    let line = if let Some(alst) = self.gen_attr_list() {
625      format!(
626        "<{} {}>{}</{}>",
627        self.tag,
628        alst.join(" "),
629        html_escape::encode_text(text),
630        self.tag
631      )
632    } else {
633      format!(
634        "<{}>{}</{}>",
635        self.tag,
636        html_escape::encode_text(text),
637        self.tag
638      )
639    };
640    bldr.line(line);
641  }
642
643  /// Consume `self` and add a tag containing text content between the opening
644  /// and closing tag to the supplied sidoc builder.
645  ///
646  /// The supplied text is not escaped.
647  ///
648  /// ```
649  /// use sidoc_html5::Element;
650  /// let mut bldr = sidoc::Builder::new();
651  /// let elem = Element::new("button")
652  ///   .add_raw_content("Do Stuff", &mut bldr);
653  /// ```
654  ///
655  /// The example above should generate:
656  /// `<button>Do Stuff</button>`
657  ///
658  /// # Panics
659  /// If the `extra-validation` feature is enabled, panic if the tag name is
660  /// not a known "void" element.
661  #[inline]
662  pub fn add_raw_content(self, text: &str, bldr: &mut sidoc::Builder) {
663    #[cfg(feature = "extra-validation")]
664    assert!(!VOID_ELEMENTS.contains(self.tag));
665
666    let line = if let Some(alst) = self.gen_attr_list() {
667      format!("<{} {}>{}</{}>", self.tag, alst.join(" "), text, self.tag)
668    } else {
669      format!("<{}>{}</{}>", self.tag, text, self.tag)
670    };
671    bldr.line(line);
672  }
673
674  /// # Panics
675  /// If the `extra-validation` feature is enabled, panic if the tag name is
676  /// not a known "void" element.
677  pub fn add_opt_content<T>(self, text: &Option<T>, bldr: &mut sidoc::Builder)
678  where
679    T: AsRef<str>
680  {
681    #[cfg(feature = "extra-validation")]
682    assert!(!VOID_ELEMENTS.contains(self.tag));
683
684    let t = text
685      .as_ref()
686      .map_or_else(|| Cow::from(""), |t| html_escape::encode_text(t));
687    let line = if let Some(alst) = self.gen_attr_list() {
688      format!("<{} {}>{}</{}>", self.tag, alst.join(" "), t, self.tag)
689    } else {
690      format!("<{}>{}</{}>", self.tag, t, self.tag)
691    };
692    bldr.line(line);
693  }
694
695
696  /// # Panics
697  /// If the `extra-validation` feature is enabled, panic if the tag name is
698  /// not a known "void" element.
699  pub fn add_scope(self, bldr: &mut sidoc::Builder) {
700    #[cfg(feature = "extra-validation")]
701    assert!(!VOID_ELEMENTS.contains(self.tag));
702
703    let line = if let Some(alst) = self.gen_attr_list() {
704      format!("<{} {}>", self.tag, alst.join(" "))
705    } else {
706      format!("<{}>", self.tag)
707    };
708    bldr.scope(line, Some(format!("</{}>", self.tag)));
709  }
710
711  /// Add an `Element` to a new scope in a `sidoc::Builder`, and call a closure
712  /// to allow child nodes to be added within the scope.
713  ///
714  /// ```
715  /// use std::sync::Arc;
716  /// use sidoc_html5::Element;
717  ///
718  /// let mut bldr = sidoc::Builder::new();
719  /// let elem = Element::new("div")
720  ///   .scope(&mut bldr, |bldr| {
721  ///     let elem = Element::new("button")
722  ///       .add_raw_content("Do Stuff", bldr);
723  ///   });
724  ///
725  /// let mut r = sidoc::RenderContext::new();
726  /// let doc = bldr.build().unwrap();
727  /// r.doc("root", Arc::new(doc));
728  /// let buf = r.render("root").unwrap();
729  ///
730  /// // Output should be:
731  /// // <div>
732  /// //   <button>Do Stuff</button>
733  /// // </div>
734  /// assert_eq!(buf, "<div>\n  <button>Do Stuff</button>\n</div>\n");
735  /// ```
736  pub fn scope<F>(self, bldr: &mut sidoc::Builder, f: F)
737  where
738    F: FnOnce(&mut sidoc::Builder)
739  {
740    let line = if let Some(alst) = self.gen_attr_list() {
741      format!("<{} {}>", self.tag, alst.join(" "))
742    } else {
743      format!("<{}>", self.tag)
744    };
745    bldr.scope(line, Some(format!("</{}>", self.tag)));
746    f(bldr);
747    bldr.exit();
748  }
749}
750
751// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :