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 :