sidoc_html5/
owned.rs

1//! A variant of `Element` that doesn't work with references.
2
3enum AttrType {
4  KV(String, String),
5  Bool(String),
6  Data(String, String),
7  BoolData(String)
8}
9
10pub struct Element {
11  tag: String,
12  classes: Vec<String>,
13  alst: Vec<AttrType>
14}
15
16impl Element {
17  #[must_use]
18  #[allow(clippy::needless_pass_by_value)]
19  pub fn new(tag: impl ToString) -> Self {
20    Self {
21      tag: tag.to_string(),
22      classes: Vec::new(),
23      alst: Vec::new()
24    }
25  }
26
27  #[must_use]
28  #[allow(clippy::needless_pass_by_value)]
29  pub fn class(mut self, cls: impl ToString) -> Self {
30    self.class_r(cls);
31    self
32  }
33
34  #[allow(clippy::needless_pass_by_value)]
35  pub fn class_r(&mut self, cls: impl ToString) -> &mut Self {
36    self.classes.push(cls.to_string());
37    self
38  }
39
40  #[must_use]
41  #[allow(clippy::needless_pass_by_value)]
42  pub fn flag(mut self, key: impl ToString) -> Self {
43    self.flag_r(key);
44    self
45  }
46
47  #[allow(clippy::needless_pass_by_value)]
48  pub fn flag_r(&mut self, key: impl ToString) -> &mut Self {
49    self.alst.push(AttrType::Bool(key.to_string()));
50    self
51  }
52
53  #[must_use]
54  pub fn attr(mut self, key: impl ToString, value: impl AsRef<str>) -> Self {
55    self.attr_r(key, value);
56    self
57  }
58
59  #[allow(clippy::needless_pass_by_value)]
60  pub fn attr_r(
61    &mut self,
62    key: impl ToString,
63    value: impl AsRef<str>
64  ) -> &mut Self {
65    let key = key.to_string();
66    debug_assert!(
67      key != "class",
68      "Use the dedicated .class() method to add classes to elements"
69    );
70    self.alst.push(AttrType::KV(
71      key,
72      html_escape::encode_double_quoted_attribute(value.as_ref()).to_string()
73    ));
74    self
75  }
76
77  #[must_use]
78  #[allow(clippy::needless_pass_by_value)]
79  pub fn data_attr(
80    mut self,
81    key: impl ToString,
82    value: impl AsRef<str>
83  ) -> Self {
84    self.data_attr_r(key, value);
85    self
86  }
87
88  #[allow(clippy::needless_pass_by_value)]
89  pub fn data_attr_r(
90    &mut self,
91    key: impl ToString,
92    value: impl AsRef<str>
93  ) -> &mut Self {
94    let key = key.to_string();
95    self.alst.push(AttrType::Data(
96      key,
97      html_escape::encode_double_quoted_attribute(value.as_ref()).to_string()
98    ));
99    self
100  }
101
102  #[must_use]
103  #[allow(clippy::needless_pass_by_value)]
104  pub fn data_flag(mut self, key: impl ToString) -> Self {
105    self.data_flag_r(key);
106    self
107  }
108
109  #[allow(clippy::needless_pass_by_value)]
110  pub fn data_flag_r(&mut self, key: impl ToString) -> &mut Self {
111    self.alst.push(AttrType::BoolData(key.to_string()));
112    self
113  }
114}
115
116impl Element {
117  #[must_use]
118  #[allow(clippy::needless_pass_by_value)]
119  pub fn raw_attr(mut self, key: impl ToString, value: impl ToString) -> Self {
120    self.raw_attr_r(key, value);
121    self
122  }
123
124  #[allow(clippy::needless_pass_by_value)]
125  pub fn raw_attr_r(
126    &mut self,
127    key: impl ToString,
128    value: impl ToString
129  ) -> &mut Self {
130    let key = key.to_string();
131    debug_assert!(
132      key != "class",
133      "Use the dedicated .class() method to add classes to elements"
134    );
135    self.alst.push(AttrType::KV(key, value.to_string()));
136    self
137  }
138}
139
140impl Element {
141  /// Conditionally call a closure to map `self` if a predicate is true.
142  ///
143  /// ```
144  /// use sidoc_html5::owned::Element;
145  /// let someval = 42;
146  /// Element::new("body")
147  ///   .map_if(someval == 42, |obj| obj.flag("selected"));
148  /// ```
149  #[must_use]
150  pub fn map_if<F>(self, flag: bool, f: F) -> Self
151  where
152    F: FnOnce(Self) -> Self
153  {
154    if flag {
155      f(self)
156    } else {
157      self
158    }
159  }
160
161  #[must_use]
162  pub fn map_opt<T, F>(self, opt: Option<T>, f: F) -> Self
163  where
164    F: FnOnce(Self, T) -> Self
165  {
166    if let Some(o) = opt {
167      f(self, o)
168    } else {
169      self
170    }
171  }
172
173  /// Conditionally call a closure to modify `self`, in-place, if a predicate
174  /// is true.
175  ///
176  /// ```
177  /// use sidoc_html5::owned::Element;
178  /// let someval = 42;
179  /// let mut e = Element::new("body");
180  /// e.mod_if(someval == 42, |obj| {
181  ///   obj.flag_r("selected");
182  /// });
183  /// ```
184  pub fn mod_if<F>(&mut self, flag: bool, f: F) -> &mut Self
185  where
186    F: FnOnce(&mut Self)
187  {
188    if flag {
189      f(self);
190    }
191    self
192  }
193
194  pub fn mod_opt<T, F>(&mut self, opt: Option<T>, f: F) -> &mut Self
195  where
196    F: FnOnce(&mut Self, T)
197  {
198    if let Some(o) = opt {
199      f(self, o);
200    }
201    self
202  }
203}
204
205impl Element {
206  /// Generate a vector of strings representing each attribute.
207  fn gen_attr_list(&self) -> Option<Vec<String>> {
208    if self.alst.is_empty() && self.classes.is_empty() {
209      None
210    } else {
211      let mut ret = Vec::new();
212
213      if !self.classes.is_empty() {
214        ret.push(format!(r#"class="{}""#, self.classes.join(" ")));
215      }
216
217      let it = self.alst.iter().map(|a| match a {
218        AttrType::KV(k, v) => {
219          format!(r#"{k}="{v}""#)
220        }
221        AttrType::Bool(a) => a.clone(),
222        AttrType::Data(k, v) => {
223          format!(r#"data-{k}="{v}""#)
224        }
225        AttrType::BoolData(a) => {
226          format!("data-{a}")
227        }
228      });
229
230      ret.extend(it);
231
232      Some(ret)
233    }
234  }
235}
236
237impl Element {
238  /// Call a closure for adding child nodes.
239  ///
240  /// ```
241  /// use sidoc_html5::owned::Element;
242  /// let mut bldr = sidoc::Builder::new();
243  /// Element::new("div")
244  ///   .sub(&mut bldr, |bldr| {
245  ///     Element::new("br")
246  ///       .add_empty(bldr);
247  /// });
248  /// ```
249  pub fn sub<F>(self, bldr: &mut sidoc::Builder, f: F)
250  where
251    F: FnOnce(&mut sidoc::Builder)
252  {
253    if let Some(lst) = self.gen_attr_list() {
254      bldr.scope(
255        format!("<{} {}>", self.tag, lst.join(" ")),
256        Some(format!("</{}>", self.tag))
257      );
258    } else {
259      let stag = format!("<{}>", self.tag);
260      let etag = format!("</{}>", self.tag);
261      bldr.scope(stag, Some(etag));
262    }
263
264    f(bldr);
265
266    bldr.exit();
267  }
268}
269
270
271impl Element {
272  /// Consume `self` and add a empty tag representation of element to a sidoc
273  /// builder.
274  ///
275  /// An empty/void tag comes is one which does not have a closing tag:
276  /// `<tagname foo="bar">`.
277  #[inline]
278  pub fn add_empty(self, bldr: &mut sidoc::Builder) {
279    let line = if let Some(alst) = self.gen_attr_list() {
280      format!("<{} {}>", self.tag, alst.join(" "))
281    } else {
282      format!("<{}>", self.tag)
283    };
284
285    bldr.line(line);
286  }
287
288  /// Consume `self` and add a tag containing text content between the opening
289  /// and closing tag to the supplied sidoc builder.
290  ///
291  /// The supplied text is escaped as needed.
292  ///
293  /// ```
294  /// use std::sync::Arc;
295  /// use sidoc_html5::owned::Element;
296  /// let mut bldr = sidoc::Builder::new();
297  /// let elem = Element::new("textarea")
298  ///   .raw_attr("rows", 8)
299  ///   .raw_attr("cols", 32)
300  ///   .add_content("This is the text content", &mut bldr);
301  ///
302  /// let mut r = sidoc::RenderContext::new();
303  /// let doc = bldr.build().unwrap();
304  /// r.doc("root", Arc::new(doc));
305  /// let buf = r.render("root").unwrap();
306  ///
307  /// assert_eq!(buf, "<textarea rows=\"8\" cols=\"32\">This is the text content</textarea>\n");
308  /// ```
309  ///
310  /// # Panics
311  /// If the `extra-validation` feature is enabled, panic if the tag name is
312  /// not a known "void" element.
313  #[inline]
314  pub fn add_content(self, text: &str, bldr: &mut sidoc::Builder) {
315    let line = if let Some(alst) = self.gen_attr_list() {
316      format!(
317        "<{} {}>{}</{}>",
318        self.tag,
319        alst.join(" "),
320        html_escape::encode_text(text),
321        self.tag
322      )
323    } else {
324      format!(
325        "<{}>{}</{}>",
326        self.tag,
327        html_escape::encode_text(text),
328        self.tag
329      )
330    };
331    bldr.line(line);
332  }
333
334  /// Consume `self` and add a tag containing text content between the opening
335  /// and closing tag to the supplied sidoc builder.
336  ///
337  /// The supplied text is not escaped.
338  ///
339  /// ```
340  /// use std::sync::Arc;
341  /// use sidoc_html5::Element;
342  /// let mut bldr = sidoc::Builder::new();
343  /// let elem = Element::new("button")
344  ///   .add_raw_content("Do Stuff", &mut bldr);
345  ///
346  /// let mut r = sidoc::RenderContext::new();
347  /// let doc = bldr.build().unwrap();
348  /// r.doc("root", Arc::new(doc));
349  /// let buf = r.render("root").unwrap();
350  ///
351  /// assert_eq!(buf, "<button>Do Stuff</button>\n");
352  /// ```
353  #[inline]
354  pub fn add_raw_content(self, text: &str, bldr: &mut sidoc::Builder) {
355    let line = if let Some(alst) = self.gen_attr_list() {
356      format!("<{} {}>{}</{}>", self.tag, alst.join(" "), text, self.tag)
357    } else {
358      format!("<{}>{}</{}>", self.tag, text, self.tag)
359    };
360    bldr.line(line);
361  }
362
363  pub fn add_scope(self, bldr: &mut sidoc::Builder) {
364    let line = if let Some(alst) = self.gen_attr_list() {
365      format!("<{} {}>", self.tag, alst.join(" "))
366    } else {
367      format!("<{}>", self.tag)
368    };
369    bldr.scope(line, Some(format!("</{}>", self.tag)));
370  }
371
372  /// ```
373  /// use std::sync::Arc;
374  /// use sidoc_html5::owned::Element;
375  ///
376  /// let mut bldr = sidoc::Builder::new();
377  /// let elem = Element::new("div")
378  ///   .scope(&mut bldr, |bldr| {
379  ///     let elem = Element::new("button")
380  ///       .add_raw_content("Do Stuff", bldr);
381  ///   });
382  ///
383  /// let mut r = sidoc::RenderContext::new();
384  /// let doc = bldr.build().unwrap();
385  /// r.doc("root", Arc::new(doc));
386  /// let buf = r.render("root").unwrap();
387  ///
388  /// // Output should be:
389  /// // <div>
390  /// //   <button>Do Stuff</button>
391  /// // </div>
392  /// assert_eq!(buf, "<div>\n  <button>Do Stuff</button>\n</div>\n");
393  /// ```
394  pub fn scope<F>(self, bldr: &mut sidoc::Builder, f: F)
395  where
396    F: FnOnce(&mut sidoc::Builder)
397  {
398    let line = if let Some(alst) = self.gen_attr_list() {
399      format!("<{} {}>", self.tag, alst.join(" "))
400    } else {
401      format!("<{}>", self.tag)
402    };
403    bldr.scope(line, Some(format!("</{}>", self.tag)));
404    f(bldr);
405    bldr.exit();
406  }
407}
408
409// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :