1use alloc::{
22 boxed::Box,
23 collections::BTreeMap,
24 string::{String, ToString},
25 vec::Vec,
26};
27use core::{fmt, fmt::Write, hash::Hash};
28
29use azul_css::{
30 css::{
31 Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
32 NodeTypeTag,
33 },
34 codegen::format::VecContents,
35 parser2::{CssParseErrorOwned, ErrorLocation},
36 props::{
37 basic::{ColorU, StyleFontFamilyVec},
38 property::CssProperty,
39 style::{
40 NormalizedLinearColorStopVec, NormalizedRadialColorStopVec, StyleBackgroundContentVec,
41 StyleBackgroundPositionVec, StyleBackgroundRepeatVec, StyleBackgroundSizeVec,
42 StyleTransformVec,
43 },
44 },
45 AzString, OptionString, StringVec, U8Vec,
46};
47
48use crate::{
49 dom::{Dom, NodeType, OptionNodeType},
50 styled_dom::StyledDom,
51 window::{AzStringPair, StringPairVec},
52};
53
54pub type SyntaxError = String;
58
59#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub struct XmlTagName {
63 pub inner: AzString,
64}
65
66impl From<AzString> for XmlTagName {
67 fn from(s: AzString) -> Self {
68 Self { inner: s }
69 }
70}
71
72impl From<String> for XmlTagName {
73 fn from(s: String) -> Self {
74 Self { inner: s.into() }
75 }
76}
77
78impl From<&str> for XmlTagName {
79 fn from(s: &str) -> Self {
80 Self { inner: s.into() }
81 }
82}
83
84impl core::ops::Deref for XmlTagName {
85 type Target = AzString;
86 fn deref(&self) -> &Self::Target {
87 &self.inner
88 }
89}
90
91pub type XmlTextContent = OptionString;
93
94#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
96#[repr(C)]
97pub struct XmlAttributeMap {
98 pub inner: StringPairVec,
99}
100
101impl From<StringPairVec> for XmlAttributeMap {
102 fn from(v: StringPairVec) -> Self {
103 Self { inner: v }
104 }
105}
106
107impl core::ops::Deref for XmlAttributeMap {
108 type Target = StringPairVec;
109 fn deref(&self) -> &Self::Target {
110 &self.inner
111 }
112}
113
114impl core::ops::DerefMut for XmlAttributeMap {
115 fn deref_mut(&mut self) -> &mut Self::Target {
116 &mut self.inner
117 }
118}
119
120type ComponentArgumentName = String;
122type ComponentArgumentType = String;
124type ComponentArgumentOrder = usize;
126
127#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
129#[repr(C)]
130pub struct ComponentArgument {
131 pub name: AzString,
132 pub arg_type: AzString,
133}
134
135impl_vec!(
136 ComponentArgument,
137 ComponentArgumentVec,
138 ComponentArgumentVecDestructor,
139 ComponentArgumentVecDestructorType,
140 ComponentArgumentVecSlice,
141 OptionComponentArgument
142);
143impl_option!(
144 ComponentArgument,
145 OptionComponentArgument,
146 copy = false,
147 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
148);
149impl_vec_debug!(ComponentArgument, ComponentArgumentVec);
150impl_vec_partialeq!(ComponentArgument, ComponentArgumentVec);
151impl_vec_eq!(ComponentArgument, ComponentArgumentVec);
152impl_vec_partialord!(ComponentArgument, ComponentArgumentVec);
153impl_vec_ord!(ComponentArgument, ComponentArgumentVec);
154impl_vec_hash!(ComponentArgument, ComponentArgumentVec);
155impl_vec_clone!(
156 ComponentArgument,
157 ComponentArgumentVec,
158 ComponentArgumentVecDestructor
159);
160impl_vec_mut!(ComponentArgument, ComponentArgumentVec);
161
162#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
165pub struct ComponentArguments {
166 pub args: ComponentArgumentVec,
167 pub accepts_text: bool,
168}
169
170type ComponentName = String;
172type CompiledComponent = String;
174
175const DEFAULT_ARGS: [&str; 8] = [
178 "id",
179 "class",
180 "tabindex",
181 "focusable",
182 "accepts_text",
183 "name",
184 "style",
185 "args",
186];
187
188#[allow(non_camel_case_types)]
191#[derive(Debug, Copy, Clone)]
192pub enum c_void {}
193
194#[repr(C)]
196#[derive(Debug, Copy, Clone)]
197pub enum XmlNodeType {
198 Root,
199 Element,
200 PI,
201 Comment,
202 Text,
203}
204
205#[repr(C)]
207#[derive(Debug)]
208pub struct XmlQualifiedName {
209 pub local_name: AzString,
210 pub namespace: OptionString,
211}
212
213#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
215#[repr(C)]
216pub enum ExternalResourceKind {
217 Image,
219 Font,
221 Stylesheet,
223 Script,
225 Icon,
227 Video,
229 Audio,
231 Unknown,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
237#[repr(C)]
238pub struct MimeTypeHint {
239 pub inner: AzString,
240}
241
242impl MimeTypeHint {
243 #[must_use] pub fn new(s: &str) -> Self {
244 Self {
245 inner: AzString::from(s),
246 }
247 }
248
249 #[must_use] pub fn from_extension(ext: &str) -> Self {
250 let mime = match ext.to_lowercase().as_str() {
251 "png" => "image/png",
253 "jpg" | "jpeg" => "image/jpeg",
254 "gif" => "image/gif",
255 "webp" => "image/webp",
256 "svg" => "image/svg+xml",
257 "ico" => "image/x-icon",
258 "bmp" => "image/bmp",
259 "avif" => "image/avif",
260 "ttf" => "font/ttf",
262 "otf" => "font/otf",
263 "woff" => "font/woff",
264 "woff2" => "font/woff2",
265 "eot" => "application/vnd.ms-fontobject",
266 "css" => "text/css",
268 "js" | "mjs" => "application/javascript",
270 "mp4" => "video/mp4",
272 "webm" => "video/webm",
273 "ogg" => "video/ogg",
274 "mp3" => "audio/mpeg",
276 "wav" => "audio/wav",
277 "flac" => "audio/flac",
278 _ => "application/octet-stream",
280 };
281 Self {
282 inner: AzString::from(mime),
283 }
284 }
285}
286
287impl_option!(
288 MimeTypeHint,
289 OptionMimeTypeHint,
290 copy = false,
291 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
292);
293
294#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
296#[repr(C)]
297pub struct ExternalResource {
298 pub url: AzString,
300 pub kind: ExternalResourceKind,
302 pub mime_type: OptionMimeTypeHint,
304 pub source_element: AzString,
306 pub source_attribute: AzString,
308}
309
310impl_option!(
311 ExternalResource,
312 OptionExternalResource,
313 copy = false,
314 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
315);
316
317impl_vec!(
318 ExternalResource,
319 ExternalResourceVec,
320 ExternalResourceVecDestructor,
321 ExternalResourceVecDestructorType,
322 ExternalResourceVecSlice,
323 OptionExternalResource
324);
325impl_vec_mut!(ExternalResource, ExternalResourceVec);
326impl_vec_debug!(ExternalResource, ExternalResourceVec);
327impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
328impl_vec_eq!(ExternalResource, ExternalResourceVec);
329impl_vec_partialord!(ExternalResource, ExternalResourceVec);
330impl_vec_ord!(ExternalResource, ExternalResourceVec);
331impl_vec_hash!(ExternalResource, ExternalResourceVec);
332impl_vec_clone!(
333 ExternalResource,
334 ExternalResourceVec,
335 ExternalResourceVecDestructor
336);
337
338const MAX_XML_NESTING_DEPTH: usize = 512;
347
348const MAX_TYPE_PARSE_DEPTH: usize = 64;
353
354#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
355#[repr(C)]
356pub struct Xml {
357 pub root: XmlNodeChildVec,
358}
359
360impl Xml {
361 #[must_use] pub fn scan_external_resources(&self) -> ExternalResourceVec {
373 let mut resources = Vec::new();
374
375 let mut stack: Vec<(&XmlNodeChild, usize)> = Vec::new();
382 for child in self.root.as_ref() {
383 stack.push((child, 0));
384 }
385 while let Some((child, depth)) = stack.pop() {
386 match child {
387 XmlNodeChild::Text(text) => {
388 Self::extract_css_urls(text.as_str(), &mut resources);
390 }
391 XmlNodeChild::Element(node) => {
392 if depth > MAX_XML_NESTING_DEPTH {
393 continue;
395 }
396 Self::scan_node(node, &mut resources);
397 for c in node.children.as_ref() {
398 stack.push((c, depth + 1));
399 }
400 }
401 }
402 }
403
404 resources.into()
405 }
406
407 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
409 let tag_name = node.node_type.inner.as_str().to_lowercase();
410
411 let get_attr = |name: &str| -> Option<String> {
413 node.attributes
414 .inner
415 .as_ref()
416 .iter()
417 .find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
418 .map(|pair| pair.value.as_str().to_string())
419 };
420
421 match tag_name.as_str() {
422 "img" => {
423 if let Some(src) = get_attr("src") {
424 let mime = Self::guess_mime_from_url(&src, "image");
425 resources.push(ExternalResource {
426 url: AzString::from(src),
427 kind: ExternalResourceKind::Image,
428 mime_type: mime.into(),
429 source_element: AzString::from("img"),
430 source_attribute: AzString::from("src"),
431 });
432 }
433 if let Some(srcset) = get_attr("srcset") {
435 for src in Self::parse_srcset(&srcset) {
436 let mime = Self::guess_mime_from_url(&src, "image");
437 resources.push(ExternalResource {
438 url: AzString::from(src),
439 kind: ExternalResourceKind::Image,
440 mime_type: mime.into(),
441 source_element: AzString::from("img"),
442 source_attribute: AzString::from("srcset"),
443 });
444 }
445 }
446 }
447 "link" => {
448 if let Some(href) = get_attr("href") {
449 let rel = get_attr("rel").unwrap_or_default().to_lowercase();
450 let type_attr = get_attr("type");
451 let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
452
453 let (kind, category) = if rel.contains("stylesheet") {
454 (ExternalResourceKind::Stylesheet, "stylesheet")
455 } else if rel.contains("icon") || rel.contains("apple-touch-icon") {
456 (ExternalResourceKind::Icon, "image")
457 } else if as_attr == "font" {
458 (ExternalResourceKind::Font, "font")
459 } else if as_attr == "script" {
460 (ExternalResourceKind::Script, "script")
461 } else if as_attr == "image" {
462 (ExternalResourceKind::Image, "image")
463 } else {
464 (ExternalResourceKind::Unknown, "")
465 };
466
467 let mime = type_attr
468 .map(|t| MimeTypeHint::new(&t))
469 .or_else(|| Self::guess_mime_from_url(&href, category));
470
471 resources.push(ExternalResource {
472 url: AzString::from(href),
473 kind,
474 mime_type: mime.into(),
475 source_element: AzString::from("link"),
476 source_attribute: AzString::from("href"),
477 });
478 }
479 }
480 "script" => {
481 if let Some(src) = get_attr("src") {
482 let type_attr = get_attr("type");
483 let mime = type_attr
484 .map(|t| MimeTypeHint::new(&t))
485 .or_else(|| Some(MimeTypeHint::new("application/javascript")));
486
487 resources.push(ExternalResource {
488 url: AzString::from(src),
489 kind: ExternalResourceKind::Script,
490 mime_type: mime.into(),
491 source_element: AzString::from("script"),
492 source_attribute: AzString::from("src"),
493 });
494 }
495 }
496 "video" => {
497 if let Some(src) = get_attr("src") {
498 let mime = Self::guess_mime_from_url(&src, "video");
499 resources.push(ExternalResource {
500 url: AzString::from(src),
501 kind: ExternalResourceKind::Video,
502 mime_type: mime.into(),
503 source_element: AzString::from("video"),
504 source_attribute: AzString::from("src"),
505 });
506 }
507 if let Some(poster) = get_attr("poster") {
508 let mime = Self::guess_mime_from_url(&poster, "image");
509 resources.push(ExternalResource {
510 url: AzString::from(poster),
511 kind: ExternalResourceKind::Image,
512 mime_type: mime.into(),
513 source_element: AzString::from("video"),
514 source_attribute: AzString::from("poster"),
515 });
516 }
517 }
518 "audio" => {
519 if let Some(src) = get_attr("src") {
520 let mime = Self::guess_mime_from_url(&src, "audio");
521 resources.push(ExternalResource {
522 url: AzString::from(src),
523 kind: ExternalResourceKind::Audio,
524 mime_type: mime.into(),
525 source_element: AzString::from("audio"),
526 source_attribute: AzString::from("src"),
527 });
528 }
529 }
530 "source" => {
531 if let Some(src) = get_attr("src") {
532 let type_attr = get_attr("type");
533 let kind = if type_attr
535 .as_ref()
536 .is_some_and(|t| t.starts_with("audio"))
537 {
538 ExternalResourceKind::Audio
539 } else {
540 ExternalResourceKind::Video
541 };
542 let mime = type_attr.map(|t| MimeTypeHint::new(&t)).or_else(|| {
543 Self::guess_mime_from_url(
544 &src,
545 if kind == ExternalResourceKind::Audio {
546 "audio"
547 } else {
548 "video"
549 },
550 )
551 });
552
553 resources.push(ExternalResource {
554 url: AzString::from(src),
555 kind,
556 mime_type: mime.into(),
557 source_element: AzString::from("source"),
558 source_attribute: AzString::from("src"),
559 });
560 }
561 if let Some(srcset) = get_attr("srcset") {
563 for src in Self::parse_srcset(&srcset) {
564 let mime = Self::guess_mime_from_url(&src, "image");
565 resources.push(ExternalResource {
566 url: AzString::from(src),
567 kind: ExternalResourceKind::Image,
568 mime_type: mime.into(),
569 source_element: AzString::from("source"),
570 source_attribute: AzString::from("srcset"),
571 });
572 }
573 }
574 }
575 "a" => {
576 if let Some(href) = get_attr("href") {
577 if Self::looks_like_resource(&href) {
579 let mime = Self::guess_mime_from_url(&href, "");
580 resources.push(ExternalResource {
581 url: AzString::from(href),
582 kind: ExternalResourceKind::Unknown,
583 mime_type: mime.into(),
584 source_element: AzString::from("a"),
585 source_attribute: AzString::from("href"),
586 });
587 }
588 }
589 }
590 "virtualized-view" | "embed" | "object" => {
591 let src_attr = if tag_name == "object" { "data" } else { "src" };
592 if let Some(src) = get_attr(src_attr) {
593 resources.push(ExternalResource {
594 url: AzString::from(src),
595 kind: ExternalResourceKind::Unknown,
596 mime_type: OptionMimeTypeHint::None,
597 source_element: AzString::from(tag_name.clone()),
598 source_attribute: AzString::from(src_attr),
599 });
600 }
601 }
602 "style" => {
603 for child in node.children.as_ref() {
605 if let XmlNodeChild::Text(text) = child {
606 Self::extract_css_urls(text.as_str(), resources);
607 }
608 }
609 }
610 _ => {}
611 }
612
613 if let Some(style) = get_attr("style") {
615 Self::extract_css_urls(&style, resources);
616 }
617
618 if let Some(bg) = get_attr("background") {
620 let mime = Self::guess_mime_from_url(&bg, "image");
621 resources.push(ExternalResource {
622 url: AzString::from(bg),
623 kind: ExternalResourceKind::Image,
624 mime_type: mime.into(),
625 source_element: AzString::from(tag_name),
626 source_attribute: AzString::from("background"),
627 });
628 }
629
630 }
632
633 fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
635 let lower = css.to_ascii_lowercase();
646
647 let mut search_from = 0;
649 while let Some(rel) = lower[search_from..].find("url(") {
650 let url_start = search_from + rel;
651 let after = url_start + 4;
652 if lower[..url_start].trim_end().ends_with("@import") {
656 search_from = after;
657 continue;
658 }
659 let after_url = &css[after..];
660 if let Some(url) = Self::extract_url_value(after_url) {
661 let mime = Self::guess_mime_from_url(&url, "");
662 let kind = Self::guess_kind_from_url(&url);
663 resources.push(ExternalResource {
664 url: AzString::from(url),
665 kind,
666 mime_type: mime.into(),
667 source_element: AzString::from("style"),
668 source_attribute: AzString::from("url()"),
669 });
670 }
671 search_from = after;
672 }
673
674 let mut search_from = 0;
676 while let Some(rel) = lower[search_from..].find("@import") {
677 let after = search_from + rel + 7;
678 let after_import = &css[after..];
679 let trimmed = after_import.trim_start();
680
681 let import_url = if trimmed.get(..4).is_some_and(|p| p.eq_ignore_ascii_case("url(")) {
685 Self::extract_url_value(&trimmed[4..])
686 } else {
687 Self::extract_quoted_string(trimmed)
688 };
689
690 if let Some(url) = import_url {
691 resources.push(ExternalResource {
692 url: AzString::from(url),
693 kind: ExternalResourceKind::Stylesheet,
694 mime_type: Some(MimeTypeHint::new("text/css")).into(),
695 source_element: AzString::from("style"),
696 source_attribute: AzString::from("@import"),
697 });
698 }
699
700 search_from = after;
701 }
702 }
703
704 fn extract_url_value(s: &str) -> Option<String> {
706 let trimmed = s.trim_start();
707 if trimmed.starts_with('"') {
708 Self::extract_quoted_string(trimmed)
709 } else if let Some(rest) = trimmed.strip_prefix('\'') {
710 let end = rest.find('\'')?;
711 Some(rest[..end].to_string())
712 } else {
713 let end = trimmed.find(')')?;
714 Some(trimmed[..end].trim().to_string())
715 }
716 }
717
718 fn extract_quoted_string(s: &str) -> Option<String> {
720 if let Some(rest) = s.strip_prefix('"') {
721 let end = rest.find('"')?;
722 Some(rest[..end].to_string())
723 } else if let Some(rest) = s.strip_prefix('\'') {
724 let end = rest.find('\'')?;
725 Some(rest[..end].to_string())
726 } else {
727 None
728 }
729 }
730
731 fn parse_srcset(srcset: &str) -> Vec<String> {
733 srcset
734 .split(',')
735 .filter_map(|entry| {
736 let trimmed = entry.trim();
737 trimmed.split_whitespace().next().map(alloc::string::ToString::to_string)
739 })
740 .filter(|url| !url.is_empty())
741 .collect()
742 }
743
744 fn looks_like_resource(url: &str) -> bool {
746 let lower = url.to_lowercase();
747 let resource_exts = [
749 ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp", ".ttf", ".otf",
750 ".woff", ".woff2", ".eot", ".css", ".js", ".mp4", ".webm", ".ogg", ".mp3", ".wav",
751 ".pdf", ".zip", ".tar", ".gz",
752 ];
753 resource_exts.iter().any(|ext| lower.ends_with(ext))
754 }
755
756 #[allow(clippy::case_sensitive_file_extension_comparisons)]
760 fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
761 let lower = url.to_lowercase();
762 let path = lower.split('?').next().unwrap_or(&lower);
764 if path.ends_with(".png")
765 || path.ends_with(".jpg")
766 || path.ends_with(".jpeg")
767 || path.ends_with(".gif")
768 || path.ends_with(".webp")
769 || path.ends_with(".svg")
770 || path.ends_with(".bmp")
771 || path.ends_with(".avif")
772 {
773 ExternalResourceKind::Image
774 } else if path.ends_with(".ttf")
775 || path.ends_with(".otf")
776 || path.ends_with(".woff")
777 || path.ends_with(".woff2")
778 || path.ends_with(".eot")
779 {
780 ExternalResourceKind::Font
781 } else if path.ends_with(".css") {
782 ExternalResourceKind::Stylesheet
783 } else if path.ends_with(".js") || path.ends_with(".mjs") {
784 ExternalResourceKind::Script
785 } else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
786 ExternalResourceKind::Video
787 } else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
788 ExternalResourceKind::Audio
789 } else if path.ends_with(".ico") {
790 ExternalResourceKind::Icon
791 } else {
792 ExternalResourceKind::Unknown
793 }
794 }
795
796 fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
798 let lower = url.to_lowercase();
799 let ext = lower.rsplit('.').next()?;
801 let ext = ext.split('?').next()?;
803
804 let valid_exts = [
806 "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif", "ttf", "otf", "woff",
807 "woff2", "eot", "css", "js", "mjs", "mp4", "webm", "ogg", "mp3", "wav", "flac",
808 ];
809
810 if valid_exts.contains(&ext) {
811 Some(MimeTypeHint::from_extension(ext))
812 } else if !category.is_empty() {
813 match category {
815 "image" => Some(MimeTypeHint::new("image/*")),
816 "font" => Some(MimeTypeHint::new("font/*")),
817 "stylesheet" => Some(MimeTypeHint::new("text/css")),
818 "script" => Some(MimeTypeHint::new("application/javascript")),
819 "video" => Some(MimeTypeHint::new("video/*")),
820 "audio" => Some(MimeTypeHint::new("audio/*")),
821 _ => None,
822 }
823 } else {
824 None
825 }
826 }
827}
828
829#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
830#[repr(C)]
831pub struct NonXmlCharError {
832 pub ch: u32, pub pos: XmlTextPos,
834}
835
836#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
837#[repr(C)]
838pub struct InvalidCharError {
839 pub expected: u8,
840 pub got: u8,
841 pub pos: XmlTextPos,
842}
843
844#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
845#[repr(C)]
846pub struct InvalidCharMultipleError {
847 pub expected: u8,
848 pub got: U8Vec,
849 pub pos: XmlTextPos,
850}
851
852#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
853#[repr(C)]
854pub struct InvalidQuoteError {
855 pub got: u8,
856 pub pos: XmlTextPos,
857}
858
859#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
860#[repr(C)]
861pub struct InvalidSpaceError {
862 pub got: u8,
863 pub pos: XmlTextPos,
864}
865
866#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
867#[repr(C)]
868pub struct InvalidStringError {
869 pub got: AzString,
870 pub pos: XmlTextPos,
871}
872
873#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
874#[repr(C, u8)]
875pub enum XmlStreamError {
876 UnexpectedEndOfStream,
877 InvalidName,
878 NonXmlChar(NonXmlCharError),
879 InvalidChar(InvalidCharError),
880 InvalidCharMultiple(InvalidCharMultipleError),
881 InvalidQuote(InvalidQuoteError),
882 InvalidSpace(InvalidSpaceError),
883 InvalidString(InvalidStringError),
884 InvalidReference,
885 InvalidExternalID,
886 InvalidCommentData,
887 InvalidCommentEnd,
888 InvalidCharacterData,
889}
890
891impl fmt::Display for XmlStreamError {
892 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
893 use self::XmlStreamError::{UnexpectedEndOfStream, InvalidName, NonXmlChar, InvalidChar, InvalidCharMultiple, InvalidQuote, InvalidSpace, InvalidString, InvalidReference, InvalidExternalID, InvalidCommentData, InvalidCommentEnd, InvalidCharacterData};
894 match self {
895 UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
896 InvalidName => write!(f, "Invalid name"),
897 NonXmlChar(nx) => write!(
898 f,
899 "Non-XML character: {:?} at {}",
900 core::char::from_u32(nx.ch),
901 nx.pos
902 ),
903 InvalidChar(ic) => write!(
904 f,
905 "Invalid character: expected: {}, got: {} at {}",
906 ic.expected as char, ic.got as char, ic.pos
907 ),
908 InvalidCharMultiple(imc) => write!(
909 f,
910 "Multiple invalid characters: expected: {}, got: {:?} at {}",
911 imc.expected,
912 imc.got.as_ref(),
913 imc.pos
914 ),
915 InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
916 InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
917 InvalidString(ise) => write!(
918 f,
919 "Invalid string: got \"{}\" at {}",
920 ise.got.as_str(),
921 ise.pos
922 ),
923 InvalidReference => write!(f, "Invalid reference"),
924 InvalidExternalID => write!(f, "Invalid external ID"),
925 InvalidCommentData => write!(f, "Invalid comment data"),
926 InvalidCommentEnd => write!(f, "Invalid comment end"),
927 InvalidCharacterData => write!(f, "Invalid character data"),
928 }
929 }
930}
931
932#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Ord, Hash, Eq)]
933#[repr(C)]
934pub struct XmlTextPos {
935 pub row: u32,
936 pub col: u32,
937}
938
939impl fmt::Display for XmlTextPos {
940 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
941 write!(f, "line {}:{}", self.row, self.col)
942 }
943}
944
945#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
946#[repr(C)]
947pub struct XmlTextError {
948 pub stream_error: XmlStreamError,
949 pub pos: XmlTextPos,
950}
951
952#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
953#[repr(C, u8)]
954pub enum XmlParseError {
955 InvalidDeclaration(XmlTextError),
956 InvalidComment(XmlTextError),
957 InvalidPI(XmlTextError),
958 InvalidDoctype(XmlTextError),
959 InvalidEntity(XmlTextError),
960 InvalidElement(XmlTextError),
961 InvalidAttribute(XmlTextError),
962 InvalidCdata(XmlTextError),
963 InvalidCharData(XmlTextError),
964 UnknownToken(XmlTextPos),
965}
966
967impl fmt::Display for XmlParseError {
968 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
969 use self::XmlParseError::{InvalidDeclaration, InvalidComment, InvalidPI, InvalidDoctype, InvalidEntity, InvalidElement, InvalidAttribute, InvalidCdata, InvalidCharData, UnknownToken};
970 match self {
971 InvalidDeclaration(e) => {
972 write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
973 }
974 InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
975 InvalidPI(e) => write!(
976 f,
977 "Invalid processing instruction: {} at {}",
978 e.stream_error, e.pos
979 ),
980 InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
981 InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
982 InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
983 InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
984 InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
985 InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
986 UnknownToken(e) => write!(f, "Unknown token at {e}"),
987 }
988 }
989}
990
991impl_result!(
992 Xml,
993 XmlError,
994 ResultXmlXmlError,
995 copy = false,
996 [Debug, PartialEq, Eq, PartialOrd, Clone]
997);
998
999#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1000#[repr(C)]
1001pub struct DuplicatedNamespaceError {
1002 pub ns: AzString,
1003 pub pos: XmlTextPos,
1004}
1005
1006#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1007#[repr(C)]
1008pub struct UnknownNamespaceError {
1009 pub ns: AzString,
1010 pub pos: XmlTextPos,
1011}
1012
1013#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1014#[repr(C)]
1015pub struct UnexpectedCloseTagError {
1016 pub expected: AzString,
1017 pub actual: AzString,
1018 pub pos: XmlTextPos,
1019}
1020
1021#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1022#[repr(C)]
1023pub struct UnknownEntityReferenceError {
1024 pub entity: AzString,
1025 pub pos: XmlTextPos,
1026}
1027
1028#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1029#[repr(C)]
1030pub struct DuplicatedAttributeError {
1031 pub attribute: AzString,
1032 pub pos: XmlTextPos,
1033}
1034
1035#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1037#[repr(C)]
1038pub struct MalformedHierarchyError {
1039 pub expected: AzString,
1041 pub got: AzString,
1043}
1044
1045#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1046#[repr(C, u8)]
1047pub enum XmlError {
1048 NoParserAvailable,
1049 InvalidXmlPrefixUri(XmlTextPos),
1050 UnexpectedXmlUri(XmlTextPos),
1051 UnexpectedXmlnsUri(XmlTextPos),
1052 InvalidElementNamePrefix(XmlTextPos),
1053 DuplicatedNamespace(DuplicatedNamespaceError),
1054 UnknownNamespace(UnknownNamespaceError),
1055 UnexpectedCloseTag(UnexpectedCloseTagError),
1056 UnexpectedEntityCloseTag(XmlTextPos),
1057 UnknownEntityReference(UnknownEntityReferenceError),
1058 MalformedEntityReference(XmlTextPos),
1059 EntityReferenceLoop(XmlTextPos),
1060 InvalidAttributeValue(XmlTextPos),
1061 DuplicatedAttribute(DuplicatedAttributeError),
1062 NoRootNode,
1063 SizeLimit,
1064 DtdDetected,
1065 MalformedHierarchy(MalformedHierarchyError),
1067 ParserError(XmlParseError),
1068 UnclosedRootNode,
1069 UnexpectedDeclaration(XmlTextPos),
1070 NodesLimitReached,
1071 AttributesLimitReached,
1072 NamespacesLimitReached,
1073 InvalidName(XmlTextPos),
1074 NonXmlChar(XmlTextPos),
1075 InvalidChar(XmlTextPos),
1076 InvalidChar2(XmlTextPos),
1077 InvalidString(XmlTextPos),
1078 InvalidExternalID(XmlTextPos),
1079 InvalidComment(XmlTextPos),
1080 InvalidCharacterData(XmlTextPos),
1081 UnknownToken(XmlTextPos),
1082 UnexpectedEndOfStream,
1083}
1084
1085impl fmt::Display for XmlError {
1086 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1087 use self::XmlError::{NoParserAvailable, InvalidXmlPrefixUri, UnexpectedXmlUri, UnexpectedXmlnsUri, InvalidElementNamePrefix, DuplicatedNamespace, UnknownNamespace, UnexpectedCloseTag, UnexpectedEntityCloseTag, UnknownEntityReference, MalformedEntityReference, EntityReferenceLoop, InvalidAttributeValue, DuplicatedAttribute, NoRootNode, SizeLimit, DtdDetected, MalformedHierarchy, ParserError, UnclosedRootNode, UnexpectedDeclaration, NodesLimitReached, AttributesLimitReached, NamespacesLimitReached, InvalidName, NonXmlChar, InvalidChar, InvalidChar2, InvalidString, InvalidExternalID, InvalidComment, InvalidCharacterData, UnknownToken, UnexpectedEndOfStream};
1088 match self {
1089 NoParserAvailable => write!(
1090 f,
1091 "Library was compiled without XML parser (XML parser not available)"
1092 ),
1093 InvalidXmlPrefixUri(pos) => {
1094 write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
1095 }
1096 UnexpectedXmlUri(pos) => {
1097 write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
1098 }
1099 UnexpectedXmlnsUri(pos) => write!(
1100 f,
1101 "Unexpected XML namespace URI at line {}:{}",
1102 pos.row, pos.col
1103 ),
1104 InvalidElementNamePrefix(pos) => write!(
1105 f,
1106 "Invalid element name prefix at line {}:{}",
1107 pos.row, pos.col
1108 ),
1109 DuplicatedNamespace(ns) => write!(
1110 f,
1111 "Duplicated namespace: \"{}\" at {}",
1112 ns.ns.as_str(),
1113 ns.pos
1114 ),
1115 UnknownNamespace(uns) => write!(
1116 f,
1117 "Unknown namespace: \"{}\" at {}",
1118 uns.ns.as_str(),
1119 uns.pos
1120 ),
1121 UnexpectedCloseTag(ct) => write!(
1122 f,
1123 "Unexpected close tag: expected \"{}\", got \"{}\" at {}",
1124 ct.expected.as_str(),
1125 ct.actual.as_str(),
1126 ct.pos
1127 ),
1128 UnexpectedEntityCloseTag(pos) => write!(
1129 f,
1130 "Unexpected entity close tag at line {}:{}",
1131 pos.row, pos.col
1132 ),
1133 UnknownEntityReference(uer) => write!(
1134 f,
1135 "Unexpected entity reference: \"{}\" at {}",
1136 uer.entity, uer.pos
1137 ),
1138 MalformedEntityReference(pos) => write!(
1139 f,
1140 "Malformed entity reference at line {}:{}",
1141 pos.row, pos.col
1142 ),
1143 EntityReferenceLoop(pos) => write!(
1144 f,
1145 "Entity reference loop (recursive entity reference) at line {}:{}",
1146 pos.row, pos.col
1147 ),
1148 InvalidAttributeValue(pos) => {
1149 write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
1150 }
1151 DuplicatedAttribute(ae) => write!(
1152 f,
1153 "Duplicated attribute \"{}\" at line {}:{}",
1154 ae.attribute.as_str(),
1155 ae.pos.row,
1156 ae.pos.col
1157 ),
1158 NoRootNode => write!(f, "No root node found"),
1159 SizeLimit => write!(f, "XML file too large (size limit reached)"),
1160 DtdDetected => write!(f, "Document type descriptor detected"),
1161 MalformedHierarchy(e) => write!(
1162 f,
1163 "Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
1164 e.expected.as_str(),
1165 e.got.as_str()
1166 ),
1167 ParserError(p) => write!(f, "{p}"),
1168 UnclosedRootNode => write!(f, "unclosed root node"),
1169 UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
1170 NodesLimitReached => write!(f, "nodes limit reached"),
1171 AttributesLimitReached => write!(f, "attributes limit reached"),
1172 NamespacesLimitReached => write!(f, "namespaces limit reached"),
1173 InvalidName(tp) => write!(f, "invalid name at {tp}"),
1174 NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
1175 InvalidChar(tp) => write!(f, "invalid char at {tp}"),
1176 InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
1177 InvalidString(tp) => write!(f, "invalid string at {tp}"),
1178 InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
1179 InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
1180 InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
1181 UnknownToken(tp) => write!(f, "unknown token at {tp}"),
1182 UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
1183 }
1184 }
1185}
1186
1187#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1195#[repr(C)]
1196pub struct ComponentId {
1197 pub collection: AzString,
1199 pub name: AzString,
1201}
1202
1203impl ComponentId {
1204 #[must_use] pub fn builtin(name: &str) -> Self {
1205 Self {
1206 collection: AzString::from_const_str("builtin"),
1207 name: AzString::from(name),
1208 }
1209 }
1210
1211 #[must_use] pub fn new(collection: &str, name: &str) -> Self {
1212 Self {
1213 collection: AzString::from(collection),
1214 name: AzString::from(name),
1215 }
1216 }
1217
1218 #[must_use] pub fn qualified_name(&self) -> String {
1220 format!("{}:{}", self.collection.as_str(), self.name.as_str())
1221 }
1222}
1223
1224#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1230#[repr(C)]
1231pub struct ComponentCallbackArg {
1232 pub name: AzString,
1234 pub arg_type: ComponentFieldType,
1236}
1237
1238impl_vec!(
1239 ComponentCallbackArg,
1240 ComponentCallbackArgVec,
1241 ComponentCallbackArgVecDestructor,
1242 ComponentCallbackArgVecDestructorType,
1243 ComponentCallbackArgVecSlice,
1244 OptionComponentCallbackArg
1245);
1246impl_option!(
1247 ComponentCallbackArg,
1248 OptionComponentCallbackArg,
1249 copy = false,
1250 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1251);
1252impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
1253impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
1254impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
1255impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
1256impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
1257impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
1258impl_vec_clone!(
1259 ComponentCallbackArg,
1260 ComponentCallbackArgVec,
1261 ComponentCallbackArgVecDestructor
1262);
1263
1264#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1266#[repr(C)]
1267pub struct ComponentCallbackSignature {
1268 pub return_type: AzString,
1270 pub args: ComponentCallbackArgVec,
1272}
1273
1274#[repr(C)]
1277pub struct ComponentFieldTypeBox {
1278 pub ptr: *mut ComponentFieldType,
1279}
1280
1281impl ComponentFieldTypeBox {
1282 #[must_use] pub fn new(t: ComponentFieldType) -> Self {
1283 Self {
1284 ptr: Box::into_raw(Box::new(t)),
1285 }
1286 }
1287
1288 #[must_use] pub fn as_ref(&self) -> &ComponentFieldType {
1289 unsafe { &*self.ptr }
1290 }
1291}
1292
1293impl Clone for ComponentFieldTypeBox {
1294 fn clone(&self) -> Self {
1295 Self::new(unsafe { (*self.ptr).clone() })
1296 }
1297}
1298
1299impl Drop for ComponentFieldTypeBox {
1300 fn drop(&mut self) {
1301 let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1308 if !ptr.is_null() {
1309 unsafe {
1310 drop(Box::from_raw(ptr));
1311 }
1312 }
1313 }
1314}
1315
1316impl fmt::Debug for ComponentFieldTypeBox {
1317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1318 if self.ptr.is_null() {
1319 write!(f, "ComponentFieldTypeBox(null)")
1320 } else {
1321 write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
1322 }
1323 }
1324}
1325
1326impl PartialEq for ComponentFieldTypeBox {
1327 fn eq(&self, other: &Self) -> bool {
1328 if self.ptr.is_null() && other.ptr.is_null() {
1329 return true;
1330 }
1331 if self.ptr.is_null() || other.ptr.is_null() {
1332 return false;
1333 }
1334 unsafe { *self.ptr == *other.ptr }
1335 }
1336}
1337
1338impl Eq for ComponentFieldTypeBox {}
1339
1340impl PartialOrd for ComponentFieldTypeBox {
1341 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1342 Some(self.cmp(other))
1343 }
1344}
1345
1346impl Ord for ComponentFieldTypeBox {
1347 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1348 match (self.ptr.is_null(), other.ptr.is_null()) {
1349 (true, true) => core::cmp::Ordering::Equal,
1350 (true, false) => core::cmp::Ordering::Less,
1351 (false, true) => core::cmp::Ordering::Greater,
1352 (false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
1353 }
1354 }
1355}
1356
1357impl Hash for ComponentFieldTypeBox {
1358 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1359 if !self.ptr.is_null() {
1360 unsafe {
1361 (*self.ptr).hash(state);
1362 }
1363 }
1364 }
1365}
1366
1367#[repr(C)]
1370pub struct ComponentFieldValueBox {
1371 pub ptr: *mut ComponentFieldValue,
1372}
1373
1374impl ComponentFieldValueBox {
1375 #[must_use] pub fn new(v: ComponentFieldValue) -> Self {
1376 Self {
1377 ptr: Box::into_raw(Box::new(v)),
1378 }
1379 }
1380
1381 #[must_use] pub fn as_ref(&self) -> &ComponentFieldValue {
1382 unsafe { &*self.ptr }
1383 }
1384}
1385
1386impl Clone for ComponentFieldValueBox {
1387 fn clone(&self) -> Self {
1388 Self::new(unsafe { (*self.ptr).clone() })
1389 }
1390}
1391
1392impl Drop for ComponentFieldValueBox {
1393 fn drop(&mut self) {
1394 let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1397 if !ptr.is_null() {
1398 unsafe {
1399 drop(Box::from_raw(ptr));
1400 }
1401 }
1402 }
1403}
1404
1405impl fmt::Debug for ComponentFieldValueBox {
1406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1407 if self.ptr.is_null() {
1408 write!(f, "ComponentFieldValueBox(null)")
1409 } else {
1410 write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
1411 }
1412 }
1413}
1414
1415impl PartialEq for ComponentFieldValueBox {
1416 fn eq(&self, other: &Self) -> bool {
1417 if self.ptr.is_null() && other.ptr.is_null() {
1418 return true;
1419 }
1420 if self.ptr.is_null() || other.ptr.is_null() {
1421 return false;
1422 }
1423 unsafe { *self.ptr == *other.ptr }
1424 }
1425}
1426
1427#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1431#[repr(C, u8)]
1432pub enum ComponentFieldType {
1433 String,
1434 Bool,
1435 I32,
1436 I64,
1437 U32,
1438 U64,
1439 Usize,
1440 F32,
1441 F64,
1442 ColorU,
1443 CssProperty,
1444 ImageRef,
1445 FontRef,
1446 StyledDom,
1448 Callback(ComponentCallbackSignature),
1450 RefAny(AzString),
1452 OptionType(ComponentFieldTypeBox),
1454 VecType(ComponentFieldTypeBox),
1456 StructRef(AzString),
1458 EnumRef(AzString),
1460}
1461
1462impl ComponentFieldType {
1463 #[must_use] pub fn parse(s: &str) -> Option<Self> {
1467 Self::parse_depth(s, 0)
1468 }
1469
1470 fn parse_depth(s: &str, depth: usize) -> Option<Self> {
1477 if depth > MAX_TYPE_PARSE_DEPTH {
1478 return None;
1479 }
1480 let s = s.trim();
1481 match s {
1482 "String" | "string" => return Some(Self::String),
1483 "Bool" | "bool" => return Some(Self::Bool),
1484 "I32" | "i32" => return Some(Self::I32),
1485 "I64" | "i64" => return Some(Self::I64),
1486 "U32" | "u32" => return Some(Self::U32),
1487 "U64" | "u64" => return Some(Self::U64),
1488 "Usize" | "usize" => return Some(Self::Usize),
1489 "F32" | "f32" => return Some(Self::F32),
1490 "F64" | "f64" => return Some(Self::F64),
1491 "ColorU" => return Some(Self::ColorU),
1492 "CssProperty" => return Some(Self::CssProperty),
1493 "ImageRef" => return Some(Self::ImageRef),
1494 "FontRef" => return Some(Self::FontRef),
1495 "StyledDom" => return Some(Self::StyledDom),
1496 "RefAny" => return Some(Self::RefAny(AzString::from(""))),
1497 _ => {}
1498 }
1499
1500 if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
1502 let inner_type = Self::parse_depth(inner, depth + 1)?;
1503 return Some(Self::OptionType(ComponentFieldTypeBox::new(
1504 inner_type,
1505 )));
1506 }
1507
1508 if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
1510 let inner_type = Self::parse_depth(inner, depth + 1)?;
1511 return Some(Self::VecType(ComponentFieldTypeBox::new(
1512 inner_type,
1513 )));
1514 }
1515
1516 if let Some(sig) = s
1518 .strip_prefix("Callback(")
1519 .and_then(|r| r.strip_suffix(')'))
1520 {
1521 return Some(Self::Callback(ComponentCallbackSignature {
1522 return_type: AzString::from(sig),
1523 args: Vec::new().into(),
1524 }));
1525 }
1526
1527 if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
1529 return Some(Self::RefAny(AzString::from(hint)));
1530 }
1531
1532 if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
1534 return Some(Self::EnumRef(AzString::from(name)));
1535 }
1536
1537 if let Some(name) = s
1539 .strip_prefix("StructRef(")
1540 .and_then(|r| r.strip_suffix(')'))
1541 {
1542 return Some(Self::StructRef(AzString::from(name)));
1543 }
1544
1545 if s.chars().next().is_some_and(char::is_uppercase) {
1547 return Some(Self::StructRef(AzString::from(s)));
1548 }
1549
1550 None
1551 }
1552
1553 #[must_use] pub fn format(&self) -> String {
1556 match self {
1557 Self::String => "String".to_string(),
1558 Self::Bool => "Bool".to_string(),
1559 Self::I32 => "I32".to_string(),
1560 Self::I64 => "I64".to_string(),
1561 Self::U32 => "U32".to_string(),
1562 Self::U64 => "U64".to_string(),
1563 Self::Usize => "Usize".to_string(),
1564 Self::F32 => "F32".to_string(),
1565 Self::F64 => "F64".to_string(),
1566 Self::ColorU => "ColorU".to_string(),
1567 Self::CssProperty => "CssProperty".to_string(),
1568 Self::ImageRef => "ImageRef".to_string(),
1569 Self::FontRef => "FontRef".to_string(),
1570 Self::StyledDom => "StyledDom".to_string(),
1571 Self::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
1572 Self::RefAny(hint) => {
1573 if hint.as_str().is_empty() {
1574 "RefAny".to_string()
1575 } else {
1576 format!("RefAny({})", hint.as_str())
1577 }
1578 }
1579 Self::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
1580 Self::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
1581 Self::StructRef(name) | Self::EnumRef(name) => name.as_str().to_string(),
1582 }
1583 }
1584}
1585
1586impl fmt::Display for ComponentFieldType {
1587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1588 f.write_str(&self.format())
1589 }
1590}
1591
1592#[derive(Debug, Clone, PartialEq)]
1594#[repr(C)]
1595pub struct ComponentEnumVariant {
1596 pub name: AzString,
1598 pub description: AzString,
1600 pub fields: ComponentDataFieldVec,
1602}
1603
1604impl_vec!(
1605 ComponentEnumVariant,
1606 ComponentEnumVariantVec,
1607 ComponentEnumVariantVecDestructor,
1608 ComponentEnumVariantVecDestructorType,
1609 ComponentEnumVariantVecSlice,
1610 OptionComponentEnumVariant
1611);
1612impl_option!(
1613 ComponentEnumVariant,
1614 OptionComponentEnumVariant,
1615 copy = false,
1616 [Debug, Clone, PartialEq]
1617);
1618impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
1619impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
1620impl_vec_clone!(
1621 ComponentEnumVariant,
1622 ComponentEnumVariantVec,
1623 ComponentEnumVariantVecDestructor
1624);
1625
1626#[derive(Debug, Clone, PartialEq)]
1629#[repr(C)]
1630pub struct ComponentEnumModel {
1631 pub name: AzString,
1633 pub description: AzString,
1635 pub variants: ComponentEnumVariantVec,
1637}
1638
1639impl_vec!(
1640 ComponentEnumModel,
1641 ComponentEnumModelVec,
1642 ComponentEnumModelVecDestructor,
1643 ComponentEnumModelVecDestructorType,
1644 ComponentEnumModelVecSlice,
1645 OptionComponentEnumModel
1646);
1647impl_option!(
1648 ComponentEnumModel,
1649 OptionComponentEnumModel,
1650 copy = false,
1651 [Debug, Clone, PartialEq]
1652);
1653impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
1654impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
1655impl_vec_clone!(
1656 ComponentEnumModel,
1657 ComponentEnumModelVec,
1658 ComponentEnumModelVecDestructor
1659);
1660
1661#[derive(Debug, Clone, PartialEq)]
1663#[repr(C, u8)]
1664pub enum ComponentDefaultValue {
1665 None,
1667 String(AzString),
1669 Bool(bool),
1671 I32(i32),
1673 I64(i64),
1675 U32(u32),
1677 U64(u64),
1679 Usize(usize),
1681 F32(f32),
1683 F64(f64),
1685 ColorU(ColorU),
1687 ComponentInstance(ComponentInstanceDefault),
1689 CallbackFnPointer(AzString),
1691 Json(AzString),
1693}
1694
1695impl_option!(
1696 ComponentDefaultValue,
1697 OptionComponentDefaultValue,
1698 copy = false,
1699 [Debug, Clone, PartialEq]
1700);
1701
1702#[derive(Debug, Clone, PartialEq)]
1704#[repr(C)]
1705pub struct ComponentInstanceDefault {
1706 pub library: AzString,
1708 pub component: AzString,
1710 pub field_overrides: ComponentFieldOverrideVec,
1712}
1713
1714#[derive(Debug, Clone, PartialEq, Eq)]
1716#[repr(C)]
1717pub struct ComponentFieldOverride {
1718 pub field_name: AzString,
1720 pub source: ComponentFieldValueSource,
1722}
1723
1724impl_vec!(
1725 ComponentFieldOverride,
1726 ComponentFieldOverrideVec,
1727 ComponentFieldOverrideVecDestructor,
1728 ComponentFieldOverrideVecDestructorType,
1729 ComponentFieldOverrideVecSlice,
1730 OptionComponentFieldOverride
1731);
1732impl_option!(
1733 ComponentFieldOverride,
1734 OptionComponentFieldOverride,
1735 copy = false,
1736 [Debug, Clone, PartialEq, Eq]
1737);
1738impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
1739impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
1740impl_vec_clone!(
1741 ComponentFieldOverride,
1742 ComponentFieldOverrideVec,
1743 ComponentFieldOverrideVecDestructor
1744);
1745
1746#[derive(Debug, Clone, PartialEq, Eq)]
1748#[repr(C, u8)]
1749pub enum ComponentFieldValueSource {
1750 Default,
1752 Literal(AzString),
1754 Binding(AzString),
1756}
1757#[allow(variant_size_differences)] #[derive(Debug, Clone, PartialEq)]
1761#[repr(C, u8)]
1762#[allow(clippy::large_enum_variant)] pub enum ComponentFieldValue {
1764 String(AzString),
1765 Bool(bool),
1766 I32(i32),
1767 I64(i64),
1768 U32(u32),
1769 U64(u64),
1770 Usize(usize),
1771 F32(f32),
1772 F64(f64),
1773 ColorU(ColorU),
1774 None,
1776 Some(ComponentFieldValueBox),
1778 Vec(ComponentFieldValueVec),
1780 StyledDom(StyledDom),
1782 Struct(ComponentFieldNamedValueVec),
1784 Enum {
1786 variant: AzString,
1787 fields: ComponentFieldNamedValueVec,
1788 },
1789 Callback(AzString),
1791 RefAny(crate::refany::RefAny),
1793}
1794
1795#[derive(Debug, Clone, PartialEq)]
1797#[repr(C)]
1798pub struct ComponentFieldNamedValue {
1799 pub name: AzString,
1800 pub value: ComponentFieldValue,
1801}
1802
1803impl_vec!(
1804 ComponentFieldNamedValue,
1805 ComponentFieldNamedValueVec,
1806 ComponentFieldNamedValueVecDestructor,
1807 ComponentFieldNamedValueVecDestructorType,
1808 ComponentFieldNamedValueVecSlice,
1809 OptionComponentFieldNamedValue
1810);
1811impl_option!(
1812 ComponentFieldNamedValue,
1813 OptionComponentFieldNamedValue,
1814 copy = false,
1815 [Debug, Clone, PartialEq]
1816);
1817impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1818impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1819impl_vec_clone!(
1820 ComponentFieldNamedValue,
1821 ComponentFieldNamedValueVec,
1822 ComponentFieldNamedValueVecDestructor
1823);
1824
1825impl ComponentFieldNamedValueVec {
1826 #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
1828 self.as_ref().iter().find_map(|v| {
1829 if v.name.as_str() == name {
1830 Some(&v.value)
1831 } else {
1832 None
1833 }
1834 })
1835 }
1836
1837 #[must_use] pub fn get_string(&self, name: &str) -> Option<&AzString> {
1839 match self.get_field(name) {
1840 Some(ComponentFieldValue::String(s)) => Some(s),
1841 _ => None,
1842 }
1843 }
1844}
1845
1846impl_vec!(
1847 ComponentFieldValue,
1848 ComponentFieldValueVec,
1849 ComponentFieldValueVecDestructor,
1850 ComponentFieldValueVecDestructorType,
1851 ComponentFieldValueVecSlice,
1852 OptionComponentFieldValue
1853);
1854impl_option!(
1855 ComponentFieldValue,
1856 OptionComponentFieldValue,
1857 copy = false,
1858 [Debug, Clone, PartialEq]
1859);
1860impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
1861impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
1862impl_vec_clone!(
1863 ComponentFieldValue,
1864 ComponentFieldValueVec,
1865 ComponentFieldValueVecDestructor
1866);
1867
1868#[derive(Debug, Clone, PartialEq)]
1870#[repr(C)]
1871pub struct ComponentDataField {
1872 pub name: AzString,
1874 pub field_type: ComponentFieldType,
1876 pub default_value: OptionComponentDefaultValue,
1878 pub required: bool,
1880 pub description: AzString,
1882}
1883
1884impl_vec!(
1885 ComponentDataField,
1886 ComponentDataFieldVec,
1887 ComponentDataFieldVecDestructor,
1888 ComponentDataFieldVecDestructorType,
1889 ComponentDataFieldVecSlice,
1890 OptionComponentDataField
1891);
1892impl_option!(
1893 ComponentDataField,
1894 OptionComponentDataField,
1895 copy = false,
1896 [Debug, Clone, PartialEq]
1897);
1898impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
1899impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
1900impl_vec_clone!(
1901 ComponentDataField,
1902 ComponentDataFieldVec,
1903 ComponentDataFieldVecDestructor
1904);
1905
1906#[derive(Debug, Clone)]
1913#[repr(C)]
1914pub struct ComponentDataModel {
1915 pub name: AzString,
1917 pub description: AzString,
1919 pub fields: ComponentDataFieldVec,
1921}
1922
1923impl ComponentDataModel {
1924 #[must_use] pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
1926 self.fields
1927 .as_ref()
1928 .iter()
1929 .find(|f| f.name.as_str() == name)
1930 }
1931
1932 #[must_use] pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
1934 self.get_field(name).and_then(|f| match &f.default_value {
1935 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
1936 _ => None,
1937 })
1938 }
1939
1940 #[must_use] pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
1943 let mut fields_vec = core::mem::replace(
1944 &mut self.fields,
1945 ComponentDataFieldVec::from_const_slice(&[]),
1946 )
1947 .into_library_owned_vec();
1948 for f in &mut fields_vec {
1949 if f.name.as_str() == name {
1950 f.default_value = OptionComponentDefaultValue::Some(value);
1951 break;
1952 }
1953 }
1954 self.fields = ComponentDataFieldVec::from_vec(fields_vec);
1955 self
1956 }
1957}
1958
1959impl_vec!(
1960 ComponentDataModel,
1961 ComponentDataModelVec,
1962 ComponentDataModelVecDestructor,
1963 ComponentDataModelVecDestructorType,
1964 ComponentDataModelVecSlice,
1965 OptionComponentDataModel
1966);
1967impl_option!(
1968 ComponentDataModel,
1969 OptionComponentDataModel,
1970 copy = false,
1971 [Debug, Clone]
1972);
1973impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
1974impl_vec_clone!(
1975 ComponentDataModel,
1976 ComponentDataModelVec,
1977 ComponentDataModelVecDestructor
1978);
1979impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
1980
1981#[cfg(feature = "serde-json")]
1986mod serde_impl {
1987 use super::*;
1988 use serde::ser::SerializeStruct;
1989 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1990
1991 fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
1994 serializer.serialize_str(s.as_str())
1995 }
1996
1997 fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
1998 let s = alloc::string::String::deserialize(deserializer)?;
1999 Ok(AzString::from(s.as_str()))
2000 }
2001
2002 impl Serialize for ComponentFieldType {
2005 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2006 serializer.serialize_str(&field_type_to_string(self))
2007 }
2008 }
2009
2010 impl<'de> Deserialize<'de> for ComponentFieldType {
2011 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2012 let s = alloc::string::String::deserialize(deserializer)?;
2013 Ok(string_to_field_type(&s))
2014 }
2015 }
2016
2017 fn field_type_to_string(ft: &ComponentFieldType) -> alloc::string::String {
2018 match ft {
2019 ComponentFieldType::String => "String".into(),
2020 ComponentFieldType::Bool => "bool".into(),
2021 ComponentFieldType::I32 => "i32".into(),
2022 ComponentFieldType::I64 => "i64".into(),
2023 ComponentFieldType::U32 => "u32".into(),
2024 ComponentFieldType::U64 => "u64".into(),
2025 ComponentFieldType::Usize => "usize".into(),
2026 ComponentFieldType::F32 => "f32".into(),
2027 ComponentFieldType::F64 => "f64".into(),
2028 ComponentFieldType::ColorU => "ColorU".into(),
2029 ComponentFieldType::CssProperty => "CssProperty".into(),
2030 ComponentFieldType::ImageRef => "ImageRef".into(),
2031 ComponentFieldType::FontRef => "FontRef".into(),
2032 ComponentFieldType::StyledDom => "Dom".into(),
2033 ComponentFieldType::Callback(sig) => {
2034 alloc::format!("Callback({})", sig.return_type.as_str())
2035 }
2036 ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
2037 ComponentFieldType::OptionType(inner) => {
2038 alloc::format!("Option<{}>", field_type_to_string(inner.as_ref()))
2039 }
2040 ComponentFieldType::VecType(inner) => {
2041 alloc::format!("Vec<{}>", field_type_to_string(inner.as_ref()))
2042 }
2043 ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
2044 ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
2045 }
2046 }
2047
2048 fn string_to_field_type(s: &str) -> ComponentFieldType {
2049 match s {
2050 "String" | "string" => ComponentFieldType::String,
2051 "bool" | "Bool" => ComponentFieldType::Bool,
2052 "i32" | "I32" => ComponentFieldType::I32,
2053 "i64" | "I64" => ComponentFieldType::I64,
2054 "u32" | "U32" => ComponentFieldType::U32,
2055 "u64" | "U64" => ComponentFieldType::U64,
2056 "usize" | "Usize" => ComponentFieldType::Usize,
2057 "f32" | "F32" => ComponentFieldType::F32,
2058 "f64" | "F64" => ComponentFieldType::F64,
2059 "ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
2060 "CssProperty" => ComponentFieldType::CssProperty,
2061 "ImageRef" | "Image" => ComponentFieldType::ImageRef,
2062 "FontRef" | "Font" => ComponentFieldType::FontRef,
2063 "Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
2064 other => {
2065 if let Some(inner) = other
2066 .strip_prefix("Option<")
2067 .and_then(|s| s.strip_suffix('>'))
2068 {
2069 ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
2070 string_to_field_type(inner),
2071 ))
2072 } else if let Some(inner) =
2073 other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>'))
2074 {
2075 ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(
2076 inner,
2077 )))
2078 } else if let Some(name) = other.strip_prefix("struct:") {
2079 ComponentFieldType::StructRef(AzString::from(name))
2080 } else if let Some(name) = other.strip_prefix("enum:") {
2081 ComponentFieldType::EnumRef(AzString::from(name))
2082 } else if other.starts_with("Callback") {
2083 let ret = other
2084 .strip_prefix("Callback(")
2085 .and_then(|s| s.strip_suffix(')'))
2086 .unwrap_or("()");
2087 ComponentFieldType::Callback(ComponentCallbackSignature {
2088 return_type: AzString::from(ret),
2089 args: ComponentCallbackArgVec::from_const_slice(&[]),
2090 })
2091 } else if other.starts_with("RefAny") {
2092 let hint = other
2093 .strip_prefix("RefAny(")
2094 .and_then(|s| s.strip_suffix(')'))
2095 .unwrap_or("");
2096 ComponentFieldType::RefAny(AzString::from(hint))
2097 } else {
2098 ComponentFieldType::String }
2100 }
2101 }
2102 }
2103
2104 impl Serialize for ComponentDefaultValue {
2107 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2108 use serde::ser::SerializeMap;
2109 match self {
2110 ComponentDefaultValue::None => serializer.serialize_none(),
2111 ComponentDefaultValue::String(s) => serializer.serialize_str(s.as_str()),
2112 ComponentDefaultValue::Bool(b) => serializer.serialize_bool(*b),
2113 ComponentDefaultValue::I32(v) => serializer.serialize_i32(*v),
2114 ComponentDefaultValue::I64(v) => serializer.serialize_i64(*v),
2115 ComponentDefaultValue::U32(v) => serializer.serialize_u32(*v),
2116 ComponentDefaultValue::U64(v) => serializer.serialize_u64(*v),
2117 ComponentDefaultValue::Usize(v) => serializer.serialize_u64(*v as u64),
2118 ComponentDefaultValue::F32(v) => serializer.serialize_f32(*v),
2119 ComponentDefaultValue::F64(v) => serializer.serialize_f64(*v),
2120 ComponentDefaultValue::ColorU(c) => serializer.serialize_str(&alloc::format!(
2121 "#{:02x}{:02x}{:02x}{:02x}",
2122 c.r,
2123 c.g,
2124 c.b,
2125 c.a
2126 )),
2127 ComponentDefaultValue::ComponentInstance(ci) => {
2128 let mut map = serializer.serialize_map(Some(2))?;
2129 map.serialize_entry("library", ci.library.as_str())?;
2130 map.serialize_entry("component", ci.component.as_str())?;
2131 map.end()
2132 }
2133 ComponentDefaultValue::CallbackFnPointer(name) => {
2134 serializer.serialize_str(name.as_str())
2135 }
2136 ComponentDefaultValue::Json(json_str) => {
2137 match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
2139 Ok(v) => v.serialize(serializer),
2140 Err(_) => serializer.serialize_str(json_str.as_str()),
2141 }
2142 }
2143 }
2144 }
2145 }
2146
2147 impl<'de> Deserialize<'de> for ComponentDefaultValue {
2148 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2149 let val = serde_json::Value::deserialize(deserializer)?;
2150 Ok(match val {
2151 serde_json::Value::Null => ComponentDefaultValue::None,
2152 serde_json::Value::Bool(b) => ComponentDefaultValue::Bool(b),
2153 serde_json::Value::Number(n) => {
2154 if let Some(i) = n.as_i64() {
2155 if let Ok(v) = i32::try_from(i) {
2156 ComponentDefaultValue::I32(v)
2157 } else {
2158 ComponentDefaultValue::I64(i)
2159 }
2160 } else if let Some(f) = n.as_f64() {
2161 ComponentDefaultValue::F64(f)
2162 } else {
2163 ComponentDefaultValue::None
2164 }
2165 }
2166 serde_json::Value::String(s) => {
2167 ComponentDefaultValue::String(AzString::from(s.as_str()))
2168 }
2169 _ => ComponentDefaultValue::None,
2170 })
2171 }
2172 }
2173
2174 impl Serialize for OptionComponentDefaultValue {
2177 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2178 match self {
2179 OptionComponentDefaultValue::Some(v) => v.serialize(serializer),
2180 OptionComponentDefaultValue::None => serializer.serialize_none(),
2181 }
2182 }
2183 }
2184
2185 impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
2186 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2187 let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
2188 Ok(match val {
2189 Some(v) => OptionComponentDefaultValue::Some(v),
2190 None => OptionComponentDefaultValue::None,
2191 })
2192 }
2193 }
2194
2195 impl Serialize for ComponentDataField {
2198 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2199 let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
2200 s.serialize_field("name", self.name.as_str())?;
2201 s.serialize_field("type", &self.field_type)?;
2202 s.serialize_field("default", &self.default_value)?;
2203 s.serialize_field("required", &self.required)?;
2204 s.serialize_field("description", self.description.as_str())?;
2205 s.end()
2206 }
2207 }
2208
2209 impl<'de> Deserialize<'de> for ComponentDataField {
2210 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2211 #[derive(Deserialize)]
2212 struct Helper {
2213 name: alloc::string::String,
2214 #[serde(rename = "type", default = "default_type")]
2215 field_type: ComponentFieldType,
2216 #[serde(default)]
2217 default: OptionComponentDefaultValue,
2218 #[serde(default)]
2219 required: bool,
2220 #[serde(default)]
2221 description: alloc::string::String,
2222 }
2223 fn default_type() -> ComponentFieldType {
2224 ComponentFieldType::String
2225 }
2226
2227 let h = Helper::deserialize(deserializer)?;
2228 Ok(ComponentDataField {
2229 name: AzString::from(h.name.as_str()),
2230 field_type: h.field_type,
2231 default_value: h.default,
2232 required: h.required,
2233 description: AzString::from(h.description.as_str()),
2234 })
2235 }
2236 }
2237
2238 impl Serialize for ComponentDataModel {
2241 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2242 let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
2243 s.serialize_field("name", self.name.as_str())?;
2244 s.serialize_field("description", self.description.as_str())?;
2245 let fields: alloc::vec::Vec<&ComponentDataField> =
2246 self.fields.as_ref().iter().collect();
2247 s.serialize_field("fields", &fields)?;
2248 s.end()
2249 }
2250 }
2251
2252 impl<'de> Deserialize<'de> for ComponentDataModel {
2253 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2261 use serde::de::{IgnoredAny, MapAccess, Visitor};
2262
2263 struct ModelVisitor;
2264
2265 impl<'de> Visitor<'de> for ModelVisitor {
2266 type Value = ComponentDataModel;
2267
2268 fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
2269 f.write_str("a data model object with `name`, `description` and `fields`")
2270 }
2271
2272 fn visit_map<A: MapAccess<'de>>(
2273 self,
2274 mut map: A,
2275 ) -> Result<Self::Value, A::Error> {
2276 let mut name: Option<alloc::string::String> = None;
2277 let mut description: Option<alloc::string::String> = None;
2278 let mut fields: Option<alloc::vec::Vec<ComponentDataField>> = None;
2279
2280 while let Some(key) = map.next_key::<alloc::string::String>()? {
2281 match key.as_str() {
2282 "name" => name = Some(map.next_value()?),
2283 "description" => description = Some(map.next_value()?),
2284 "fields" => fields = Some(map.next_value()?),
2285 _ => {
2288 map.next_value::<IgnoredAny>()?;
2289 }
2290 }
2291 }
2292
2293 Ok(ComponentDataModel {
2294 name: AzString::from(name.unwrap_or_default().as_str()),
2295 description: AzString::from(description.unwrap_or_default().as_str()),
2296 fields: ComponentDataFieldVec::from_vec(fields.unwrap_or_default()),
2297 })
2298 }
2299 }
2300
2301 deserializer.deserialize_map(ModelVisitor)
2302 }
2303 }
2304}
2305
2306#[cfg(feature = "serde-json")]
2308pub use serde_impl::*;
2309
2310#[cfg(feature = "serde-json")]
2311impl ComponentDataModel {
2312 pub fn to_json(&self) -> Result<alloc::string::String, alloc::string::String> {
2314 serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{}", e))
2315 }
2316
2317 pub fn from_json(json: &str) -> Result<Self, alloc::string::String> {
2319 serde_json::from_str(json).map_err(|e| alloc::format!("{}", e))
2320 }
2321}
2322
2323#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2325#[repr(C)]
2326#[derive(Default)]
2327pub enum ComponentSource {
2328 Builtin,
2330 Compiled,
2332 #[default]
2334 UserDefined,
2335}
2336
2337
2338impl ComponentSource {
2339 #[must_use] pub fn create() -> Self {
2340 Self::default()
2341 }
2342}
2343
2344#[allow(missing_copy_implementations)]
2349#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2350#[repr(C)]
2351pub enum CompileTarget {
2352 Rust,
2353 C,
2354 Cpp,
2355 Python,
2356}
2357
2358impl_result!(
2359 StyledDom,
2360 RenderDomError,
2361 ResultStyledDomRenderDomError,
2362 copy = false,
2363 [Debug, Clone, PartialEq]
2364);
2365
2366impl_result!(
2367 AzString,
2368 CompileError,
2369 ResultStringCompileError,
2370 copy = false,
2371 [Debug, Clone, PartialEq]
2372);
2373
2374pub type ComponentRenderFn =
2381 fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError;
2382
2383pub type ComponentCompileFn = fn(
2385 &ComponentDef,
2386 &CompileTarget,
2387 &ComponentDataModel,
2388 indent: usize,
2389) -> ResultStringCompileError;
2390
2391pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
2394
2395#[repr(C)]
2403pub struct RegisterComponentFn {
2404 pub cb: RegisterComponentFnType,
2405 pub ctx: crate::refany::OptionRefAny,
2408}
2409
2410impl_callback!(RegisterComponentFn, RegisterComponentFnType);
2411
2412pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
2415
2416#[repr(C)]
2424pub struct RegisterComponentLibraryFn {
2425 pub cb: RegisterComponentLibraryFnType,
2426 pub ctx: crate::refany::OptionRefAny,
2429}
2430
2431impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
2432
2433#[derive(Clone)]
2437#[repr(C)]
2438pub struct ComponentDef {
2439 pub id: ComponentId,
2441 pub display_name: AzString,
2443 pub description: AzString,
2445 pub css: AzString,
2447 pub source: ComponentSource,
2449 pub data_model: ComponentDataModel,
2455 pub render_fn: ComponentRenderFn,
2457 pub compile_fn: ComponentCompileFn,
2459 pub render_fn_source: OptionString,
2461 pub compile_fn_source: OptionString,
2463}
2464
2465impl fmt::Debug for ComponentDef {
2466 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2467 f.debug_struct("ComponentDef")
2468 .field("id", &self.id)
2469 .field("display_name", &self.display_name)
2470 .field("source", &self.source)
2471 .field("data_model", &self.data_model.name)
2472 .finish_non_exhaustive()
2473 }
2474}
2475
2476impl_vec!(
2477 ComponentDef,
2478 ComponentDefVec,
2479 ComponentDefVecDestructor,
2480 ComponentDefVecDestructorType,
2481 ComponentDefVecSlice,
2482 OptionComponentDef
2483);
2484impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
2485impl_vec_debug!(ComponentDef, ComponentDefVec);
2486impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
2487impl_vec_mut!(ComponentDef, ComponentDefVec);
2488
2489#[derive(Debug, Clone)]
2491#[repr(C)]
2492pub struct ComponentLibrary {
2493 pub name: AzString,
2495 pub version: AzString,
2497 pub description: AzString,
2499 pub components: ComponentDefVec,
2501 pub exportable: bool,
2503 pub modifiable: bool,
2506 pub data_models: ComponentDataModelVec,
2509 pub enum_models: ComponentEnumModelVec,
2512}
2513
2514impl_vec!(
2515 ComponentLibrary,
2516 ComponentLibraryVec,
2517 ComponentLibraryVecDestructor,
2518 ComponentLibraryVecDestructorType,
2519 ComponentLibraryVecSlice,
2520 OptionComponentLibrary
2521);
2522impl_option!(
2523 ComponentLibrary,
2524 OptionComponentLibrary,
2525 copy = false,
2526 [Debug, Clone]
2527);
2528impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
2529impl_vec_clone!(
2530 ComponentLibrary,
2531 ComponentLibraryVec,
2532 ComponentLibraryVecDestructor
2533);
2534impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
2535
2536#[derive(Debug, Clone)]
2538#[repr(C)]
2539pub struct ComponentMap {
2540 pub libraries: ComponentLibraryVec,
2542}
2543
2544impl ComponentMap {
2545 #[must_use] pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
2547 self.libraries
2548 .iter()
2549 .find(|lib| lib.name.as_str() == collection)
2550 .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
2551 }
2552
2553 #[must_use] pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
2555 self.get("builtin", name)
2556 }
2557
2558 #[must_use] pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
2560 if let Some((collection, name)) = qualified.split_once(':') {
2561 self.get(collection, name)
2562 } else {
2563 self.get_unqualified(qualified)
2564 }
2565 }
2566
2567 #[must_use] pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
2569 self.libraries.iter().filter(|lib| lib.exportable).collect()
2570 }
2571
2572 #[must_use] pub fn all_components(&self) -> Vec<&ComponentDef> {
2574 self.libraries
2575 .iter()
2576 .flat_map(|lib| lib.components.iter())
2577 .collect()
2578 }
2579}
2580
2581macro_rules! html_tag_node_types {
2593 ($($tag:literal => $variant:ident),* $(,)?) => {
2594 #[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
2597 match tag {
2598 "img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
2603 crate::resources::ImageRef::null_image(
2604 0,
2605 0,
2606 crate::resources::RawImageFormat::RGBA8,
2607 alloc::vec::Vec::new(),
2608 ),
2609 )),
2610 $($tag => NodeType::$variant,)*
2611 _ => NodeType::Div,
2612 }
2613 }
2614
2615 fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
2618 match tag {
2619 "img" | "image" => NodeTypeTag::Img,
2622 "icon" => NodeTypeTag::Icon,
2623 $($tag => NodeTypeTag::$variant,)*
2624 _ => NodeTypeTag::Div,
2625 }
2626 }
2627 };
2628}
2629
2630html_tag_node_types! {
2631 "html" => Html,
2633 "head" => Head,
2634 "title" => Title,
2635 "body" => Body,
2636 "div" => Div,
2638 "header" => Header,
2639 "footer" => Footer,
2640 "section" => Section,
2641 "article" => Article,
2642 "aside" => Aside,
2643 "nav" => Nav,
2644 "main" => Main,
2645 "figure" => Figure,
2646 "figcaption" => FigCaption,
2647 "address" => Address,
2648 "details" => Details,
2649 "summary" => Summary,
2650 "dialog" => Dialog,
2651 "h1" => H1,
2653 "h2" => H2,
2654 "h3" => H3,
2655 "h4" => H4,
2656 "h5" => H5,
2657 "h6" => H6,
2658 "p" => P,
2660 "span" => Span,
2661 "pre" => Pre,
2662 "code" => Code,
2663 "blockquote" => BlockQuote,
2664 "br" => Br,
2665 "hr" => Hr,
2666 "ul" => Ul,
2668 "ol" => Ol,
2669 "li" => Li,
2670 "dl" => Dl,
2671 "dt" => Dt,
2672 "dd" => Dd,
2673 "menu" => Menu,
2674 "menuitem" => MenuItem,
2675 "dir" => Dir,
2676 "table" => Table,
2678 "caption" => Caption,
2679 "thead" => THead,
2680 "tbody" => TBody,
2681 "tfoot" => TFoot,
2682 "tr" => Tr,
2683 "th" => Th,
2684 "td" => Td,
2685 "colgroup" => ColGroup,
2686 "col" => Col,
2687 "form" => Form,
2689 "fieldset" => FieldSet,
2690 "legend" => Legend,
2691 "label" => Label,
2692 "input" => Input,
2693 "button" => Button,
2694 "select" => Select,
2695 "optgroup" => OptGroup,
2696 "option" => SelectOption,
2697 "textarea" => TextArea,
2698 "output" => Output,
2699 "progress" => Progress,
2700 "meter" => Meter,
2701 "datalist" => DataList,
2702 "a" => A,
2704 "strong" => Strong,
2705 "em" => Em,
2706 "b" => B,
2707 "i" => I,
2708 "u" => U,
2709 "s" => S,
2710 "small" => Small,
2711 "mark" => Mark,
2712 "del" => Del,
2713 "ins" => Ins,
2714 "samp" => Samp,
2715 "kbd" => Kbd,
2716 "var" => Var,
2717 "cite" => Cite,
2718 "dfn" => Dfn,
2719 "abbr" => Abbr,
2720 "acronym" => Acronym,
2721 "q" => Q,
2722 "time" => Time,
2723 "sub" => Sub,
2724 "sup" => Sup,
2725 "big" => Big,
2726 "bdo" => Bdo,
2727 "bdi" => Bdi,
2728 "wbr" => Wbr,
2729 "ruby" => Ruby,
2730 "rt" => Rt,
2731 "rtc" => Rtc,
2732 "rp" => Rp,
2733 "data" => Data,
2734 "canvas" => Canvas,
2736 "object" => Object,
2737 "param" => Param,
2738 "embed" => Embed,
2739 "audio" => Audio,
2740 "video" => Video,
2741 "source" => Source,
2742 "track" => Track,
2743 "map" => Map,
2744 "area" => Area,
2745 "svg" => Svg,
2747 "g" => SvgG,
2748 "defs" => SvgDefs,
2749 "symbol" => SvgSymbol,
2750 "use" => SvgUse,
2751 "switch" => SvgSwitch,
2752 "path" => SvgPath,
2753 "circle" => SvgCircle,
2754 "rect" => SvgRect,
2755 "ellipse" => SvgEllipse,
2756 "line" => SvgLine,
2757 "polygon" => SvgPolygon,
2758 "polyline" => SvgPolyline,
2759 "tspan" => SvgTspan,
2760 "textpath" => SvgTextPath,
2761 "lineargradient" => SvgLinearGradient,
2762 "radialgradient" => SvgRadialGradient,
2763 "stop" => SvgStop,
2764 "pattern" => SvgPattern,
2765 "clippath" => SvgClipPathElement,
2766 "mask" => SvgMask,
2767 "filter" => SvgFilter,
2768 "feblend" => SvgFeBlend,
2769 "fecolormatrix" => SvgFeColorMatrix,
2770 "fecomponenttransfer" => SvgFeComponentTransfer,
2771 "fecomposite" => SvgFeComposite,
2772 "feconvolvematrix" => SvgFeConvolveMatrix,
2773 "fediffuselighting" => SvgFeDiffuseLighting,
2774 "fedisplacementmap" => SvgFeDisplacementMap,
2775 "fedistantlight" => SvgFeDistantLight,
2776 "fedropshadow" => SvgFeDropShadow,
2777 "feflood" => SvgFeFlood,
2778 "fefuncr" => SvgFeFuncR,
2779 "fefuncg" => SvgFeFuncG,
2780 "fefuncb" => SvgFeFuncB,
2781 "fefunca" => SvgFeFuncA,
2782 "fegaussianblur" => SvgFeGaussianBlur,
2783 "feimage" => SvgFeImage,
2784 "femerge" => SvgFeMerge,
2785 "femergenode" => SvgFeMergeNode,
2786 "femorphology" => SvgFeMorphology,
2787 "feoffset" => SvgFeOffset,
2788 "fepointlight" => SvgFePointLight,
2789 "fespecularlighting" => SvgFeSpecularLighting,
2790 "fespotlight" => SvgFeSpotLight,
2791 "fetile" => SvgFeTile,
2792 "feturbulence" => SvgFeTurbulence,
2793 "foreignobject" => SvgForeignObject,
2794 "desc" => SvgDesc,
2795 "view" => SvgView,
2796 "animate" => SvgAnimate,
2797 "animatemotion" => SvgAnimateMotion,
2798 "animatetransform" => SvgAnimateTransform,
2799 "set" => SvgSet,
2800 "mpath" => SvgMpath,
2801 "meta" => Meta,
2803 "link" => Link,
2804 "script" => Script,
2805 "style" => Style,
2806 "base" => Base,
2807}
2808
2809fn builtin_render_fn(
2812 def: &ComponentDef,
2813 data: &ComponentDataModel,
2814 _component_map: &ComponentMap,
2815) -> ResultStyledDomRenderDomError {
2816 let node_type = tag_to_node_type(def.id.name.as_str());
2817 let mut dom = Dom::create_node(node_type);
2818 if let Some(text_str) = data.get_default_string("text") {
2819 let prepared = prepare_string(text_str);
2820 if !prepared.is_empty() {
2821 dom = dom.with_children(alloc::vec![Dom::create_text(prepared)].into());
2822 }
2823 }
2824 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2825 r.into()
2826}
2827
2828fn builtin_compile_fn(
2831 def: &ComponentDef,
2832 target: &CompileTarget,
2833 data: &ComponentDataModel,
2834 indent: usize,
2835) -> ResultStringCompileError {
2836 let node_type = tag_to_node_type(def.id.name.as_str());
2837 let type_name = format!("{node_type:?}"); let text = data.get_default_string("text");
2839
2840 let r: Result<AzString, CompileError> = match target {
2841 CompileTarget::Rust => {
2842 text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
2843 "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text(\"{}\")])",
2844 type_name,
2845 text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
2846 ).into()))
2847 }
2848 CompileTarget::C => {
2849 text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
2850 "AzDom_createText(AZ_STR(\"{}\"))",
2851 text_str
2852 .as_str()
2853 .replace('\\', "\\\\")
2854 .replace('"', "\\\"")
2855 )
2856 .into()))
2857 }
2858 CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
2859 CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
2860 };
2861 r.into()
2862}
2863
2864fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn fmt::Display) {
2866 use crate::dom::{Dom, NodeType};
2867 let text = alloc::format!("{field_name}: {value}");
2868 children.push(
2869 Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(text)].into()),
2870 );
2871}
2872
2873#[allow(clippy::too_many_lines)] #[must_use] pub fn user_defined_render_fn(
2886 def: &ComponentDef,
2887 data: &ComponentDataModel,
2888 component_map: &ComponentMap,
2889) -> ResultStyledDomRenderDomError {
2890 use crate::dom::{Dom, NodeType};
2891 use azul_css::css::Css;
2892
2893 let mut children: Vec<Dom> = Vec::new();
2894
2895 for field in data.fields.as_ref() {
2896 let field_name = field.name.as_str();
2897
2898 match &field.default_value {
2900 OptionComponentDefaultValue::None => {
2901 }
2903 OptionComponentDefaultValue::Some(default_val) => {
2904 match default_val {
2905 ComponentDefaultValue::String(s) => {
2906 let text = s.as_str().trim();
2907 if !text.is_empty() {
2908 let label_dom = Dom::create_node(NodeType::Div).with_children(
2909 alloc::vec![Dom::create_text(text.to_string())].into(),
2910 );
2911 children.push(label_dom);
2912 }
2913 }
2914 ComponentDefaultValue::Bool(v) => {
2915 push_scalar_field(&mut children, field_name, v);
2916 }
2917 ComponentDefaultValue::I32(v) => {
2918 push_scalar_field(&mut children, field_name, v);
2919 }
2920 ComponentDefaultValue::I64(v) => {
2921 push_scalar_field(&mut children, field_name, v);
2922 }
2923 ComponentDefaultValue::U32(v) => {
2924 push_scalar_field(&mut children, field_name, v);
2925 }
2926 ComponentDefaultValue::U64(v) => {
2927 push_scalar_field(&mut children, field_name, v);
2928 }
2929 ComponentDefaultValue::Usize(v) => {
2930 push_scalar_field(&mut children, field_name, v);
2931 }
2932 ComponentDefaultValue::F32(v) => {
2933 push_scalar_field(&mut children, field_name, v);
2934 }
2935 ComponentDefaultValue::F64(v) => {
2936 push_scalar_field(&mut children, field_name, v);
2937 }
2938 ComponentDefaultValue::ColorU(c) => {
2939 let text = alloc::format!(
2940 "{}: #{:02x}{:02x}{:02x}{:02x}",
2941 field_name,
2942 c.r,
2943 c.g,
2944 c.b,
2945 c.a
2946 );
2947 children.push(
2948 Dom::create_node(NodeType::Div)
2949 .with_children(alloc::vec![Dom::create_text(text)].into()),
2950 );
2951 }
2952 ComponentDefaultValue::ComponentInstance(ci) => {
2953 if let Some(sub_comp) =
2955 component_map.get(ci.library.as_str(), ci.component.as_str())
2956 {
2957 let sub_data = sub_comp.data_model.clone();
2958 match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
2959 ResultStyledDomRenderDomError::Ok(_styled_dom) => {
2960 let text = alloc::format!(
2963 "[{}:{}]",
2964 ci.library.as_str(),
2965 ci.component.as_str()
2966 );
2967 children.push(
2968 Dom::create_node(NodeType::Div).with_children(
2969 alloc::vec![Dom::create_text(text)].into(),
2970 ),
2971 );
2972 }
2973 ResultStyledDomRenderDomError::Err(_) => {
2974 let text = alloc::format!(
2976 "[Error rendering {}:{}]",
2977 ci.library.as_str(),
2978 ci.component.as_str()
2979 );
2980 children.push(
2981 Dom::create_node(NodeType::Div).with_children(
2982 alloc::vec![Dom::create_text(text)].into(),
2983 ),
2984 );
2985 }
2986 }
2987 } else {
2988 let text = alloc::format!(
2989 "[Unknown component {}:{}]",
2990 ci.library.as_str(),
2991 ci.component.as_str()
2992 );
2993 children.push(
2994 Dom::create_node(NodeType::Div)
2995 .with_children(alloc::vec![Dom::create_text(text)].into()),
2996 );
2997 }
2998 }
2999 ComponentDefaultValue::CallbackFnPointer(name) => {
3000 let text = alloc::format!("{}: fn({})", field_name, name.as_str());
3002 children.push(
3003 Dom::create_node(NodeType::Div)
3004 .with_children(alloc::vec![Dom::create_text(text)].into()),
3005 );
3006 }
3007 ComponentDefaultValue::Json(json_str) => {
3008 let text = alloc::format!("{}: {}", field_name, json_str.as_str());
3009 children.push(
3010 Dom::create_node(NodeType::Div)
3011 .with_children(alloc::vec![Dom::create_text(text)].into()),
3012 );
3013 }
3014 ComponentDefaultValue::None => {
3015 }
3017 }
3018 }
3019 }
3020 }
3021
3022 let mut wrapper = Dom::create_node(NodeType::Div);
3023 if !children.is_empty() {
3024 wrapper = wrapper.with_children(children.into());
3025 }
3026
3027 let css = if def.css.as_str().is_empty() {
3029 Css::empty()
3030 } else {
3031 Css::from_string(def.css.clone())
3032 };
3033
3034 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
3035 r.into()
3036}
3037
3038#[allow(clippy::too_many_lines)] #[must_use] pub fn user_defined_compile_fn(
3048 def: &ComponentDef,
3049 target: &CompileTarget,
3050 data: &ComponentDataModel,
3051 indent: usize,
3052) -> ResultStringCompileError {
3053 let tag = def.id.name.as_str();
3054 let indent_str = " ".repeat(indent * 4);
3055 let inner_indent = " ".repeat((indent + 1) * 4);
3056
3057 let r: Result<AzString, CompileError> = match target {
3058 CompileTarget::Rust => {
3059 let mut lines = Vec::new();
3060 lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3061 lines.push(alloc::format!(
3062 "{indent_str}let mut children: Vec<Dom> = Vec::new();"
3063 ));
3064
3065 for field in data.fields.as_ref() {
3066 let fname = field.name.as_str();
3067 match &field.default_value {
3068 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3069 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3070 lines.push(alloc::format!(
3071 "{inner_indent}children.push(Dom::create_text(\"{escaped}\"));"
3072 ));
3073 }
3074 OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
3075 lines.push(alloc::format!(
3076 "{inner_indent}children.push(Dom::create_text(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
3077 ));
3078 }
3079 OptionComponentDefaultValue::Some(
3080 ComponentDefaultValue::ComponentInstance(ci),
3081 ) => {
3082 let fn_name =
3083 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3084 lines.push(alloc::format!(
3085 "{}children.push({}()); // sub-component {}:{}",
3086 inner_indent,
3087 fn_name,
3088 ci.library.as_str(),
3089 ci.component.as_str()
3090 ));
3091 }
3092 _ => {
3093 lines.push(alloc::format!(
3095 "{}// field '{}': {:?}",
3096 inner_indent,
3097 fname,
3098 field.field_type
3099 ));
3100 }
3101 }
3102 }
3103
3104 lines.push(alloc::format!(
3105 "{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
3106 ));
3107 Ok(lines.join("\n").into())
3108 }
3109 CompileTarget::C => {
3110 let mut lines = Vec::new();
3111 lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
3112 lines.push(alloc::format!(
3113 "{indent_str}AzDom root = AzDom_createDiv();"
3114 ));
3115
3116 for field in data.fields.as_ref() {
3117 let fname = field.name.as_str();
3118 match &field.default_value {
3119 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3120 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3121 lines.push(alloc::format!(
3122 "{inner_indent}AzDom_addChild(&root, AzDom_createText(AZ_STR(\"{escaped}\")));"
3123 ));
3124 }
3125 OptionComponentDefaultValue::Some(
3126 ComponentDefaultValue::ComponentInstance(ci),
3127 ) => {
3128 let fn_name =
3129 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3130 lines.push(alloc::format!(
3131 "{inner_indent}AzDom_addChild(&root, {fn_name}());"
3132 ));
3133 }
3134 _ => {
3135 lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
3136 }
3137 }
3138 }
3139
3140 lines.push(alloc::format!("{indent_str}return root;"));
3141 Ok(lines.join("\n").into())
3142 }
3143 CompileTarget::Cpp => {
3144 let mut lines = Vec::new();
3145 lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3146 lines.push(alloc::format!(
3147 "{indent_str}auto root = Dom::create_div();"
3148 ));
3149
3150 for field in data.fields.as_ref() {
3151 let fname = field.name.as_str();
3152 match &field.default_value {
3153 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3154 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3155 lines.push(alloc::format!(
3156 "{inner_indent}root.add_child(Dom::create_text(String(\"{escaped}\")));"
3157 ));
3158 }
3159 OptionComponentDefaultValue::Some(
3160 ComponentDefaultValue::ComponentInstance(ci),
3161 ) => {
3162 let fn_name =
3163 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3164 lines.push(alloc::format!(
3165 "{inner_indent}root.add_child({fn_name}());"
3166 ));
3167 }
3168 _ => {
3169 lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
3170 }
3171 }
3172 }
3173
3174 lines.push(alloc::format!("{indent_str}return root;"));
3175 Ok(lines.join("\n").into())
3176 }
3177 CompileTarget::Python => {
3178 let mut lines = Vec::new();
3179 lines.push(alloc::format!("{indent_str}# Component: {tag}"));
3180 lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
3181
3182 for field in data.fields.as_ref() {
3183 let fname = field.name.as_str();
3184 match &field.default_value {
3185 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3186 let escaped = s
3187 .as_str()
3188 .replace('\\', "\\\\")
3189 .replace('"', "\\\"")
3190 .replace('\'', "\\'");
3191 lines.push(alloc::format!(
3192 "{inner_indent}root = root.with_child(Dom.create_text(\"{escaped}\"))"
3193 ));
3194 }
3195 OptionComponentDefaultValue::Some(
3196 ComponentDefaultValue::ComponentInstance(ci),
3197 ) => {
3198 let fn_name =
3199 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3200 lines.push(alloc::format!(
3201 "{inner_indent}root = root.with_child({fn_name}())"
3202 ));
3203 }
3204 _ => {
3205 lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
3206 }
3207 }
3208 }
3209
3210 lines.push(alloc::format!("{indent_str}return root"));
3211 Ok(lines.join("\n").into())
3212 }
3213 };
3214 r.into()
3215}
3216
3217fn builtin_component_def(
3229 tag: &str,
3230 display_name: &str,
3231 default_text: Option<&str>,
3232 css: &str,
3233) -> ComponentDef {
3234 let mut fields = builtin_data_model(tag);
3235 if let Some(text) = default_text {
3237 fields.push(data_field(
3238 "text",
3239 ComponentFieldType::String,
3240 Some(ComponentDefaultValue::String(AzString::from(text))),
3241 "Text content of the element",
3242 ));
3243 }
3244 let model_name = format!("{display_name}Data");
3245 ComponentDef {
3246 id: ComponentId::builtin(tag),
3247 display_name: AzString::from(display_name),
3248 description: AzString::from(format!("HTML <{tag}> element").as_str()),
3249 css: AzString::from(css),
3250 source: ComponentSource::Builtin,
3251 data_model: ComponentDataModel {
3252 name: AzString::from(model_name.as_str()),
3253 description: AzString::from(format!("Data model for <{tag}>").as_str()),
3254 fields: fields.into(),
3255 },
3256 render_fn: builtin_render_fn,
3257 compile_fn: builtin_compile_fn,
3258 render_fn_source: None.into(),
3259 compile_fn_source: None.into(),
3260 }
3261}
3262
3263fn data_field(
3265 name: &str,
3266 ft: ComponentFieldType,
3267 default: Option<ComponentDefaultValue>,
3268 description: &str,
3269) -> ComponentDataField {
3270 let required = default.is_none();
3271 ComponentDataField {
3272 name: AzString::from(name),
3273 field_type: ft,
3274 default_value: default.map_or_else(|| OptionComponentDefaultValue::None, OptionComponentDefaultValue::Some),
3275 required,
3276 description: AzString::from(description),
3277 }
3278}
3279
3280#[allow(clippy::too_many_lines)] fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
3287 use ComponentDefaultValue as D;
3288 use ComponentFieldType::{String, Bool, I32};
3289 match tag {
3290 "a" => alloc::vec![
3291 data_field(
3292 "href",
3293 String,
3294 Some(D::String(AzString::from_const_str(""))),
3295 "URL the link points to"
3296 ),
3297 data_field(
3298 "target",
3299 String,
3300 Some(D::String(AzString::from_const_str(""))),
3301 "Where to open the linked document (_blank, _self, _parent, _top)"
3302 ),
3303 data_field(
3304 "rel",
3305 String,
3306 Some(D::String(AzString::from_const_str(""))),
3307 "Relationship between current and linked document"
3308 ),
3309 ],
3310 "img" | "image" => alloc::vec![
3311 data_field("src", String, None, "URL of the image"),
3312 data_field(
3313 "alt",
3314 String,
3315 Some(D::String(AzString::from_const_str(""))),
3316 "Alternative text for the image"
3317 ),
3318 data_field(
3319 "width",
3320 String,
3321 Some(D::String(AzString::from_const_str(""))),
3322 "Width of the image"
3323 ),
3324 data_field(
3325 "height",
3326 String,
3327 Some(D::String(AzString::from_const_str(""))),
3328 "Height of the image"
3329 ),
3330 ],
3331 "form" => alloc::vec![
3332 data_field(
3333 "action",
3334 String,
3335 Some(D::String(AzString::from_const_str(""))),
3336 "URL where form data is submitted"
3337 ),
3338 data_field(
3339 "method",
3340 String,
3341 Some(D::String(AzString::from_const_str("GET"))),
3342 "HTTP method for form submission (GET or POST)"
3343 ),
3344 ],
3345 "label" => alloc::vec![data_field(
3346 "for",
3347 String,
3348 Some(D::String(AzString::from_const_str(""))),
3349 "ID of the form element this label is for"
3350 ),],
3351 "button" => alloc::vec![
3352 data_field(
3353 "type",
3354 String,
3355 Some(D::String(AzString::from_const_str("button"))),
3356 "Button type (button, submit, reset)"
3357 ),
3358 data_field(
3359 "disabled",
3360 Bool,
3361 Some(D::Bool(false)),
3362 "Whether the button is disabled"
3363 ),
3364 ],
3365 "td" | "th" => alloc::vec![
3366 data_field(
3367 "colspan",
3368 I32,
3369 Some(D::I32(1)),
3370 "Number of columns the cell spans"
3371 ),
3372 data_field(
3373 "rowspan",
3374 I32,
3375 Some(D::I32(1)),
3376 "Number of rows the cell spans"
3377 ),
3378 ],
3379 "icon" => alloc::vec![data_field(
3380 "name",
3381 String,
3382 Some(D::String(AzString::from_const_str(""))),
3383 "Icon name"
3384 ),],
3385 "ol" => alloc::vec![
3386 data_field(
3387 "start",
3388 I32,
3389 Some(D::I32(1)),
3390 "Start value for the ordered list"
3391 ),
3392 data_field(
3393 "type",
3394 String,
3395 Some(D::String(AzString::from_const_str("1"))),
3396 "Numbering type (1, A, a, I, i)"
3397 ),
3398 ],
3399 "input" => alloc::vec![
3401 data_field(
3402 "type",
3403 String,
3404 Some(D::String(AzString::from_const_str("text"))),
3405 "Input type (text, password, email, number, checkbox, radio, etc.)"
3406 ),
3407 data_field(
3408 "name",
3409 String,
3410 Some(D::String(AzString::from_const_str(""))),
3411 "Name of the input for form submission"
3412 ),
3413 data_field(
3414 "value",
3415 String,
3416 Some(D::String(AzString::from_const_str(""))),
3417 "Current value of the input"
3418 ),
3419 data_field(
3420 "placeholder",
3421 String,
3422 Some(D::String(AzString::from_const_str(""))),
3423 "Placeholder text"
3424 ),
3425 data_field(
3426 "disabled",
3427 Bool,
3428 Some(D::Bool(false)),
3429 "Whether the input is disabled"
3430 ),
3431 data_field(
3432 "required",
3433 Bool,
3434 Some(D::Bool(false)),
3435 "Whether the input is required"
3436 ),
3437 data_field(
3438 "readonly",
3439 Bool,
3440 Some(D::Bool(false)),
3441 "Whether the input is read-only"
3442 ),
3443 data_field(
3444 "checked",
3445 Bool,
3446 Some(D::Bool(false)),
3447 "Whether the checkbox/radio is checked"
3448 ),
3449 data_field(
3450 "min",
3451 String,
3452 Some(D::String(AzString::from_const_str(""))),
3453 "Minimum value (for number, range, date)"
3454 ),
3455 data_field(
3456 "max",
3457 String,
3458 Some(D::String(AzString::from_const_str(""))),
3459 "Maximum value (for number, range, date)"
3460 ),
3461 data_field(
3462 "step",
3463 String,
3464 Some(D::String(AzString::from_const_str(""))),
3465 "Step increment (for number, range)"
3466 ),
3467 data_field(
3468 "pattern",
3469 String,
3470 Some(D::String(AzString::from_const_str(""))),
3471 "Regex pattern for validation"
3472 ),
3473 data_field(
3474 "maxlength",
3475 String,
3476 Some(D::String(AzString::from_const_str(""))),
3477 "Maximum number of characters"
3478 ),
3479 ],
3480 "select" => alloc::vec![
3481 data_field(
3482 "name",
3483 String,
3484 Some(D::String(AzString::from_const_str(""))),
3485 "Name for form submission"
3486 ),
3487 data_field(
3488 "multiple",
3489 Bool,
3490 Some(D::Bool(false)),
3491 "Whether multiple options can be selected"
3492 ),
3493 data_field(
3494 "disabled",
3495 Bool,
3496 Some(D::Bool(false)),
3497 "Whether the select is disabled"
3498 ),
3499 data_field(
3500 "required",
3501 Bool,
3502 Some(D::Bool(false)),
3503 "Whether selection is required"
3504 ),
3505 data_field(
3506 "size",
3507 String,
3508 Some(D::String(AzString::from_const_str(""))),
3509 "Number of visible options"
3510 ),
3511 ],
3512 "option" => alloc::vec![
3513 data_field(
3514 "value",
3515 String,
3516 Some(D::String(AzString::from_const_str(""))),
3517 "Value submitted with the form"
3518 ),
3519 data_field(
3520 "selected",
3521 Bool,
3522 Some(D::Bool(false)),
3523 "Whether this option is selected"
3524 ),
3525 data_field(
3526 "disabled",
3527 Bool,
3528 Some(D::Bool(false)),
3529 "Whether this option is disabled"
3530 ),
3531 ],
3532 "optgroup" => alloc::vec![
3533 data_field(
3534 "label",
3535 String,
3536 Some(D::String(AzString::from_const_str(""))),
3537 "Label for the option group"
3538 ),
3539 data_field(
3540 "disabled",
3541 Bool,
3542 Some(D::Bool(false)),
3543 "Whether the group is disabled"
3544 ),
3545 ],
3546 "textarea" => alloc::vec![
3547 data_field(
3548 "name",
3549 String,
3550 Some(D::String(AzString::from_const_str(""))),
3551 "Name for form submission"
3552 ),
3553 data_field(
3554 "placeholder",
3555 String,
3556 Some(D::String(AzString::from_const_str(""))),
3557 "Placeholder text"
3558 ),
3559 data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
3560 data_field(
3561 "cols",
3562 I32,
3563 Some(D::I32(20)),
3564 "Visible width in average character widths"
3565 ),
3566 data_field(
3567 "disabled",
3568 Bool,
3569 Some(D::Bool(false)),
3570 "Whether the textarea is disabled"
3571 ),
3572 data_field(
3573 "required",
3574 Bool,
3575 Some(D::Bool(false)),
3576 "Whether content is required"
3577 ),
3578 data_field(
3579 "readonly",
3580 Bool,
3581 Some(D::Bool(false)),
3582 "Whether the textarea is read-only"
3583 ),
3584 data_field(
3585 "maxlength",
3586 String,
3587 Some(D::String(AzString::from_const_str(""))),
3588 "Maximum number of characters"
3589 ),
3590 ],
3591 "fieldset" => alloc::vec![data_field(
3592 "disabled",
3593 Bool,
3594 Some(D::Bool(false)),
3595 "Whether all controls in the fieldset are disabled"
3596 ),],
3597 "output" => alloc::vec![
3598 data_field(
3599 "for",
3600 String,
3601 Some(D::String(AzString::from_const_str(""))),
3602 "IDs of elements that contributed to the output"
3603 ),
3604 data_field(
3605 "name",
3606 String,
3607 Some(D::String(AzString::from_const_str(""))),
3608 "Name for form submission"
3609 ),
3610 ],
3611 "progress" => alloc::vec![
3612 data_field(
3613 "value",
3614 String,
3615 Some(D::String(AzString::from_const_str(""))),
3616 "Current progress value"
3617 ),
3618 data_field(
3619 "max",
3620 String,
3621 Some(D::String(AzString::from_const_str("1"))),
3622 "Maximum value"
3623 ),
3624 ],
3625 "meter" => alloc::vec![
3626 data_field(
3627 "value",
3628 String,
3629 Some(D::String(AzString::from_const_str(""))),
3630 "Current value"
3631 ),
3632 data_field(
3633 "min",
3634 String,
3635 Some(D::String(AzString::from_const_str("0"))),
3636 "Minimum value"
3637 ),
3638 data_field(
3639 "max",
3640 String,
3641 Some(D::String(AzString::from_const_str("1"))),
3642 "Maximum value"
3643 ),
3644 data_field(
3645 "low",
3646 String,
3647 Some(D::String(AzString::from_const_str(""))),
3648 "Low threshold"
3649 ),
3650 data_field(
3651 "high",
3652 String,
3653 Some(D::String(AzString::from_const_str(""))),
3654 "High threshold"
3655 ),
3656 data_field(
3657 "optimum",
3658 String,
3659 Some(D::String(AzString::from_const_str(""))),
3660 "Optimum value"
3661 ),
3662 ],
3663 "details" => alloc::vec![data_field(
3665 "open",
3666 Bool,
3667 Some(D::Bool(false)),
3668 "Whether the details are visible"
3669 ),],
3670 "dialog" => alloc::vec![data_field(
3671 "open",
3672 Bool,
3673 Some(D::Bool(false)),
3674 "Whether the dialog is active and can be interacted with"
3675 ),],
3676 "audio" | "video" => alloc::vec![
3678 data_field(
3679 "src",
3680 String,
3681 Some(D::String(AzString::from_const_str(""))),
3682 "URL of the media resource"
3683 ),
3684 data_field(
3685 "controls",
3686 Bool,
3687 Some(D::Bool(false)),
3688 "Whether to show playback controls"
3689 ),
3690 data_field(
3691 "autoplay",
3692 Bool,
3693 Some(D::Bool(false)),
3694 "Whether to start playing automatically"
3695 ),
3696 data_field(
3697 "loop",
3698 Bool,
3699 Some(D::Bool(false)),
3700 "Whether to loop playback"
3701 ),
3702 data_field(
3703 "muted",
3704 Bool,
3705 Some(D::Bool(false)),
3706 "Whether audio is muted"
3707 ),
3708 data_field(
3709 "preload",
3710 String,
3711 Some(D::String(AzString::from_const_str("auto"))),
3712 "Preload hint (none, metadata, auto)"
3713 ),
3714 ],
3715 "source" => alloc::vec![
3716 data_field("src", String, None, "URL of the media resource"),
3717 data_field(
3718 "type",
3719 String,
3720 Some(D::String(AzString::from_const_str(""))),
3721 "MIME type of the resource"
3722 ),
3723 ],
3724 "track" => alloc::vec![
3725 data_field("src", String, None, "URL of the track file"),
3726 data_field(
3727 "kind",
3728 String,
3729 Some(D::String(AzString::from_const_str("subtitles"))),
3730 "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"
3731 ),
3732 data_field(
3733 "srclang",
3734 String,
3735 Some(D::String(AzString::from_const_str(""))),
3736 "Language of the track text"
3737 ),
3738 data_field(
3739 "label",
3740 String,
3741 Some(D::String(AzString::from_const_str(""))),
3742 "User-readable title for the track"
3743 ),
3744 data_field(
3745 "default",
3746 Bool,
3747 Some(D::Bool(false)),
3748 "Whether this is the default track"
3749 ),
3750 ],
3751 "canvas" => alloc::vec![
3752 data_field(
3753 "width",
3754 String,
3755 Some(D::String(AzString::from_const_str("300"))),
3756 "Width of the canvas in pixels"
3757 ),
3758 data_field(
3759 "height",
3760 String,
3761 Some(D::String(AzString::from_const_str("150"))),
3762 "Height of the canvas in pixels"
3763 ),
3764 ],
3765 "embed" => alloc::vec![
3766 data_field("src", String, None, "URL of the resource to embed"),
3767 data_field(
3768 "type",
3769 String,
3770 Some(D::String(AzString::from_const_str(""))),
3771 "MIME type of the embedded content"
3772 ),
3773 data_field(
3774 "width",
3775 String,
3776 Some(D::String(AzString::from_const_str(""))),
3777 "Width"
3778 ),
3779 data_field(
3780 "height",
3781 String,
3782 Some(D::String(AzString::from_const_str(""))),
3783 "Height"
3784 ),
3785 ],
3786 "object" => alloc::vec![
3787 data_field(
3788 "data",
3789 String,
3790 Some(D::String(AzString::from_const_str(""))),
3791 "URL of the resource"
3792 ),
3793 data_field(
3794 "type",
3795 String,
3796 Some(D::String(AzString::from_const_str(""))),
3797 "MIME type of the resource"
3798 ),
3799 data_field(
3800 "width",
3801 String,
3802 Some(D::String(AzString::from_const_str(""))),
3803 "Width"
3804 ),
3805 data_field(
3806 "height",
3807 String,
3808 Some(D::String(AzString::from_const_str(""))),
3809 "Height"
3810 ),
3811 ],
3812 "param" => alloc::vec![
3813 data_field("name", String, None, "Name of the parameter"),
3814 data_field(
3815 "value",
3816 String,
3817 Some(D::String(AzString::from_const_str(""))),
3818 "Value of the parameter"
3819 ),
3820 ],
3821 "area" => alloc::vec![
3822 data_field(
3823 "shape",
3824 String,
3825 Some(D::String(AzString::from_const_str("default"))),
3826 "Shape of the area (default, rect, circle, poly)"
3827 ),
3828 data_field(
3829 "coords",
3830 String,
3831 Some(D::String(AzString::from_const_str(""))),
3832 "Coordinates of the area"
3833 ),
3834 data_field(
3835 "href",
3836 String,
3837 Some(D::String(AzString::from_const_str(""))),
3838 "URL for the area link"
3839 ),
3840 data_field(
3841 "alt",
3842 String,
3843 Some(D::String(AzString::from_const_str(""))),
3844 "Alternative text"
3845 ),
3846 data_field(
3847 "target",
3848 String,
3849 Some(D::String(AzString::from_const_str(""))),
3850 "Where to open the linked document"
3851 ),
3852 ],
3853 "map" => alloc::vec![data_field(
3854 "name",
3855 String,
3856 None,
3857 "Name of the image map (referenced by usemap)"
3858 ),],
3859 "time" => alloc::vec![data_field(
3861 "datetime",
3862 String,
3863 Some(D::String(AzString::from_const_str(""))),
3864 "Machine-readable date/time value"
3865 ),],
3866 "data" => alloc::vec![data_field(
3867 "value",
3868 String,
3869 Some(D::String(AzString::from_const_str(""))),
3870 "Machine-readable value"
3871 ),],
3872 "abbr" | "acronym" | "dfn" => alloc::vec![data_field(
3873 "title",
3874 String,
3875 Some(D::String(AzString::from_const_str(""))),
3876 "Full expansion or definition"
3877 ),],
3878 "q" | "blockquote" => alloc::vec![data_field(
3879 "cite",
3880 String,
3881 Some(D::String(AzString::from_const_str(""))),
3882 "URL of the source of the quotation"
3883 ),],
3884 "del" | "ins" => alloc::vec![
3885 data_field(
3886 "cite",
3887 String,
3888 Some(D::String(AzString::from_const_str(""))),
3889 "URL explaining the change"
3890 ),
3891 data_field(
3892 "datetime",
3893 String,
3894 Some(D::String(AzString::from_const_str(""))),
3895 "Date/time of the change"
3896 ),
3897 ],
3898 "bdo" => alloc::vec![data_field(
3899 "dir",
3900 String,
3901 Some(D::String(AzString::from_const_str("ltr"))),
3902 "Text direction (ltr, rtl)"
3903 ),],
3904 "col" | "colgroup" => alloc::vec![data_field(
3905 "span",
3906 I32,
3907 Some(D::I32(1)),
3908 "Number of columns the element spans"
3909 ),],
3910 "meta" => alloc::vec![
3912 data_field(
3913 "name",
3914 String,
3915 Some(D::String(AzString::from_const_str(""))),
3916 "Metadata name"
3917 ),
3918 data_field(
3919 "content",
3920 String,
3921 Some(D::String(AzString::from_const_str(""))),
3922 "Metadata value"
3923 ),
3924 data_field(
3925 "charset",
3926 String,
3927 Some(D::String(AzString::from_const_str(""))),
3928 "Character encoding"
3929 ),
3930 data_field(
3931 "http-equiv",
3932 String,
3933 Some(D::String(AzString::from_const_str(""))),
3934 "HTTP header equivalent"
3935 ),
3936 ],
3937 "link" => alloc::vec![
3938 data_field("rel", String, None, "Relationship type"),
3939 data_field(
3940 "href",
3941 String,
3942 Some(D::String(AzString::from_const_str(""))),
3943 "URL of the linked resource"
3944 ),
3945 data_field(
3946 "type",
3947 String,
3948 Some(D::String(AzString::from_const_str(""))),
3949 "MIME type of the linked resource"
3950 ),
3951 ],
3952 "script" => alloc::vec![
3953 data_field(
3954 "src",
3955 String,
3956 Some(D::String(AzString::from_const_str(""))),
3957 "URL of external script"
3958 ),
3959 data_field(
3960 "type",
3961 String,
3962 Some(D::String(AzString::from_const_str(""))),
3963 "MIME type or module"
3964 ),
3965 data_field(
3966 "async",
3967 Bool,
3968 Some(D::Bool(false)),
3969 "Execute asynchronously"
3970 ),
3971 data_field(
3972 "defer",
3973 Bool,
3974 Some(D::Bool(false)),
3975 "Defer execution until page load"
3976 ),
3977 ],
3978 "style" => alloc::vec![data_field(
3979 "type",
3980 String,
3981 Some(D::String(AzString::from_const_str("text/css"))),
3982 "MIME type of the style sheet"
3983 ),],
3984 "base" => alloc::vec![
3985 data_field(
3986 "href",
3987 String,
3988 Some(D::String(AzString::from_const_str(""))),
3989 "Base URL for relative URLs"
3990 ),
3991 data_field(
3992 "target",
3993 String,
3994 Some(D::String(AzString::from_const_str(""))),
3995 "Default target for hyperlinks"
3996 ),
3997 ],
3998 _ => alloc::vec![],
3999 }
4000}
4001
4002impl Default for ComponentMap {
4003 fn default() -> Self {
4009 Self {
4010 libraries: ComponentLibraryVec::from_const_slice(&[]),
4011 }
4012 }
4013}
4014
4015impl ComponentMap {
4016 #[must_use] pub fn create() -> Self {
4017 Self::default()
4018 }
4019
4020 #[must_use] pub fn with_builtin() -> Self {
4022 Self {
4023 libraries: alloc::vec![register_builtin_components()].into(),
4024 }
4025 }
4026
4027 #[must_use] pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
4033 Self {
4034 libraries: libs.clone(),
4035 }
4036 }
4037}
4038
4039fn xml_attrs_to_data_model(
4054 base_model: &ComponentDataModel,
4055 xml_attributes: &XmlAttributeMap,
4056 text_content: Option<&str>,
4057) -> ComponentDataModel {
4058 let mut model = base_model.clone();
4059
4060 let mut fields_vec = core::mem::replace(
4062 &mut model.fields,
4063 ComponentDataFieldVec::from_const_slice(&[]),
4064 )
4065 .into_library_owned_vec();
4066
4067 for field in &mut fields_vec {
4068 if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
4069 field.default_value = OptionComponentDefaultValue::Some(ComponentDefaultValue::String(
4071 attr_value.clone(),
4072 ));
4073 }
4074 }
4075
4076 model.fields = ComponentDataFieldVec::from_vec(fields_vec);
4077
4078 if let Some(text) = text_content {
4080 let prepared = prepare_string(text);
4081 if !prepared.is_empty() {
4082 model = model.with_default(
4083 "text",
4084 ComponentDefaultValue::String(AzString::from(prepared.as_str())),
4085 );
4086 }
4087 }
4088
4089 model
4090}
4091
4092fn builtin_if_component() -> ComponentDef {
4099 ComponentDef {
4100 id: ComponentId::builtin("if"),
4101 display_name: AzString::from_const_str("If"),
4102 description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
4103 css: AzString::from_const_str(""),
4104 source: ComponentSource::Builtin,
4105 data_model: ComponentDataModel {
4106 name: AzString::from_const_str("IfData"),
4107 description: AzString::from_const_str("Data for conditional rendering"),
4108 fields: alloc::vec![
4109 data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
4110 ].into(),
4111 },
4112 render_fn: builtin_if_render_fn,
4113 compile_fn: builtin_if_compile_fn,
4114 render_fn_source: None.into(),
4115 compile_fn_source: None.into(),
4116 }
4117}
4118
4119fn builtin_if_render_fn(
4120 _comp: &ComponentDef,
4121 data_model: &ComponentDataModel,
4122 _component_map: &ComponentMap,
4123) -> ResultStyledDomRenderDomError {
4124 let condition = data_model
4126 .fields
4127 .iter()
4128 .find(|f| f.name.as_str() == "condition")
4129 .and_then(|f| match &f.default_value {
4130 OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b),
4131 _ => None,
4132 })
4133 .unwrap_or(false);
4134
4135 let label = if condition {
4136 "if: true (then branch)"
4137 } else {
4138 "if: false (else branch)"
4139 };
4140 let mut dom =
4141 Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4142 let css = Css::empty();
4143 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4144}
4145
4146fn builtin_if_compile_fn(
4147 _comp: &ComponentDef,
4148 target: &CompileTarget,
4149 _data: &ComponentDataModel,
4150 _indent: usize,
4151) -> ResultStringCompileError {
4152 match target {
4153 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4154 "if data.condition {\n // then branch\n Dom::create_div()\n} else {\n // else branch\n Dom::create_div()\n}"
4155 )),
4156 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4157 "if (data.condition) {\n // then branch\n AzDom_createDiv();\n} else {\n // else branch\n AzDom_createDiv();\n}"
4158 )),
4159 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4160 "if (data.condition) {\n // then branch\n Dom::create_div();\n} else {\n // else branch\n Dom::create_div();\n}"
4161 )),
4162 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4163 "if data.condition:\n # then branch\n Dom.create_div()\nelse:\n # else branch\n Dom.create_div()"
4164 )),
4165 }
4166}
4167
4168fn builtin_for_component() -> ComponentDef {
4171 ComponentDef {
4172 id: ComponentId::builtin("for"),
4173 display_name: AzString::from_const_str("For Loop"),
4174 description: AzString::from_const_str(
4175 "Iterative rendering: repeats children 'count' times.",
4176 ),
4177 css: AzString::from_const_str(""),
4178 source: ComponentSource::Builtin,
4179 data_model: ComponentDataModel {
4180 name: AzString::from_const_str("ForData"),
4181 description: AzString::from_const_str("Data for iterative rendering"),
4182 fields: alloc::vec![data_field(
4183 "count",
4184 ComponentFieldType::U32,
4185 Some(ComponentDefaultValue::U32(3)),
4186 "Number of iterations"
4187 ),]
4188 .into(),
4189 },
4190 render_fn: builtin_for_render_fn,
4191 compile_fn: builtin_for_compile_fn,
4192 render_fn_source: None.into(),
4193 compile_fn_source: None.into(),
4194 }
4195}
4196
4197fn builtin_for_render_fn(
4198 _comp: &ComponentDef,
4199 data_model: &ComponentDataModel,
4200 _component_map: &ComponentMap,
4201) -> ResultStyledDomRenderDomError {
4202 let count = data_model
4203 .fields
4204 .iter()
4205 .find(|f| f.name.as_str() == "count")
4206 .and_then(|f| match &f.default_value {
4207 OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n),
4208 _ => None,
4209 })
4210 .unwrap_or(3);
4211
4212 let mut items: Vec<Dom> = Vec::new();
4213 for i in 0..count {
4214 items.push(
4215 Dom::create_node(NodeType::Div)
4216 .with_children(alloc::vec![Dom::create_text(alloc::format!("Item {i}"))].into()),
4217 );
4218 }
4219 let mut dom = Dom::create_node(NodeType::Div).with_children(items.into());
4220 let css = Css::empty();
4221 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4222}
4223
4224fn builtin_for_compile_fn(
4225 _comp: &ComponentDef,
4226 target: &CompileTarget,
4227 _data: &ComponentDataModel,
4228 _indent: usize,
4229) -> ResultStringCompileError {
4230 match target {
4231 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4232 "let mut children = Vec::new();\nfor i in 0..data.count {\n children.push(Dom::create_div());\n}\nDom::create_div().with_children(children)"
4233 )),
4234 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4235 "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n AzDom_addChild(&container, AzDom_createDiv());\n}"
4236 )),
4237 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4238 "auto container = Dom::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n container.add_child(Dom::create_div());\n}"
4239 )),
4240 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4241 "container = Dom.create_div()\nfor i in range(data.count):\n container = container.with_child(Dom.create_div())"
4242 )),
4243 }
4244}
4245
4246fn builtin_map_component() -> ComponentDef {
4249 ComponentDef {
4250 id: ComponentId::builtin("map"),
4251 display_name: AzString::from_const_str("Map"),
4252 description: AzString::from_const_str(
4253 "Map data to DOM: applies a template to each item in a collection.",
4254 ),
4255 css: AzString::from_const_str(""),
4256 source: ComponentSource::Builtin,
4257 data_model: ComponentDataModel {
4258 name: AzString::from_const_str("MapData"),
4259 description: AzString::from_const_str("Data for map rendering"),
4260 fields: alloc::vec![data_field(
4261 "data_json",
4262 ComponentFieldType::String,
4263 Some(ComponentDefaultValue::String(AzString::from_const_str(
4264 "[]"
4265 ))),
4266 "JSON array of items to map over"
4267 ),]
4268 .into(),
4269 },
4270 render_fn: builtin_map_render_fn,
4271 compile_fn: builtin_map_compile_fn,
4272 render_fn_source: None.into(),
4273 compile_fn_source: None.into(),
4274 }
4275}
4276
4277fn builtin_map_render_fn(
4278 _comp: &ComponentDef,
4279 data_model: &ComponentDataModel,
4280 _component_map: &ComponentMap,
4281) -> ResultStyledDomRenderDomError {
4282 let data_str = data_model
4284 .fields
4285 .iter()
4286 .find(|f| f.name.as_str() == "data_json")
4287 .and_then(|f| match &f.default_value {
4288 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
4289 Some(s.as_str().to_string())
4290 }
4291 _ => None,
4292 })
4293 .unwrap_or_else(|| "[]".to_string());
4294
4295 let label = alloc::format!("map: data_json={data_str}");
4296 let mut dom =
4297 Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4298 let css = Css::empty();
4299 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4300}
4301
4302fn builtin_map_compile_fn(
4303 _comp: &ComponentDef,
4304 target: &CompileTarget,
4305 _data: &ComponentDataModel,
4306 _indent: usize,
4307) -> ResultStringCompileError {
4308 match target {
4309 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4310 "let items: Vec<serde_json::Value> = serde_json::from_str(&data.data_json).unwrap_or_default();\nlet children: Vec<Dom> = items.iter().map(|item| {\n Dom::create_div() // map template\n}).collect();\nDom::create_div().with_children(children)"
4311 )),
4312 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4313 "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
4314 )),
4315 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4316 "// Parse data.data_json and map each item\nauto container = Dom::create_div();\n// TODO: iterate parsed JSON array"
4317 )),
4318 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4319 "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n container = container.with_child(Dom.create_div())"
4320 )),
4321 }
4322}
4323
4324#[allow(clippy::too_many_lines)] #[must_use] pub extern "C" fn register_builtin_components() -> ComponentLibrary {
4334 ComponentLibrary {
4335 name: AzString::from_const_str("builtin"),
4336 version: AzString::from_const_str("1.0.0"),
4337 description: AzString::from_const_str("Built-in HTML elements"),
4338 exportable: false,
4339 modifiable: false,
4340 data_models: Vec::new().into(),
4341 enum_models: Vec::new().into(),
4342 components: alloc::vec![
4343 builtin_component_def("html", "HTML", None, ""),
4345 builtin_component_def("head", "Head", None, ""),
4346 builtin_component_def("title", "Title", Some(""), ""),
4347 builtin_component_def("body", "Body", None, ""),
4348 builtin_component_def("div", "Div", None, ""),
4350 builtin_component_def("header", "Header", None, ""),
4351 builtin_component_def("footer", "Footer", None, ""),
4352 builtin_component_def("section", "Section", None, ""),
4353 builtin_component_def("article", "Article", None, ""),
4354 builtin_component_def("aside", "Aside", None, ""),
4355 builtin_component_def("nav", "Nav", None, ""),
4356 builtin_component_def("main", "Main", None, ""),
4357 builtin_component_def("figure", "Figure", None, ""),
4358 builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
4359 builtin_component_def("address", "Address", Some(""), ""),
4360 builtin_component_def("details", "Details", None, ""),
4361 builtin_component_def("summary", "Summary", Some("Details"), ""),
4362 builtin_component_def("dialog", "Dialog", None, ""),
4363 builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
4365 builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
4366 builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
4367 builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
4368 builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
4369 builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
4370 builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
4372 builtin_component_def("span", "Span", Some(""), ""),
4373 builtin_component_def("pre", "Preformatted", Some(""), ""),
4374 builtin_component_def("code", "Code", Some(""), ""),
4375 builtin_component_def("blockquote", "Blockquote", Some(""), ""),
4376 builtin_component_def("br", "Line Break", None, ""),
4377 builtin_component_def("hr", "Horizontal Rule", None, ""),
4378 builtin_component_def("icon", "Icon", Some(""), ""),
4379 builtin_component_def("ul", "Unordered List", None, ""),
4381 builtin_component_def("ol", "Ordered List", None, ""),
4382 builtin_component_def("li", "List Item", Some("List item"), ""),
4383 builtin_component_def("dl", "Description List", None, ""),
4384 builtin_component_def("dt", "Description Term", Some(""), ""),
4385 builtin_component_def("dd", "Description Details", Some(""), ""),
4386 builtin_component_def("menu", "Menu", None, ""),
4387 builtin_component_def("menuitem", "Menu Item", Some(""), ""),
4388 builtin_component_def("dir", "Directory List", None, ""),
4389 builtin_component_def("table", "Table", None, ""),
4391 builtin_component_def("caption", "Table Caption", Some(""), ""),
4392 builtin_component_def("thead", "Table Head", None, ""),
4393 builtin_component_def("tbody", "Table Body", None, ""),
4394 builtin_component_def("tfoot", "Table Foot", None, ""),
4395 builtin_component_def("tr", "Table Row", None, ""),
4396 builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
4397 builtin_component_def("td", "Table Data Cell", Some(""), ""),
4398 builtin_component_def("colgroup", "Column Group", None, ""),
4399 builtin_component_def("col", "Column", None, ""),
4400 builtin_component_def("a", "Link", Some("Link text"), ""),
4402 builtin_component_def("strong", "Strong", Some(""), ""),
4403 builtin_component_def("em", "Emphasis", Some(""), ""),
4404 builtin_component_def("b", "Bold", Some(""), ""),
4405 builtin_component_def("i", "Italic", Some(""), ""),
4406 builtin_component_def("u", "Underline", Some(""), ""),
4407 builtin_component_def("s", "Strikethrough", Some(""), ""),
4408 builtin_component_def("small", "Small", Some(""), ""),
4409 builtin_component_def("mark", "Mark", Some(""), ""),
4410 builtin_component_def("del", "Deleted Text", Some(""), ""),
4411 builtin_component_def("ins", "Inserted Text", Some(""), ""),
4412 builtin_component_def("sub", "Subscript", Some(""), ""),
4413 builtin_component_def("sup", "Superscript", Some(""), ""),
4414 builtin_component_def("samp", "Sample Output", Some(""), ""),
4415 builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
4416 builtin_component_def("var", "Variable", Some(""), ""),
4417 builtin_component_def("cite", "Citation", Some(""), ""),
4418 builtin_component_def("dfn", "Definition", Some(""), ""),
4419 builtin_component_def("abbr", "Abbreviation", Some(""), ""),
4420 builtin_component_def("acronym", "Acronym", Some(""), ""),
4421 builtin_component_def("q", "Inline Quote", Some(""), ""),
4422 builtin_component_def("time", "Time", Some(""), ""),
4423 builtin_component_def("big", "Big", Some(""), ""),
4424 builtin_component_def("bdo", "BiDi Override", Some(""), ""),
4425 builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
4426 builtin_component_def("wbr", "Word Break Opportunity", None, ""),
4427 builtin_component_def("ruby", "Ruby Annotation", None, ""),
4428 builtin_component_def("rt", "Ruby Text", Some(""), ""),
4429 builtin_component_def("rtc", "Ruby Text Container", None, ""),
4430 builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
4431 builtin_component_def("data", "Data", Some(""), ""),
4432 builtin_component_def("form", "Form", None, ""),
4434 builtin_component_def("fieldset", "Field Set", None, ""),
4435 builtin_component_def("legend", "Legend", Some("Legend"), ""),
4436 builtin_component_def("label", "Label", Some("Label"), ""),
4437 builtin_component_def("input", "Input", None, ""),
4438 builtin_component_def("button", "Button", Some("Button text"), ""),
4439 builtin_component_def("select", "Select", None, ""),
4440 builtin_component_def("optgroup", "Option Group", None, ""),
4441 builtin_component_def("option", "Option", Some(""), ""),
4442 builtin_component_def("textarea", "Text Area", Some(""), ""),
4443 builtin_component_def("output", "Output", Some(""), ""),
4444 builtin_component_def("progress", "Progress", None, ""),
4445 builtin_component_def("meter", "Meter", None, ""),
4446 builtin_component_def("datalist", "Data List", None, ""),
4447 builtin_component_def("canvas", "Canvas", None, ""),
4449 builtin_component_def("object", "Object", None, ""),
4450 builtin_component_def("param", "Parameter", None, ""),
4451 builtin_component_def("embed", "Embed", None, ""),
4452 builtin_component_def("audio", "Audio", None, ""),
4453 builtin_component_def("video", "Video", None, ""),
4454 builtin_component_def("source", "Source", None, ""),
4455 builtin_component_def("track", "Track", None, ""),
4456 builtin_component_def("map", "Image Map", None, ""),
4457 builtin_component_def("area", "Map Area", None, ""),
4458 builtin_component_def("svg", "SVG", None, ""),
4459 builtin_component_def("meta", "Meta", None, ""),
4461 builtin_component_def("link", "Link (Resource)", None, ""),
4462 builtin_component_def("script", "Script", Some(""), ""),
4463 builtin_component_def("style", "Style", Some(""), ""),
4464 builtin_component_def("base", "Base URL", None, ""),
4465 builtin_if_component(),
4467 builtin_for_component(),
4468 builtin_map_component(),
4469 ]
4470 .into(),
4471 }
4472}
4473
4474#[derive(Debug, Default)]
4481pub struct DomXml {
4482 pub parsed_dom: StyledDom,
4483}
4484
4485impl DomXml {
4486 #[cfg(test)]
4504 pub fn assert_eq(self, other: StyledDom) {
4505 let mut body = Dom::create_body();
4506 let mut fixed = StyledDom::create(&mut body, Css::empty());
4507 fixed.append_child(other);
4508 assert!(!(self.parsed_dom != fixed),
4509 "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
4510 ----------\r\n{}\r\n",
4511 self.parsed_dom.get_html_string("", "", true),
4512 fixed.get_html_string("", "", true)
4513 );
4514 }
4515
4516 #[must_use] pub fn into_styled_dom(self) -> StyledDom {
4517 self.into()
4518 }
4519}
4520
4521impl From<DomXml> for StyledDom {
4522 fn from(val: DomXml) -> Self {
4523 val.parsed_dom
4524 }
4525}
4526
4527#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4529#[repr(C, u8)]
4530pub enum XmlNodeChild {
4531 Text(AzString),
4533 Element(XmlNode),
4535}
4536
4537impl_option!(
4538 XmlNodeChild,
4539 OptionXmlNodeChild,
4540 copy = false,
4541 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4542);
4543
4544impl XmlNodeChild {
4545 #[must_use] pub fn as_text(&self) -> Option<&str> {
4547 match self {
4548 Self::Text(s) => Some(s.as_str()),
4549 Self::Element(_) => None,
4550 }
4551 }
4552
4553 #[must_use] pub const fn as_element(&self) -> Option<&XmlNode> {
4555 match self {
4556 Self::Text(_) => None,
4557 Self::Element(node) => Some(node),
4558 }
4559 }
4560
4561 pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
4563 match self {
4564 Self::Text(_) => None,
4565 Self::Element(node) => Some(node),
4566 }
4567 }
4568}
4569
4570impl_vec!(
4571 XmlNodeChild,
4572 XmlNodeChildVec,
4573 XmlNodeChildVecDestructor,
4574 XmlNodeChildVecDestructorType,
4575 XmlNodeChildVecSlice,
4576 OptionXmlNodeChild
4577);
4578impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
4579impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
4580impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
4581impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
4582impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
4583impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
4584impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
4585impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
4586
4587#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4589#[repr(C)]
4590pub struct XmlNode {
4591 pub node_type: XmlTagName,
4593 pub attributes: XmlAttributeMap,
4595 pub children: XmlNodeChildVec,
4597}
4598
4599impl_option!(
4600 XmlNode,
4601 OptionXmlNode,
4602 copy = false,
4603 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4604);
4605
4606impl XmlNode {
4607 pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
4608 Self {
4609 node_type: node_type.into(),
4610 ..Default::default()
4611 }
4612 }
4613 #[must_use] pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
4614 Self {
4615 children: v.into(),
4616 ..self
4617 }
4618 }
4619
4620 #[must_use] pub fn get_text_content(&self) -> String {
4622 self.children
4623 .as_ref()
4624 .iter()
4625 .filter_map(|child| child.as_text())
4626 .collect::<Vec<_>>()
4627 .join("")
4628 }
4629
4630 #[must_use] pub fn has_only_text_children(&self) -> bool {
4632 self.children
4633 .as_ref()
4634 .iter()
4635 .all(|child| matches!(child, XmlNodeChild::Text(_)))
4636 }
4637}
4638
4639impl_vec!(
4640 XmlNode,
4641 XmlNodeVec,
4642 XmlNodeVecDestructor,
4643 XmlNodeVecDestructorType,
4644 XmlNodeVecSlice,
4645 OptionXmlNode
4646);
4647impl_vec_mut!(XmlNode, XmlNodeVec);
4648impl_vec_debug!(XmlNode, XmlNodeVec);
4649impl_vec_partialeq!(XmlNode, XmlNodeVec);
4650impl_vec_eq!(XmlNode, XmlNodeVec);
4651impl_vec_partialord!(XmlNode, XmlNodeVec);
4652impl_vec_ord!(XmlNode, XmlNodeVec);
4653impl_vec_hash!(XmlNode, XmlNodeVec);
4654impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
4655
4656#[derive(Debug, Clone, PartialEq)]
4657#[repr(C, u8)]
4658pub enum DomXmlParseError {
4659 NoHtmlNode,
4661 MultipleHtmlRootNodes,
4663 NoBodyInHtml,
4665 MultipleBodyNodes,
4667 Xml(XmlError),
4672 MalformedHierarchy(MalformedHierarchyError),
4674 RenderDom(RenderDomError),
4677 Component(ComponentParseError),
4679 Css(CssParseErrorOwned),
4681}
4682
4683impl From<XmlError> for DomXmlParseError {
4684 fn from(e: XmlError) -> Self {
4685 Self::Xml(e)
4686 }
4687}
4688
4689impl From<ComponentParseError> for DomXmlParseError {
4690 fn from(e: ComponentParseError) -> Self {
4691 Self::Component(e)
4692 }
4693}
4694
4695impl From<RenderDomError> for DomXmlParseError {
4696 fn from(e: RenderDomError) -> Self {
4697 Self::RenderDom(e)
4698 }
4699}
4700
4701impl From<CssParseErrorOwned> for DomXmlParseError {
4702 fn from(e: CssParseErrorOwned) -> Self {
4703 Self::Css(e)
4704 }
4705}
4706
4707#[derive(Debug, Clone, PartialEq)]
4710#[repr(C, u8)]
4711pub enum CompileError {
4712 Dom(RenderDomError),
4713 Xml(DomXmlParseError),
4714 Css(CssParseErrorOwned),
4715}
4716
4717impl From<ComponentError> for CompileError {
4718 fn from(e: ComponentError) -> Self {
4719 Self::Dom(RenderDomError::Component(e))
4720 }
4721}
4722
4723impl From<CssParseErrorOwned> for CompileError {
4724 fn from(e: CssParseErrorOwned) -> Self {
4725 Self::Css(e)
4726 }
4727}
4728
4729impl fmt::Display for CompileError {
4730 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4731 use self::CompileError::{Dom, Xml, Css};
4732 match self {
4733 Dom(d) => write!(f, "{d}"),
4734 Xml(s) => write!(f, "{s}"),
4735 Css(s) => write!(f, "{}", s.to_shared()),
4736 }
4737 }
4738}
4739
4740impl From<RenderDomError> for CompileError {
4741 fn from(e: RenderDomError) -> Self {
4742 Self::Dom(e)
4743 }
4744}
4745
4746impl From<DomXmlParseError> for CompileError {
4747 fn from(e: DomXmlParseError) -> Self {
4748 Self::Xml(e)
4749 }
4750}
4751
4752#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4754#[repr(C)]
4755pub struct UselessFunctionArgumentError {
4756 pub component_name: AzString,
4757 pub argument_name: AzString,
4758 pub valid_args: StringVec,
4759}
4760
4761#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4762#[repr(C, u8)]
4763pub enum ComponentError {
4764 UselessFunctionArgument(UselessFunctionArgumentError),
4767 UnknownComponent(AzString),
4772}
4773
4774#[derive(Debug, Clone, PartialEq)]
4775#[repr(C, u8)]
4776pub enum RenderDomError {
4777 Component(ComponentError),
4778 CssError(CssParseErrorOwned),
4780}
4781
4782impl From<ComponentError> for RenderDomError {
4783 fn from(e: ComponentError) -> Self {
4784 Self::Component(e)
4785 }
4786}
4787
4788impl From<CssParseErrorOwned> for RenderDomError {
4789 fn from(e: CssParseErrorOwned) -> Self {
4790 Self::CssError(e)
4791 }
4792}
4793
4794#[derive(Debug, Clone, PartialEq, Eq)]
4796#[repr(C)]
4797pub struct MissingTypeError {
4798 pub arg_pos: usize,
4799 pub arg_name: AzString,
4800}
4801
4802#[derive(Debug, Clone, PartialEq, Eq)]
4804#[repr(C)]
4805pub struct WhiteSpaceInComponentNameError {
4806 pub arg_pos: usize,
4807 pub arg_name: AzString,
4808}
4809
4810#[derive(Debug, Clone, PartialEq, Eq)]
4812#[repr(C)]
4813pub struct WhiteSpaceInComponentTypeError {
4814 pub arg_pos: usize,
4815 pub arg_name: AzString,
4816 pub arg_type: AzString,
4817}
4818
4819#[derive(Debug, Clone, PartialEq)]
4820#[repr(C, u8)]
4821pub enum ComponentParseError {
4822 NotAComponent,
4824 UnnamedComponent,
4826 MissingName(usize),
4828 MissingType(MissingTypeError),
4831 WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
4834 WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
4837 CssError(CssParseErrorOwned),
4839}
4840
4841impl fmt::Display for DomXmlParseError {
4842 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4843 use self::DomXmlParseError::{NoHtmlNode, MultipleHtmlRootNodes, NoBodyInHtml, MultipleBodyNodes, Xml, MalformedHierarchy, RenderDom, Component, Css};
4844 match self {
4845 NoHtmlNode => write!(
4846 f,
4847 "No <html> node found as the root of the file - empty file?"
4848 ),
4849 MultipleHtmlRootNodes => write!(
4850 f,
4851 "Multiple <html> nodes found as the root of the file - only one root node allowed"
4852 ),
4853 NoBodyInHtml => write!(
4854 f,
4855 "No <body> node found as a direct child of an <html> node - malformed DOM \
4856 hierarchy?"
4857 ),
4858 MultipleBodyNodes => write!(
4859 f,
4860 "Multiple <body> nodes present, only one <body> node is allowed"
4861 ),
4862 Xml(e) => write!(f, "Error parsing XML: {e}"),
4863 MalformedHierarchy(e) => write!(
4864 f,
4865 "Invalid </{}> tag: expected </{}>",
4866 e.got.as_str(),
4867 e.expected.as_str()
4868 ),
4869 RenderDom(e) => write!(f, "Error rendering DOM: {e}"),
4870 Component(c) => write!(f, "Error parsing component in <head> node:\r\n{c}"),
4871 Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
4872 }
4873 }
4874}
4875
4876impl fmt::Display for ComponentParseError {
4877 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4878 use self::ComponentParseError::{NotAComponent, UnnamedComponent, MissingName, MissingType, WhiteSpaceInComponentName, WhiteSpaceInComponentType, CssError};
4879 match self {
4880 NotAComponent => write!(f, "Expected <component/> node, found no such node"),
4881 UnnamedComponent => write!(
4882 f,
4883 "Found <component/> tag with out a \"name\" attribute, component must have a name"
4884 ),
4885 MissingName(arg_pos) => write!(
4886 f,
4887 "Argument at position {arg_pos} is either empty or has no name"
4888 ),
4889 MissingType(e) => write!(
4890 f,
4891 "Argument \"{}\" at position {} doesn't have a `: type`",
4892 e.arg_name, e.arg_pos
4893 ),
4894 WhiteSpaceInComponentName(e) => {
4895 write!(
4896 f,
4897 "Missing `:` between the name and the type in argument {} (around \"{}\")",
4898 e.arg_pos, e.arg_name
4899 )
4900 }
4901 WhiteSpaceInComponentType(e) => {
4902 write!(
4903 f,
4904 "Missing `,` between two arguments (in argument {}, position {}, around \
4905 \"{}\")",
4906 e.arg_name, e.arg_pos, e.arg_type
4907 )
4908 }
4909 CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
4910 }
4911 }
4912}
4913
4914impl fmt::Display for ComponentError {
4915 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4916 use self::ComponentError::{UselessFunctionArgument, UnknownComponent};
4917 match self {
4918 UselessFunctionArgument(e) => {
4919 write!(
4920 f,
4921 "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
4922 e.component_name, e.argument_name, e.valid_args
4923 )
4924 }
4925 UnknownComponent(name) => write!(f, "Unknown component: \"{name}\""),
4926 }
4927 }
4928}
4929
4930impl fmt::Display for RenderDomError {
4931 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4932 use self::RenderDomError::{Component, CssError};
4933 match self {
4934 Component(c) => write!(f, "{c}"),
4935 CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
4936 }
4937 }
4938}
4939
4940#[allow(clippy::result_large_err)] pub fn get_html_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4947 let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
4948 if let XmlNodeChild::Element(node) = child {
4949 if node.node_type.as_str().eq_ignore_ascii_case("html") {
4953 Some(node)
4954 } else {
4955 None
4956 }
4957 } else {
4958 None
4959 }
4960 });
4961
4962 let html_node = html_node_iterator
4963 .next()
4964 .ok_or(DomXmlParseError::NoHtmlNode)?;
4965 if html_node_iterator.next().is_some() {
4966 Err(DomXmlParseError::MultipleHtmlRootNodes)
4967 } else {
4968 Ok(html_node)
4969 }
4970}
4971
4972#[allow(clippy::result_large_err)] pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4979 fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
4980 if depth > MAX_XML_NESTING_DEPTH {
4983 return None;
4984 }
4985 for child in nodes {
4986 if let XmlNodeChild::Element(node) = child {
4987 if node.node_type.as_str().eq_ignore_ascii_case("body") {
4989 return Some(node);
4990 }
4991 if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
4993 return Some(found);
4994 }
4995 }
4996 }
4997 None
4998 }
4999
5000 let direct_body = root_nodes.iter().find_map(|child| {
5002 if let XmlNodeChild::Element(node) = child {
5003 if node.node_type.as_str().eq_ignore_ascii_case("body") {
5005 Some(node)
5006 } else {
5007 None
5008 }
5009 } else {
5010 None
5011 }
5012 });
5013
5014 if let Some(body) = direct_body {
5015 return Ok(body);
5016 }
5017
5018 find_body_recursive(root_nodes, 0).ok_or(DomXmlParseError::NoBodyInHtml)
5021}
5022
5023fn find_node_by_type<'a>(root_nodes: &'a [XmlNodeChild], node_type: &str) -> Option<&'a XmlNode> {
5027 for child in root_nodes {
5029 if let XmlNodeChild::Element(node) = child {
5030 if node.node_type.as_str().eq_ignore_ascii_case(node_type) {
5032 return Some(node);
5033 }
5034 }
5035 }
5036
5037 for child in root_nodes {
5039 if let XmlNodeChild::Element(node) = child {
5040 if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
5041 return Some(found);
5042 }
5043 }
5044 }
5045
5046 None
5047}
5048
5049#[must_use] pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
5050 node.attributes
5051 .iter()
5052 .find(|n| normalize_casing(n.key.as_str()).as_str() == attribute)
5053 .map(|s| &s.value)
5054}
5055
5056#[must_use] pub fn normalize_casing(input: &str) -> String {
5058 let mut words: Vec<String> = Vec::new();
5059 let mut cur_str = Vec::new();
5060
5061 for ch in input.chars() {
5062 if ch.is_uppercase() || ch == '_' || ch == '-' {
5063 if !cur_str.is_empty() {
5064 words.push(cur_str.iter().collect());
5065 cur_str.clear();
5066 }
5067 if ch.is_uppercase() {
5068 cur_str.extend(ch.to_lowercase());
5069 }
5070 } else {
5071 cur_str.extend(ch.to_lowercase());
5072 }
5073 }
5074
5075 if !cur_str.is_empty() {
5076 words.push(cur_str.iter().collect());
5077 cur_str.clear();
5078 }
5079
5080 words.join("_")
5081}
5082
5083#[allow(trivial_casts)]
5086pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
5087 let mut hierarchy = hierarchy.to_vec();
5088 hierarchy.reverse();
5089 let Some(item) = hierarchy.pop() else {
5090 return Some(root_node);
5091 };
5092 let child = root_node.children.as_mut().get_mut(item)?;
5093 match child {
5094 XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
5095 XmlNodeChild::Text(_) => None, }
5097}
5098
5099fn get_item_internal<'a>(
5100 hierarchy: &mut Vec<usize>,
5101 root_node: &'a mut XmlNode,
5102) -> Option<&'a mut XmlNode> {
5103 if hierarchy.is_empty() {
5104 return Some(root_node);
5105 }
5106 let Some(cur_item) = hierarchy.pop() else {
5107 return Some(root_node);
5108 };
5109 let child = root_node.children.as_mut().get_mut(cur_item)?;
5110 match child {
5111 XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
5112 XmlNodeChild::Text(_) => None, }
5114}
5115
5116#[allow(clippy::result_large_err)] pub fn str_to_dom<'a>(
5123 root_nodes: &'a [XmlNodeChild],
5124 component_map: &'a ComponentMap,
5125 max_width: Option<f32>,
5126) -> Result<StyledDom, DomXmlParseError> {
5127 str_to_dom_fast(root_nodes, component_map, max_width)
5129}
5130
5131#[allow(clippy::result_large_err)] fn str_to_dom_fast<'a>(
5137 root_nodes: &'a [XmlNodeChild],
5138 component_map: &'a ComponentMap,
5139 max_width: Option<f32>,
5140) -> Result<StyledDom, DomXmlParseError> {
5141 let html_node = get_html_node(root_nodes)?;
5142 let body_node = get_body_node(html_node.children.as_ref())?;
5143
5144 let mut global_style = None;
5145
5146 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5147 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5148 let text = style_node.get_text_content();
5149 if !text.is_empty() {
5150 let parsed_css = Css::from_string(text.into());
5151 global_style = Some(parsed_css);
5152 }
5153 }
5154 }
5155
5156 render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
5157 .map_err(Into::into)
5158}
5159
5160#[allow(clippy::result_large_err)] pub fn str_to_dom_unstyled<'a>(
5173 root_nodes: &'a [XmlNodeChild],
5174 component_map: &'a ComponentMap,
5175) -> Result<Dom, DomXmlParseError> {
5176 let html_node = get_html_node(root_nodes)?;
5177 let body_node = get_body_node(html_node.children.as_ref())?;
5178
5179 let mut global_style = None;
5180
5181 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5182 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5183 let text = style_node.get_text_content();
5184 if !text.is_empty() {
5185 let parsed_css = Css::from_string(text.into());
5186 global_style = Some(parsed_css);
5187 }
5188 }
5189 }
5190
5191 let body_dom = xml_node_to_dom_fast(body_node, component_map, false, 0)
5193 .map_err(DomXmlParseError::from)?;
5194
5195 let root_node_type = body_dom.root.node_type.clone();
5197
5198 let mut full_dom = match root_node_type {
5199 NodeType::Html => body_dom,
5200 NodeType::Body => Dom::create_html().with_child(body_dom),
5201 _ => {
5202 let body_wrapper = Dom::create_body().with_child(body_dom);
5203 Dom::create_html().with_child(body_wrapper)
5204 }
5205 };
5206
5207 if let Some(css) = global_style {
5209 full_dom.css = alloc::vec![css].into();
5210 }
5211
5212 Ok(full_dom)
5213}
5214
5215#[allow(clippy::result_large_err)] pub fn str_to_rust_code<'a>(
5222 root_nodes: &'a [XmlNodeChild],
5223 imports: &str,
5224 component_map: &'a ComponentMap,
5225) -> Result<String, CompileError> {
5226 let html_node = get_html_node(root_nodes)?;
5227 let body_node = get_body_node(html_node.children.as_ref())?;
5228 let mut global_style = Css::empty();
5229
5230 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5231 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5232 let text = style_node.get_text_content();
5233 if !text.is_empty() {
5234 let parsed_css = azul_css::parser2::new_from_str(&text).0;
5235 global_style = parsed_css;
5236 }
5237 }
5238 }
5239
5240 global_style.sort_by_specificity();
5241
5242 let mut css_blocks = BTreeMap::new();
5243 let mut extra_blocks = VecContents::default();
5244 let app_source = compile_body_node_to_rust_code(
5245 body_node,
5246 component_map,
5247 &mut extra_blocks,
5248 &mut css_blocks,
5249 &global_style,
5250 CssMatcher {
5251 path: Vec::new(),
5252 indices_in_parent: vec![0],
5253 children_length: vec![body_node.children.as_ref().len()],
5254 },
5255 )?;
5256
5257 let app_source = app_source
5258 .lines()
5259 .map(|l| format!(" {l}"))
5260 .collect::<Vec<String>>()
5261 .join("\r\n");
5262
5263 let _ = (&css_blocks, &extra_blocks);
5268
5269 let main_func = "
5270
5271use azul::{
5272 app::{App, AppConfig},
5273 dom::Dom,
5274 callbacks::{RefAny, LayoutCallbackInfo},
5275 window::WindowCreateOptions,
5276};
5277
5278struct Data { }
5279
5280extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
5281 crate::ui::render()
5282}
5283
5284fn main() {
5285 let config = AppConfig::create();
5286 let app = App::create(RefAny::new(Data { }), config);
5287 let window = WindowCreateOptions::create(render);
5288 app.run(window);
5289}";
5290
5291 let ui_module = format!(
5292 "#[allow(unused_imports)]\r\npub mod ui {{
5293
5294 use azul::prelude::*;
5295 use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
5296 use azul::str::String as AzString;
5297
5298 pub fn render() -> Dom {{\r\n{app_source}\r\n }}\r\n}}"
5299 );
5300 let source_code = format!(
5301 "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
5302 code\r\n{}\r\n{}\r\n\r\n{}{}",
5303 imports,
5304 compile_components(Vec::new()), ui_module,
5306 main_func,
5307 );
5308
5309 Ok(source_code)
5310}
5311
5312#[allow(clippy::needless_pass_by_value)] fn compile_components(
5315 components: Vec<(
5316 ComponentName,
5317 CompiledComponent,
5318 ComponentArguments,
5319 BTreeMap<String, String>,
5320 )>,
5321) -> String {
5322 let cs = components
5323 .iter()
5324 .map(|(name, function_body, function_args, css_blocks)| {
5325 let name = &normalize_casing(name);
5326 let f = compile_component(name, function_args, function_body)
5327 .lines()
5328 .map(|l| format!(" {l}"))
5329 .collect::<Vec<String>>()
5330 .join("\r\n");
5331
5332 format!(
5335 "#[allow(unused_imports)]\r\npub mod {name} {{\r\n use azul::dom::Dom;\r\n use \
5336 azul::str::String as AzString;\r\n{f}\r\n}}"
5337 )
5338 })
5339 .collect::<Vec<String>>()
5340 .join("\r\n\r\n");
5341
5342 let cs = cs
5343 .lines()
5344 .map(|l| format!(" {l}"))
5345 .collect::<Vec<String>>()
5346 .join("\r\n");
5347
5348 if cs.is_empty() {
5349 cs
5350 } else {
5351 format!("pub mod components {{\r\n{cs}\r\n}}")
5352 }
5353}
5354
5355fn format_component_args(component_args: &ComponentArgumentVec) -> String {
5356 let mut args = component_args
5357 .iter()
5358 .map(|a| format!("{}: {}", a.name, a.arg_type))
5359 .collect::<Vec<String>>();
5360
5361 args.sort_by(|a, b| b.cmp(a));
5362
5363 args.join(", ")
5364}
5365
5366#[must_use] pub fn compile_component(
5367 component_name: &str,
5368 component_args: &ComponentArguments,
5369 component_function_body: &str,
5370) -> String {
5371 let component_name = &normalize_casing(component_name);
5372 let function_args = format_component_args(&component_args.args);
5373 let component_function_body = component_function_body
5374 .lines()
5375 .map(|l| format!(" {l}"))
5376 .collect::<Vec<String>>()
5377 .join("\r\n");
5378 let should_inline = component_function_body.lines().count() == 1;
5379 format!(
5380 "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
5381 if should_inline { "#[inline]\r\n" } else { "" },
5382 if component_args.accepts_text {
5384 "text: AzString"
5385 } else {
5386 ""
5387 },
5388 if function_args.is_empty() || !component_args.accepts_text {
5389 ""
5390 } else {
5391 ", "
5392 },
5393 function_args,
5394 component_function_body,
5395 )
5396}
5397
5398fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
5400 attr?.as_str().trim().parse::<f32>().ok()
5401}
5402
5403fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
5405 let nums: Vec<f32> = pts
5406 .split(|c: char| c == ',' || c.is_ascii_whitespace())
5407 .filter(|s| !s.is_empty())
5408 .filter_map(|s| s.parse::<f32>().ok())
5409 .collect();
5410 if nums.len() < 4 || !nums.len().is_multiple_of(2) {
5411 return None;
5412 }
5413 let mut elements = Vec::new();
5414 let points: Vec<azul_css::props::basic::SvgPoint> = nums
5415 .chunks_exact(2)
5416 .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
5417 .collect();
5418 for w in points.windows(2) {
5419 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5420 w[0], w[1],
5421 )));
5422 }
5423 if close && points.len() >= 2 {
5424 let first = points[0];
5425 let last = *points.last().unwrap();
5426 if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
5427 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5428 last, first,
5429 )));
5430 }
5431 }
5432 Some(crate::svg::SvgMultiPolygon {
5433 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5434 items: crate::svg::SvgPathElementVec::from_vec(elements),
5435 }]),
5436 })
5437}
5438
5439#[allow(clippy::too_many_lines)] fn apply_xml_node_attributes(
5450 node: &mut crate::dom::NodeData,
5451 xml_node: &XmlNode,
5452 component_name: &str,
5453 inside_svg: bool,
5454) {
5455 use crate::dom::{IdOrClass, NodeType, TabIndex};
5456
5457 if component_name == "img" {
5463 if let Some(src) = xml_node.attributes.get_key("src") {
5464 let width = xml_node
5465 .attributes
5466 .get_key("width")
5467 .and_then(|w| {
5468 w.as_str()
5469 .trim()
5470 .trim_end_matches("px")
5471 .trim()
5472 .parse::<usize>()
5473 .ok()
5474 })
5475 .unwrap_or(0);
5476 let height = xml_node
5477 .attributes
5478 .get_key("height")
5479 .and_then(|h| {
5480 h.as_str()
5481 .trim()
5482 .trim_end_matches("px")
5483 .trim()
5484 .parse::<usize>()
5485 .ok()
5486 })
5487 .unwrap_or(0);
5488 let image_ref = crate::resources::ImageRef::null_image(
5489 width,
5490 height,
5491 crate::resources::RawImageFormat::RGBA8,
5492 src.as_str().as_bytes().to_vec(),
5493 );
5494 node
5495 .set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
5496 }
5497 }
5498
5499 let mut ids_and_classes = Vec::new();
5501 if let Some(id_str) = xml_node.attributes.get_key("id") {
5502 for id in id_str.split_whitespace() {
5503 ids_and_classes.push(IdOrClass::Id(id.into()));
5504 }
5505 }
5506 if let Some(class_str) = xml_node.attributes.get_key("class") {
5507 for class in class_str.split_whitespace() {
5508 ids_and_classes.push(IdOrClass::Class(class.into()));
5509 }
5510 }
5511 if !ids_and_classes.is_empty() {
5512 node.set_ids_and_classes(ids_and_classes.into());
5513 }
5514
5515 if let Some(focusable) = xml_node
5517 .attributes
5518 .get_key("focusable")
5519 .and_then(|f| parse_bool(f.as_str()))
5520 {
5521 if focusable { node.set_tab_index(TabIndex::Auto) } else { node.set_tab_index(TabIndex::NoKeyboardFocus) }
5522 }
5523
5524 if let Some(tab_index) = xml_node
5526 .attributes
5527 .get_key("tabindex")
5528 .and_then(|val| val.parse::<isize>().ok())
5529 {
5530 match tab_index {
5531 0 => node.set_tab_index(TabIndex::Auto),
5532 i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(u32::try_from(i).unwrap_or(u32::MAX))),
5533 _ => node.set_tab_index(TabIndex::NoKeyboardFocus),
5534 }
5535 }
5536
5537 apply_cell_span_attributes(node, xml_node);
5539
5540 let dir_prop = xml_node.attributes.get_key("dir").and_then(|d| {
5544 let v = d.as_str().trim();
5545 if v.eq_ignore_ascii_case("rtl") {
5546 Some(azul_css::props::style::StyleDirection::Rtl)
5547 } else if v.eq_ignore_ascii_case("ltr") {
5548 Some(azul_css::props::style::StyleDirection::Ltr)
5549 } else {
5550 None
5551 }
5552 });
5553
5554 let style_attr = xml_node.attributes.get_key("style");
5556 if style_attr.is_some() || dir_prop.is_some() {
5557 use azul_css::dynamic_selector::CssPropertyWithConditions;
5558 let css_key_map = azul_css::props::property::get_css_key_map();
5559 let mut props: Vec<CssPropertyWithConditions> = Vec::new();
5560 if let Some(dir) = dir_prop {
5561 props.push(CssPropertyWithConditions::simple(
5562 azul_css::props::property::CssProperty::Direction(
5563 azul_css::css::CssPropertyValue::Exact(dir),
5564 ),
5565 ));
5566 }
5567 if let Some(style) = style_attr {
5568 let mut attributes = Vec::new();
5569 for s in style.as_str().split(';') {
5570 let mut s = s.split(':');
5571 let Some(key) = s.next() else {
5572 continue;
5573 };
5574 let Some(value) = s.next() else {
5575 continue;
5576 };
5577 drop(azul_css::parser2::parse_css_declaration(
5580 key.trim(),
5581 value.trim(),
5582 azul_css::parser2::ErrorLocationRange::default(),
5583 &css_key_map,
5584 &mut Vec::new(),
5585 &mut attributes,
5586 ));
5587 }
5588 props.extend(attributes.into_iter().filter_map(|s| match s {
5589 CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
5590 CssDeclaration::Dynamic(_) => None,
5591 }));
5592 }
5593 if !props.is_empty() {
5594 node.set_css_props(props.into());
5595 }
5596 }
5597
5598 let tag = component_name;
5600 let is_svg_shape = inside_svg
5601 && matches!(
5602 tag,
5603 "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
5604 );
5605
5606 if is_svg_shape {
5607 let clip = match tag {
5608 "path" => xml_node
5609 .attributes
5610 .get_key("d")
5611 .and_then(|d| crate::path_parser::parse_svg_path_d(d.as_str()).ok()),
5612 "circle" => {
5613 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5614 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5615 let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
5616 if r > 0.0 {
5617 Some(crate::svg::SvgMultiPolygon {
5618 rings: crate::svg::SvgPathVec::from_vec(vec![
5619 crate::path_parser::svg_circle_to_paths(cx, cy, r),
5620 ]),
5621 })
5622 } else {
5623 None
5624 }
5625 }
5626 "rect" => {
5627 let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
5628 let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
5629 let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
5630 let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
5631 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5632 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
5633 if w > 0.0 && h > 0.0 {
5634 Some(crate::svg::SvgMultiPolygon {
5635 rings: crate::svg::SvgPathVec::from_vec(vec![
5636 crate::path_parser::svg_rect_to_path(x, y, w, h, rx, ry),
5637 ]),
5638 })
5639 } else {
5640 None
5641 }
5642 }
5643 "ellipse" => {
5644 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5645 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5646 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5647 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
5648 if rx > 0.0 && ry > 0.0 {
5649 use azul_css::props::basic::{SvgCubicCurve, SvgPoint};
5651 const KAPPA: f32 = 0.552_284_8;
5652 let kx = rx * KAPPA;
5653 let ky = ry * KAPPA;
5654 let elements = vec![
5655 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5656 start: SvgPoint { x: cx, y: cy - ry },
5657 ctrl_1: SvgPoint {
5658 x: cx + kx,
5659 y: cy - ry,
5660 },
5661 ctrl_2: SvgPoint {
5662 x: cx + rx,
5663 y: cy - ky,
5664 },
5665 end: SvgPoint { x: cx + rx, y: cy },
5666 }),
5667 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5668 start: SvgPoint { x: cx + rx, y: cy },
5669 ctrl_1: SvgPoint {
5670 x: cx + rx,
5671 y: cy + ky,
5672 },
5673 ctrl_2: SvgPoint {
5674 x: cx + kx,
5675 y: cy + ry,
5676 },
5677 end: SvgPoint { x: cx, y: cy + ry },
5678 }),
5679 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5680 start: SvgPoint { x: cx, y: cy + ry },
5681 ctrl_1: SvgPoint {
5682 x: cx - kx,
5683 y: cy + ry,
5684 },
5685 ctrl_2: SvgPoint {
5686 x: cx - rx,
5687 y: cy + ky,
5688 },
5689 end: SvgPoint { x: cx - rx, y: cy },
5690 }),
5691 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5692 start: SvgPoint { x: cx - rx, y: cy },
5693 ctrl_1: SvgPoint {
5694 x: cx - rx,
5695 y: cy - ky,
5696 },
5697 ctrl_2: SvgPoint {
5698 x: cx - kx,
5699 y: cy - ry,
5700 },
5701 end: SvgPoint { x: cx, y: cy - ry },
5702 }),
5703 ];
5704 Some(crate::svg::SvgMultiPolygon {
5705 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5706 items: crate::svg::SvgPathElementVec::from_vec(elements),
5707 }]),
5708 })
5709 } else {
5710 None
5711 }
5712 }
5713 "line" => {
5714 let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
5715 let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
5716 let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
5717 let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
5718 Some(crate::svg::SvgMultiPolygon {
5719 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5720 items: crate::svg::SvgPathElementVec::from_vec(vec![
5721 crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5722 azul_css::props::basic::SvgPoint { x: x1, y: y1 },
5723 azul_css::props::basic::SvgPoint { x: x2, y: y2 },
5724 )),
5725 ]),
5726 }]),
5727 })
5728 }
5729 "polygon" | "polyline" => xml_node
5730 .attributes
5731 .get_key("points")
5732 .and_then(|pts| parse_svg_points(pts.as_str(), tag == "polygon")),
5733 _ => None,
5734 };
5735
5736 if let Some(mp) = clip {
5737 node.set_svg_data(crate::dom::SvgNodeData::Path(mp));
5738 }
5739 }
5740}
5741
5742fn apply_cell_span_attributes(node: &mut crate::dom::NodeData, xml_node: &XmlNode) {
5748 let mut spans = Vec::new();
5749 if let Some(n) = xml_node
5750 .attributes
5751 .get_key("colspan")
5752 .and_then(|v| v.as_str().trim().parse::<i32>().ok())
5753 {
5754 spans.push(crate::dom::AttributeType::ColSpan(n));
5755 }
5756 if let Some(n) = xml_node
5757 .attributes
5758 .get_key("rowspan")
5759 .and_then(|v| v.as_str().trim().parse::<i32>().ok())
5760 {
5761 spans.push(crate::dom::AttributeType::RowSpan(n));
5762 }
5763 if !spans.is_empty() {
5764 let mut v = node.attributes().clone().into_library_owned_vec();
5765 v.extend(spans);
5766 node.set_attributes(v.into());
5767 }
5768}
5769
5770#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
5776fn xml_node_to_dom_fast<'a>(
5777 xml_node: &'a XmlNode,
5778 component_map: &'a ComponentMap,
5779 inside_svg: bool,
5780 depth: usize,
5781) -> Result<Dom, RenderDomError> {
5782 use crate::dom::Dom;
5783
5784 let component_name = normalize_casing(&xml_node.node_type);
5785
5786 let node_type = tag_to_node_type(&component_name);
5788 let mut dom = Dom::create_node(node_type);
5789
5790 apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
5791
5792 let child_inside_svg = inside_svg || component_name == "svg";
5793
5794 if depth >= MAX_XML_NESTING_DEPTH {
5799 return Ok(dom);
5800 }
5801
5802 let mut children = Vec::new();
5804 for child in xml_node.children.as_ref() {
5805 match child {
5806 XmlNodeChild::Element(child_node) => {
5807 let child_dom =
5808 xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
5809 children.push(child_dom);
5810 }
5811 XmlNodeChild::Text(text) => {
5812 let text_dom = Dom::create_text(AzString::from(text.as_str()));
5813 children.push(text_dom);
5814 }
5815 }
5816 }
5817
5818 if !children.is_empty() {
5819 dom = dom.with_children(children.into());
5820 }
5821
5822 Ok(dom)
5823}
5824
5825#[derive(Debug)]
5828pub struct CompactDomBuilder {
5829 hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
5830 node_data: Vec<crate::dom::NodeData>,
5831 css: Vec<crate::dom::CssWithNodeId>,
5832 stack: Vec<(usize, Option<usize>)>,
5834}
5835
5836impl Default for CompactDomBuilder {
5837 fn default() -> Self {
5838 Self::new()
5839 }
5840}
5841
5842impl CompactDomBuilder {
5843 #[must_use] pub const fn new() -> Self {
5844 Self {
5845 hierarchy: Vec::new(),
5846 node_data: Vec::new(),
5847 css: Vec::new(),
5848 stack: Vec::new(),
5849 }
5850 }
5851
5852 #[must_use] pub fn with_capacity(cap: usize) -> Self {
5853 Self {
5854 hierarchy: Vec::with_capacity(cap),
5855 node_data: Vec::with_capacity(cap),
5856 css: Vec::new(),
5857 stack: Vec::new(),
5858 }
5859 }
5860
5861 pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
5863 use crate::id::NodeId;
5864 use crate::styled_dom::NodeHierarchyItem;
5865
5866 let idx = self.hierarchy.len();
5867
5868 let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
5870 NodeId::into_raw(&Some(NodeId::new(parent_idx)))
5871 } else {
5872 0 };
5874
5875 let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
5877 prev_child
5878 .map_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
5879 } else {
5880 0
5881 };
5882
5883 if let Some(&(_, Some(prev_idx))) = self.stack.last() {
5885 self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
5886 }
5887
5888 if let Some(parent) = self.stack.last_mut() {
5890 parent.1 = Some(idx);
5891 }
5892
5893 self.hierarchy.push(NodeHierarchyItem {
5895 parent: parent_raw,
5896 previous_sibling: prev_sibling_raw,
5897 next_sibling: 0, last_child: 0, });
5900 self.node_data.push(node_data);
5901
5902 self.stack.push((idx, None));
5904 }
5905
5906 pub fn close_node(&mut self) {
5908 use crate::id::NodeId;
5909
5910 if let Some((idx, last_child_idx)) = self.stack.pop() {
5911 self.hierarchy[idx].last_child = last_child_idx
5913 .map_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
5914 }
5915 }
5916
5917 pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
5919 self.open_node(node_data);
5920 self.close_node();
5921 }
5922
5923 pub fn add_css(&mut self, node_id: usize, css: Css) {
5925 self.css.push(crate::dom::CssWithNodeId { node_id, css });
5926 }
5927
5928 #[must_use] pub fn finish(self) -> crate::dom::FastDom {
5930 crate::dom::FastDom {
5931 node_hierarchy: self.hierarchy.into(),
5932 node_data: self.node_data.into(),
5933 css: self.css.into(),
5934 }
5935 }
5936}
5937
5938#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
5943fn xml_node_to_fast_dom<'a>(
5944 xml_node: &'a XmlNode,
5945 component_map: &'a ComponentMap,
5946 inside_svg: bool,
5947 builder: &mut CompactDomBuilder,
5948 depth: usize,
5949) -> Result<(), RenderDomError> {
5950 use crate::dom::NodeData;
5951
5952 let component_name = normalize_casing(&xml_node.node_type);
5953 let node_type = tag_to_node_type(&component_name);
5954 let mut node_data = NodeData::create_node(node_type);
5955
5956 apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
5957
5958 let child_inside_svg = inside_svg || component_name == "svg";
5959
5960 builder.open_node(node_data);
5962
5963 if depth < MAX_XML_NESTING_DEPTH {
5968 for child in xml_node.children.as_ref() {
5970 match child {
5971 XmlNodeChild::Element(child_node) => {
5972 xml_node_to_fast_dom(
5973 child_node,
5974 component_map,
5975 child_inside_svg,
5976 builder,
5977 depth + 1,
5978 )?;
5979 }
5980 XmlNodeChild::Text(text) => {
5981 builder.add_leaf(NodeData::create_text(AzString::from(text.as_str())));
5982 }
5983 }
5984 }
5985 }
5986
5987 builder.close_node();
5989
5990 Ok(())
5991}
5992
5993#[allow(clippy::result_large_err)] fn render_dom_from_body_node_fast<'a>(
5997 body_node: &'a XmlNode,
5998 mut global_css: Option<Css>,
5999 component_map: &'a ComponentMap,
6000 max_width: Option<f32>,
6001) -> Result<StyledDom, RenderDomError> {
6002 use crate::dom::{NodeData, NodeType};
6003
6004 let mut builder = CompactDomBuilder::new();
6005
6006 builder.open_node(NodeData::create_node(NodeType::Html));
6009 xml_node_to_fast_dom(body_node, component_map, false, &mut builder, 0)?;
6011 builder.close_node();
6013
6014 let mut combined_rules: Vec<CssRuleBlock> = Vec::new();
6016 if let Some(max_width) = max_width {
6017 let max_width_css =
6018 Css::from_string(format!("html {{ max-width: {max_width}px; }}").into());
6019 combined_rules.extend(max_width_css.rules.into_library_owned_vec());
6020 }
6021 if let Some(css) = global_css.take() {
6022 combined_rules.extend(css.rules.into_library_owned_vec());
6023 }
6024 let combined_css = Css::new(combined_rules);
6025
6026 let mut fast_dom = builder.finish();
6028 fast_dom.css = vec![crate::dom::CssWithNodeId {
6029 node_id: 0, css: combined_css,
6031 }]
6032 .into();
6033
6034 let styled = StyledDom::create_from_fast_dom(fast_dom);
6036 Ok(styled)
6037}
6038
6039fn set_stringified_attributes(
6042 dom_string: &mut String,
6043 xml_attributes: &XmlAttributeMap,
6044 filtered_xml_attributes: &ComponentArgumentVec,
6045 tabs: usize,
6046) {
6047 let t0 = String::from(" ").repeat(tabs);
6048 let t = String::from(" ").repeat(tabs + 1);
6049
6050 let _ = &t;
6053 for id in xml_attributes
6054 .get_key("id")
6055 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6056 .unwrap_or_default()
6057 {
6058 let _ = write!(
6059 dom_string,
6060 "\r\n{}.with_id(\"{}\")",
6061 t0,
6062 format_args_dynamic(id, filtered_xml_attributes)
6063 );
6064 }
6065
6066 for class in xml_attributes
6067 .get_key("class")
6068 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6069 .unwrap_or_default()
6070 {
6071 let _ = write!(
6072 dom_string,
6073 "\r\n{}.with_class(\"{}\")",
6074 t0,
6075 format_args_dynamic(class, filtered_xml_attributes)
6076 );
6077 }
6078
6079 if let Some(focusable) = xml_attributes
6080 .get_key("focusable")
6081 .map(|f| format_args_dynamic(f, filtered_xml_attributes))
6082 .and_then(|f| parse_bool(&f))
6083 {
6084 if focusable { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); } else { let _ = write!(dom_string,
6085 "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6086 ); }
6087 }
6088
6089 if let Some(tab_index) = xml_attributes
6090 .get_key("tabindex")
6091 .map(|val| format_args_dynamic(val, filtered_xml_attributes))
6092 .and_then(|val| val.parse::<isize>().ok())
6093 {
6094 match tab_index {
6095 0 => { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); },
6096 i if i > 0 => { let _ = write!(dom_string,
6097 "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
6098 t, usize::try_from(i).unwrap_or(0)
6099 ); },
6100 _ => { let _ = write!(dom_string,
6101 "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6102 ); },
6103 }
6104 }
6105}
6106
6107#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6109pub enum DynamicItem {
6110 Var {
6112 name: String,
6113 format_spec: Option<String>,
6115 },
6116 Str(String),
6117}
6118
6119#[must_use] pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
6137 use self::DynamicItem::{Str, Var};
6138
6139 let input: Vec<char> = input.chars().collect();
6140 let input_chars_len = input.len();
6141
6142 let mut items = Vec::new();
6143 let mut current_idx = 0;
6144 let mut last_idx = 0;
6145
6146 while current_idx < input_chars_len {
6147 let c = input[current_idx];
6148 match c {
6149 '{' if input.get(current_idx + 1).copied() != Some('{') => {
6150 let mut start_offset = 1;
6152 let mut has_found_variable = false;
6153 while let Some(c) = input.get(current_idx + start_offset) {
6154 if c.is_whitespace() {
6155 break;
6156 }
6157 if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
6158 {
6159 start_offset += 1;
6160 has_found_variable = true;
6161 break;
6162 }
6163 start_offset += 1;
6164 }
6165
6166 if has_found_variable {
6170 if last_idx != current_idx {
6171 items.push(Str(input[last_idx..current_idx].iter().collect()));
6172 }
6173
6174 let var_content: String = input
6176 [(current_idx + 1)..(current_idx + start_offset - 1)]
6177 .iter()
6178 .collect();
6179 let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
6181 let name = var_content[..colon_pos].to_string();
6182 let spec = var_content[(colon_pos + 1)..].to_string();
6183 (name, Some(spec))
6184 } else {
6185 (var_content, None)
6186 };
6187 items.push(Var {
6188 name: var_name,
6189 format_spec,
6190 });
6191 current_idx += start_offset;
6192 last_idx = current_idx;
6193 } else {
6194 current_idx += start_offset;
6195 }
6196 }
6197 _ => {
6198 current_idx += 1;
6199 }
6200 }
6201 }
6202
6203 if current_idx != last_idx {
6204 items.push(Str(input[last_idx..].iter().collect()));
6205 }
6206
6207 for item in &mut items {
6208 if let Str(s) = item {
6210 *s = s.replace("{{", "{").replace("}}", "}");
6211 }
6212 }
6213
6214 items
6215}
6216
6217fn combine_and_replace_dynamic_items(
6224 input: &[DynamicItem],
6225 variables: &ComponentArgumentVec,
6226) -> String {
6227 let mut s = String::new();
6228
6229 for item in input {
6230 match item {
6231 DynamicItem::Var { name, format_spec } => {
6232 let variable_name = normalize_casing(name.trim());
6233 if let Some(resolved_var) = variables
6234 .iter()
6235 .find(|s| s.name.as_str() == variable_name)
6236 .map(|q| &q.arg_type) {
6237 s.push_str(resolved_var);
6239 } else {
6240 s.push('{');
6241 s.push_str(name);
6242 if let Some(spec) = format_spec {
6243 s.push(':');
6244 s.push_str(spec);
6245 }
6246 s.push('}');
6247 }
6248 }
6249 DynamicItem::Str(dynamic_str) => {
6250 s.push_str(dynamic_str);
6251 }
6252 }
6253 }
6254
6255 s
6256}
6257
6258#[must_use] pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
6277 let dynamic_str_items = split_dynamic_string(input);
6278 combine_and_replace_dynamic_items(&dynamic_str_items, variables)
6279}
6280
6281fn decode_numeric_entity(entity: &str) -> Option<char> {
6285 let num = entity.strip_prefix('#')?;
6286 let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
6287 u32::from_str_radix(hex, 16).ok()?
6288 } else {
6289 num.parse::<u32>().ok()?
6290 };
6291 char::from_u32(code)
6292}
6293
6294fn decode_entities(input: &str) -> String {
6301 const MAX_ENTITY_BODY: usize = 12;
6304
6305 let mut out = String::with_capacity(input.len());
6306 let bytes = input.as_bytes();
6307 let mut i = 0;
6308 while i < input.len() {
6309 if bytes[i] == b'&' {
6310 if let Some(semi_rel) = input[i + 1..].find(';') {
6311 if semi_rel <= MAX_ENTITY_BODY {
6312 let body = &input[i + 1..i + 1 + semi_rel];
6313 let end = i + 1 + semi_rel; if body.eq_ignore_ascii_case("nbsp") {
6316 out.push_str(&input[i..=end]);
6317 i = end + 1;
6318 continue;
6319 }
6320 let decoded = match body {
6321 "lt" => Some('<'),
6322 "gt" => Some('>'),
6323 "amp" => Some('&'),
6324 "quot" => Some('"'),
6325 "apos" => Some('\''),
6326 _ => decode_numeric_entity(body),
6327 };
6328 if let Some(c) = decoded {
6329 out.push(c);
6330 i = end + 1;
6331 continue;
6332 }
6333 }
6334 }
6335 out.push('&');
6337 i += 1;
6338 } else {
6339 let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
6341 out.push(ch);
6342 i += ch.len_utf8();
6343 }
6344 }
6345 out
6346}
6347
6348#[must_use] pub fn prepare_string(input: &str) -> String {
6350 const SPACE: &str = " ";
6351 const RETURN: &str = "\n";
6352
6353 let input = input.trim();
6354
6355 if input.is_empty() {
6356 return String::new();
6357 }
6358
6359 let input = decode_entities(input);
6367
6368 let input_len = input.len();
6369 let mut final_lines: Vec<String> = Vec::new();
6370 let mut last_line_was_empty = false;
6371
6372 for line in input.lines() {
6373 let line = line.trim();
6374 let line = line.replace(" ", " ");
6375 let current_line_is_empty = line.is_empty();
6376
6377 if !current_line_is_empty {
6378 if last_line_was_empty {
6379 final_lines.push(format!("{RETURN}{line}"));
6380 } else {
6381 final_lines.push(line.to_string());
6382 }
6383 }
6384
6385 last_line_was_empty = current_line_is_empty;
6386 }
6387
6388 let mut target = String::with_capacity(input_len);
6389 for (line_idx, line) in final_lines.iter().enumerate() {
6390 if !(line.starts_with(RETURN) || line_idx == 0) {
6395 target.push_str(SPACE);
6396 }
6397 target.push_str(line);
6398 }
6399 target
6400}
6401
6402#[must_use] pub fn parse_bool(input: &str) -> Option<bool> {
6404 match input {
6405 "true" => Some(true),
6406 "false" => Some(false),
6407 _ => None,
6408 }
6409}
6410
6411#[derive(Debug, Clone)]
6412pub struct CssMatcher {
6413 path: Vec<CssPathSelector>,
6414 indices_in_parent: Vec<usize>,
6415 children_length: Vec<usize>,
6416}
6417
6418impl CssMatcher {
6419 fn get_hash(&self) -> u64 {
6420 use core::hash::Hash;
6421
6422 use core::hash::Hasher;
6423
6424 let mut hasher = crate::hash::DefaultHasher::new();
6425 for p in &self.path {
6426 p.hash(&mut hasher);
6427 }
6428 hasher.finish()
6429 }
6430}
6431
6432impl CssMatcher {
6433 fn matches(&self, path: &CssPath) -> bool {
6434 use azul_css::css::CssPathSelector::*;
6435
6436 use crate::style::{CssGroupIterator, CssGroupSplitReason};
6437
6438 if self.path.is_empty() {
6439 return false;
6440 }
6441 if path.selectors.as_ref().is_empty() {
6442 return false;
6443 }
6444
6445 let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
6447 path_groups.reverse();
6448
6449 if path_groups.is_empty() {
6450 return false;
6451 }
6452 let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
6453 self_groups.reverse();
6454 if self_groups.is_empty() {
6455 return false;
6456 }
6457
6458 if self.indices_in_parent.len() != self_groups.len() {
6459 return false;
6460 }
6461 if self.children_length.len() != self_groups.len() {
6462 return false;
6463 }
6464
6465 let mut cur_selfgroup_scan = 0;
6479 let mut cur_pathgroup_scan = 0;
6480 let mut valid = false;
6481 let mut path_group = path_groups[cur_pathgroup_scan].clone();
6482
6483 while cur_selfgroup_scan < self_groups.len() {
6484 let mut advance = None;
6485
6486 for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
6488 let gm = group_matches(
6489 &path_group.0,
6490 &self_groups[cur_selfgroup_scan + id].0,
6491 self.indices_in_parent[cur_selfgroup_scan + id],
6492 self.children_length[cur_selfgroup_scan + id],
6493 );
6494
6495 if gm {
6496 advance = Some(id);
6499 break;
6500 }
6501 }
6502
6503 match advance {
6504 Some(n) => {
6505 if cur_pathgroup_scan == path_groups.len() - 1 {
6508 return cur_selfgroup_scan + n == self_groups.len() - 1;
6510 }
6511 cur_pathgroup_scan += 1;
6512 cur_selfgroup_scan += n;
6513 path_group = path_groups[cur_pathgroup_scan].clone();
6514 }
6515 None => return false, }
6517 }
6518
6519 cur_pathgroup_scan == path_groups.len() - 1
6521 }
6522}
6523
6524fn group_matches(
6527 a: &[&CssPathSelector],
6528 b: &[&CssPathSelector],
6529 idx_in_parent: usize,
6530 parent_children: usize,
6531) -> bool {
6532 use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::{Global, PseudoSelector, Type, Class, Id}};
6533
6534 for selector in a {
6535 match selector {
6536 Global |
6538PseudoSelector(CssPathPseudoSelector::Hover | CssPathPseudoSelector::Active |
6539CssPathPseudoSelector::Focus) => {}
6540
6541 Type(tag) => {
6542 if !b.iter().any(|t| **t == Type(*tag)) {
6543 return false;
6544 }
6545 }
6546 Class(class) => {
6547 if !b.iter().any(|t| **t == Class(class.clone())) {
6548 return false;
6549 }
6550 }
6551 Id(id) => {
6552 if !b.iter().any(|t| **t == Id(id.clone())) {
6553 return false;
6554 }
6555 }
6556 PseudoSelector(CssPathPseudoSelector::First) => {
6557 if idx_in_parent != 0 {
6558 return false;
6559 }
6560 }
6561 PseudoSelector(CssPathPseudoSelector::Last) => {
6562 if idx_in_parent != parent_children.saturating_sub(1) {
6563 return false;
6564 }
6565 }
6566 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
6567 if idx_in_parent != *i as usize {
6568 return false;
6569 }
6570 }
6571 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
6572 if !idx_in_parent.is_multiple_of(2) {
6573 return false;
6574 }
6575 }
6576 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
6577 if idx_in_parent.is_multiple_of(2) {
6578 return false;
6579 }
6580 }
6581 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
6582 if !idx_in_parent.saturating_sub(p.offset as usize).is_multiple_of(p.pattern_repeat as usize)
6583 {
6584 return false;
6585 }
6586 }
6587
6588 _ => return false, }
6590 }
6591
6592 true
6593}
6594
6595struct CssBlock {
6596 ending: Option<CssPathPseudoSelector>,
6597 block: CssRuleBlock,
6598}
6599
6600#[allow(clippy::result_large_err)] pub fn compile_body_node_to_rust_code<'a>(
6605 body_node: &'a XmlNode,
6606 component_map: &'a ComponentMap,
6607 extra_blocks: &mut VecContents,
6608 css_blocks: &mut BTreeMap<String, String>,
6609 css: &Css,
6610 mut matcher: CssMatcher,
6611) -> Result<String, CompileError> {
6612 use azul_css::css::CssDeclaration;
6613
6614 let t = "";
6615 let t2 = " ";
6616 let mut dom_string = String::from("Dom::create_body()");
6617 let node_type = CssPathSelector::Type(NodeTypeTag::Body);
6618 matcher.path.push(node_type);
6619
6620 let ids = body_node
6621 .attributes
6622 .get_key("id")
6623 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6624 .unwrap_or_default();
6625 matcher.path.extend(
6626 ids.into_iter()
6627 .map(|id| CssPathSelector::Id(id.to_string().into())),
6628 );
6629 let classes = body_node
6630 .attributes
6631 .get_key("class")
6632 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6633 .unwrap_or_default();
6634 matcher.path.extend(
6635 classes
6636 .into_iter()
6637 .map(|class| CssPathSelector::Class(class.to_string().into())),
6638 );
6639
6640 let matcher_hash = matcher.get_hash();
6641 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6642 if !css_blocks_for_this_node.is_empty() {
6643 for css_block in &css_blocks_for_this_node {
6649 for declaration in css_block.block.declarations.as_ref() {
6650 let prop = match declaration {
6651 CssDeclaration::Static(s) => s,
6652 CssDeclaration::Dynamic(d) => &d.default_value,
6653 };
6654 extra_blocks.insert_from_css_property(prop);
6655 }
6656 }
6657
6658 let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6659 if !inline_css.is_empty() {
6660 let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6661 let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6662 }
6663 let _ = (&mut *css_blocks, matcher_hash); }
6665
6666 if !body_node.children.as_ref().is_empty() {
6667 use azul_css::codegen::format::GetHash;
6668 let children_hash = body_node.children.as_ref().get_hash();
6669 dom_string.push_str("\r\n.with_children(vec![\r\n");
6670
6671 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
6672 match child {
6673 XmlNodeChild::Element(child_node) => {
6674 let mut matcher = matcher.clone();
6675 matcher.path.push(CssPathSelector::Children);
6676 matcher.indices_in_parent.push(child_idx);
6677 matcher.children_length.push(body_node.children.len());
6678
6679 let _ = write!(dom_string,
6680 "{}{},\r\n",
6681 t,
6682 compile_node_to_rust_code_inner(
6683 child_node,
6684 component_map,
6685 1,
6686 extra_blocks,
6687 css_blocks,
6688 css,
6689 matcher,
6690 )?
6691 );
6692 }
6693 XmlNodeChild::Text(text) => {
6694 let text = text.trim();
6695 if !text.is_empty() {
6696 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6697 let _ = write!(dom_string,
6698 "{t}Dom::create_text(\"{escaped}\"),\r\n"
6699 );
6700 }
6701 }
6702 }
6703 }
6704 let _ = write!(dom_string, "\r\n{t}])");
6705 }
6706
6707 let dom_string = dom_string.trim();
6708 Ok(dom_string.to_string())
6709}
6710
6711fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
6717 fn decls_of(block: &CssBlock) -> Vec<String> {
6718 block
6719 .block
6720 .declarations
6721 .as_ref()
6722 .iter()
6723 .map(|d| {
6724 let prop = match d {
6725 CssDeclaration::Static(s) => s,
6726 CssDeclaration::Dynamic(dy) => &dy.default_value,
6727 };
6728 format!("{}: {};", prop.key(), prop.value())
6729 })
6730 .collect()
6731 }
6732
6733 let mut normal: Vec<String> = Vec::new();
6734 let mut pseudo: Vec<String> = Vec::new();
6735 for block in blocks {
6736 let pseudo_sel = match block.ending {
6737 Some(CssPathPseudoSelector::Hover) => Some(":hover"),
6738 Some(CssPathPseudoSelector::Active) => Some(":active"),
6739 Some(CssPathPseudoSelector::Focus) => Some(":focus"),
6740 _ => None,
6741 };
6742 match pseudo_sel {
6743 None => normal.extend(decls_of(block)),
6744 Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
6745 }
6746 }
6747
6748 let mut parts = normal;
6749 parts.extend(pseudo);
6750 parts.join(" ")
6751}
6752
6753fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
6754 let mut blocks = Vec::new();
6755
6756 for css_block in css.rules.as_ref() {
6757 if matcher.matches(&css_block.path) {
6758 let mut ending = None;
6759
6760 if let Some(CssPathSelector::PseudoSelector(p)) =
6761 css_block.path.selectors.as_ref().last()
6762 {
6763 ending = Some(p.clone());
6764 }
6765
6766 blocks.push(CssBlock {
6767 ending,
6768 block: css_block.clone(),
6769 });
6770 }
6771 }
6772
6773 blocks
6774}
6775
6776fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
6777 use self::DynamicItem::{Var, Str};
6778 if input.is_empty() {
6779 String::from("AzString::from_const_str(\"\")")
6780 } else if input.len() == 1 {
6781 match &input[0] {
6783 Var { name, format_spec } => {
6784 let var_name = normalize_casing(name.trim());
6785 if let Some(spec) = format_spec {
6786 format!("format!(\"{{:{spec}}}\", {var_name}).into()")
6787 } else {
6788 var_name
6789 }
6790 }
6791 Str(s) => format!("AzString::from_const_str(\"{s}\")"),
6792 }
6793 } else {
6794 let mut formatted_str = String::from("format!(\"");
6796 let mut variables = Vec::new();
6797 for item in input {
6798 match item {
6799 Var { name, format_spec } => {
6800 let variable_name = normalize_casing(name.trim());
6801 if let Some(spec) = format_spec {
6802 let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
6803 } else {
6804 let _ = write!(formatted_str, "{{{variable_name}}}");
6805 }
6806 variables.push(variable_name.clone());
6807 }
6808 Str(s) => {
6809 let s = s.replace('"', "\\\"");
6810 formatted_str.push_str(&s);
6811 }
6812 }
6813 }
6814
6815 formatted_str.push('\"');
6816 if !variables.is_empty() {
6817 formatted_str.push_str(", ");
6818 }
6819
6820 formatted_str.push_str(&variables.join(", "));
6821 formatted_str.push_str(").into()");
6822 formatted_str
6823 }
6824}
6825
6826fn format_args_for_rust_code(input: &str) -> String {
6827 let dynamic_str_items = split_dynamic_string(input);
6828 compile_and_format_dynamic_items(&dynamic_str_items)
6829}
6830
6831#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
6835#[allow(clippy::too_many_lines)] fn compile_node_to_rust_code_inner(
6837 node: &XmlNode,
6838 component_map: &ComponentMap,
6839 tabs: usize,
6840 extra_blocks: &mut VecContents,
6841 css_blocks: &mut BTreeMap<String, String>,
6842 css: &Css,
6843 mut matcher: CssMatcher,
6844) -> Result<String, CompileError> {
6845 use azul_css::css::CssDeclaration;
6846
6847 let t = String::from(" ").repeat(tabs - 1);
6848 let t2 = String::from(" ").repeat(tabs);
6849
6850 let component_name = normalize_casing(&node.node_type);
6851
6852 let node_type_tag = tag_to_node_type_tag(&component_name);
6854 let node_type = CssPathSelector::Type(node_type_tag);
6855
6856 let ctor = analyze_node_ctor(&component_name, node);
6865 let mut dom_string = ctor.render_rust().map_or_else(|| {
6866 let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
6867 format!("{t2}Dom::create_node(NodeType::{tag})")
6868 }, |expr| format!("{t2}{expr}"));
6869
6870 matcher.path.push(node_type);
6871 let ids = node
6872 .attributes
6873 .get_key("id")
6874 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6875 .unwrap_or_default();
6876
6877 matcher.path.extend(
6878 ids.into_iter()
6879 .map(|id| CssPathSelector::Id(id.to_string().into())),
6880 );
6881
6882 let classes = node
6883 .attributes
6884 .get_key("class")
6885 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6886 .unwrap_or_default();
6887
6888 matcher.path.extend(
6889 classes
6890 .into_iter()
6891 .map(|class| CssPathSelector::Class(class.to_string().into())),
6892 );
6893
6894 let matcher_hash = matcher.get_hash();
6895 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6896 if !css_blocks_for_this_node.is_empty() {
6897 for css_block in &css_blocks_for_this_node {
6903 for declaration in css_block.block.declarations.as_ref() {
6904 let prop = match declaration {
6905 CssDeclaration::Static(s) => s,
6906 CssDeclaration::Dynamic(d) => &d.default_value,
6907 };
6908 extra_blocks.insert_from_css_property(prop);
6909 }
6910 }
6911
6912 let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6913 if !inline_css.is_empty() {
6914 let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6915 let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6916 }
6917 let _ = (&mut *css_blocks, matcher_hash); }
6919
6920 set_stringified_attributes(
6921 &mut dom_string,
6922 &node.attributes,
6923 &ComponentArgumentVec::new(),
6924 tabs,
6925 );
6926
6927 let mut caption_skipped = false;
6930 let mut children_string = node
6931 .children
6932 .as_ref()
6933 .iter()
6934 .enumerate()
6935 .filter_map(|(child_idx, c)| match c {
6936 XmlNodeChild::Element(child_node) => {
6937 if ctor.skip_caption()
6938 && !caption_skipped
6939 && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
6940 {
6941 caption_skipped = true;
6942 return None;
6943 }
6944 let mut matcher = matcher.clone();
6945 matcher.path.push(CssPathSelector::Children);
6946 matcher.indices_in_parent.push(child_idx);
6947 matcher.children_length.push(node.children.len());
6948
6949 Some(compile_node_to_rust_code_inner(
6950 child_node,
6951 component_map,
6952 tabs + 1,
6953 extra_blocks,
6954 css_blocks,
6955 css,
6956 matcher,
6957 ))
6958 }
6959 XmlNodeChild::Text(text) => {
6960 if ctor.consumes_text() {
6961 return None;
6962 }
6963 let text = text.trim();
6964 if text.is_empty() {
6965 None
6966 } else {
6967 let t2 = String::from(" ").repeat(tabs);
6968 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6969 Some(Ok(format!(
6970 "{t2}Dom::create_text(\"{escaped}\")"
6971 )))
6972 }
6973 }
6974 })
6975 .collect::<Result<Vec<_>, _>>()?
6976 .join(",\r\n");
6977
6978 if !children_string.is_empty() {
6979 let _ = write!(dom_string,
6980 "\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
6981 );
6982 }
6983
6984 Ok(dom_string)
6985}
6986
6987const SAFE_CONTAINER_TAGS: &[&str] = &[
7003 "Abbr", "Acronym", "Address", "Article", "Aside", "B", "Bdi", "Bdo", "Big",
7008 "BlockQuote", "Body", "Br", "Caption", "Cite", "Code", "ColGroup", "Dd",
7009 "Del", "Dfn", "Dir", "Div", "Dl", "Dt", "Em", "Embed", "FigCaption",
7010 "Figure", "Footer", "H1", "H2", "H3", "H4", "H5", "H6", "Head", "Header",
7011 "Hr", "Html", "I", "Ins", "Kbd", "Li", "Link", "Main", "Map", "Mark",
7012 "Meta", "Nav", "Object", "Ol", "P", "Pre", "Q", "Rp", "Rt", "Rtc", "Ruby",
7013 "S", "Samp", "Script", "Section", "Small", "Span", "Strong", "Style", "Sub",
7014 "Sup", "Svg", "TBody", "Td", "TFoot", "Th", "THead", "Title", "Tr", "U",
7015 "Ul", "Var", "Wbr",
7016];
7017
7018fn safe_container_tag(tag_dbg: &str) -> &'static str {
7021 SAFE_CONTAINER_TAGS.iter().copied().find(|t| *t == tag_dbg).unwrap_or("Div")
7022}
7023
7024#[derive(Debug, Clone)]
7051enum CtorArg {
7052 Str(String),
7054 Aria(String),
7056 Float(f32),
7058 OptSome(String),
7060 OptNone,
7062}
7063
7064enum NodeCtor {
7066 Plain,
7068 Semantic {
7070 suffix: String,
7073 args: Vec<CtorArg>,
7074 consumes_text: bool,
7077 skip_caption: bool,
7080 },
7081}
7082
7083fn cap_first(tag: &str) -> String {
7087 let mut c = tag.chars();
7088 c.next().map_or_else(String::new, |f| f.to_uppercase().collect::<String>() + c.as_str())
7089}
7090
7091fn camel_to_snake(s: &str) -> String {
7095 let chars: Vec<char> = s.chars().collect();
7096 let mut out = String::new();
7097 for (i, &ch) in chars.iter().enumerate() {
7098 if ch.is_ascii_uppercase() && i > 0 {
7099 let prev = chars[i - 1];
7100 let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
7101 if prev.is_ascii_lowercase()
7102 || prev.is_ascii_digit()
7103 || (prev.is_ascii_uppercase() && next_lower)
7104 {
7105 out.push('_');
7106 }
7107 }
7108 out.extend(ch.to_lowercase());
7109 }
7110 out
7111}
7112
7113fn esc_lit(s: &str) -> String {
7115 s.replace('\\', "\\\\").replace('"', "\\\"")
7116}
7117
7118fn fmt_f32_lit(f: f32) -> String {
7120 let s = format!("{f}");
7121 if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
7122 s
7123 } else {
7124 format!("{s}.0")
7125 }
7126}
7127
7128fn node_direct_text(node: &XmlNode) -> String {
7130 node.children
7131 .as_ref()
7132 .iter()
7133 .filter_map(|c| match c {
7134 XmlNodeChild::Text(t) => {
7135 let t = t.trim();
7136 if t.is_empty() { None } else { Some(t.to_string()) }
7137 }
7138 XmlNodeChild::Element(_) => None,
7139 })
7140 .collect::<Vec<_>>()
7141 .join(" ")
7142}
7143
7144fn node_aria_label(node: &XmlNode) -> Option<String> {
7146 node.attributes.get_key("aria-label").and_then(|v| {
7147 let v = v.as_str().trim();
7148 if v.is_empty() { None } else { Some(v.to_string()) }
7149 })
7150}
7151
7152fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
7154 node.attributes
7155 .get_key(key).map_or_else(|| default.to_string(), |v| v.as_str().to_string())
7156}
7157
7158fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
7160 node.attributes
7161 .get_key(key)
7162 .and_then(|v| v.as_str().trim().parse::<f32>().ok())
7163 .unwrap_or(default)
7164}
7165
7166fn first_caption_text(node: &XmlNode) -> Option<String> {
7168 node.children.as_ref().iter().find_map(|c| match c {
7169 XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
7170 let t = e.get_text_content();
7171 let t = t.trim();
7172 if t.is_empty() { None } else { Some(t.to_string()) }
7173 }
7174 _ => None,
7175 })
7176}
7177
7178const WITH_TEXT_TAGS: &[&str] = &[
7180 "acronym", "b", "bdi", "bdo", "big", "blockquote", "cite", "code", "del",
7181 "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "ins", "kbd", "li",
7182 "mark", "p", "pre", "rp", "rt", "s", "samp", "small", "span", "strong",
7183 "style", "sub", "sup", "td", "th", "title", "u", "var",
7184];
7185
7186#[allow(clippy::too_many_lines)] fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
7189 fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
7191 NodeCtor::Semantic {
7192 suffix: suffix.into(),
7193 args,
7194 consumes_text,
7195 skip_caption: false,
7196 }
7197 }
7198
7199 let aria = node_aria_label(node);
7200 let has_aria = aria.is_some();
7201 let label = aria.unwrap_or_default();
7202 let pure_text = node.has_only_text_children();
7205 let text = node_direct_text(node);
7206 let has_text = !text.is_empty();
7207 let cap = cap_first(tag);
7208
7209 if WITH_TEXT_TAGS.contains(&tag) {
7211 if pure_text && has_text {
7212 return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
7213 }
7214 return NodeCtor::Plain;
7215 }
7216
7217 match tag {
7218 "details" | "form" | "fieldset" | "legend" | "menu" | "output"
7220 | "datalist" | "canvas" | "audio" | "video" | "area" => {
7221 if has_aria {
7222 sem(cap, vec![CtorArg::Aria(label)], false)
7223 } else {
7224 sem(format!("{cap}NoA11y"), vec![], false)
7225 }
7226 }
7227 "summary" => {
7229 if pure_text && has_text {
7230 if has_aria {
7231 sem("SummaryWithText", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7232 } else {
7233 sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
7234 }
7235 } else if has_aria {
7236 sem("Summary", vec![CtorArg::Aria(label)], false)
7237 } else {
7238 sem("SummaryNoA11y", vec![], false)
7239 }
7240 }
7241
7242 "button" => {
7244 if has_aria {
7245 sem("Button", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7246 } else {
7247 sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
7248 }
7249 }
7250 "a" => {
7251 let href = node_attr_or(node, "href", "");
7252 if has_aria {
7253 sem("A", vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)], true)
7254 } else {
7255 let lbl = if has_text { CtorArg::OptSome(text) } else { CtorArg::OptNone };
7256 sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
7257 }
7258 }
7259 "label" => {
7260 let for_id = node_attr_or(node, "for", "");
7261 if has_aria {
7262 sem("Label", vec![CtorArg::Str(for_id), CtorArg::Str(text), CtorArg::Aria(label)], true)
7263 } else {
7264 sem("LabelNoA11y", vec![CtorArg::Str(for_id), CtorArg::Str(text)], true)
7265 }
7266 }
7267 "input" => {
7268 let ty = node_attr_or(node, "type", "text");
7269 let name = node_attr_or(node, "name", "");
7270 if has_aria {
7271 sem("Input", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7272 } else {
7273 sem("InputNoA11y", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)], false)
7274 }
7275 }
7276 "textarea" => {
7277 let name = node_attr_or(node, "name", "");
7278 if has_aria {
7279 sem("Textarea", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7280 } else {
7281 sem("TextareaNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7282 }
7283 }
7284 "select" => {
7285 let name = node_attr_or(node, "name", "");
7286 if has_aria {
7287 sem("Select", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7288 } else {
7289 sem("SelectNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7290 }
7291 }
7292 "option" => {
7293 let value = node_attr_or(node, "value", "");
7294 if has_aria {
7295 sem("Option", vec![CtorArg::Str(value), CtorArg::Str(text), CtorArg::Aria(label)], true)
7296 } else {
7297 sem("OptionNoA11y", vec![CtorArg::Str(value), CtorArg::Str(text)], true)
7298 }
7299 }
7300 "optgroup" => {
7301 let lbl = node_attr_or(node, "label", "");
7302 if has_aria {
7303 sem("Optgroup", vec![CtorArg::Str(lbl), CtorArg::Aria(label)], false)
7304 } else {
7305 sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
7306 }
7307 }
7308 "table" => {
7309 if has_aria {
7310 let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
7313 NodeCtor::Semantic {
7314 suffix: "Table".to_string(),
7315 args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
7316 consumes_text: false,
7317 skip_caption: true,
7318 }
7319 } else {
7320 sem("TableNoA11y", vec![], false)
7321 }
7322 }
7323
7324 "progress" => sem(
7326 "ProgressNoA11y",
7327 vec![
7328 CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7329 CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7330 ],
7331 false,
7332 ),
7333 "meter" => sem(
7334 "MeterNoA11y",
7335 vec![
7336 CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7337 CtorArg::Float(node_attr_f32(node, "min", 0.0)),
7338 CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7339 ],
7340 false,
7341 ),
7342 "dialog" => sem("DialogNoA11y", vec![], false),
7343
7344 _ => NodeCtor::Plain,
7345 }
7346}
7347
7348impl CtorArg {
7349 fn render_rust(&self) -> String {
7352 match self {
7353 Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
7354 Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
7355 Self::Float(f) => fmt_f32_lit(*f),
7356 Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
7357 Self::OptNone => "OptionString::None".to_string(),
7358 }
7359 }
7360 fn render_c(&self) -> String {
7361 match self {
7362 Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
7363 Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
7364 Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7365 Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
7366 Self::OptNone => "AzOptionString_none()".to_string(),
7367 }
7368 }
7369 fn render_cpp(&self) -> String {
7370 match self {
7371 Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
7372 Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
7373 Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7374 Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
7375 Self::OptNone => "OptionString::none()".to_string(),
7376 }
7377 }
7378 fn render_python(&self) -> String {
7379 match self {
7380 Self::Str(s) => format!("\"{}\"", esc_lit(s)),
7381 Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
7382 Self::Float(f) => fmt_f32_lit(*f),
7383 Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
7384 Self::OptNone => "azul.OptionString.none()".to_string(),
7385 }
7386 }
7387}
7388
7389impl NodeCtor {
7390 const fn consumes_text(&self) -> bool {
7391 matches!(self, Self::Semantic { consumes_text: true, .. })
7392 }
7393 const fn skip_caption(&self) -> bool {
7394 matches!(self, Self::Semantic { skip_caption: true, .. })
7395 }
7396 fn render_rust(&self) -> Option<String> {
7398 match self {
7399 Self::Plain => None,
7400 Self::Semantic { suffix, args, .. } => Some(format!(
7401 "Dom::create_{}({})",
7402 camel_to_snake(suffix),
7403 args.iter().map(CtorArg::render_rust).collect::<Vec<_>>().join(", ")
7404 )),
7405 }
7406 }
7407 fn render_c(&self) -> Option<String> {
7409 match self {
7410 Self::Plain => None,
7411 Self::Semantic { suffix, args, .. } => Some(format!(
7412 "AzDom_create{}({})",
7413 suffix,
7414 args.iter().map(CtorArg::render_c).collect::<Vec<_>>().join(", ")
7415 )),
7416 }
7417 }
7418 fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
7420 match self {
7421 Self::Plain => None,
7422 Self::Semantic { suffix, args, .. } => {
7423 let snake = camel_to_snake(suffix);
7424 let (prefix, rendered) = match target {
7425 CompileTarget::Cpp => (
7426 format!("Dom::create_{snake}"),
7427 args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
7428 ),
7429 CompileTarget::Python => (
7430 format!("azul.Dom.create_{snake}"),
7431 args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
7432 ),
7433 _ => return None,
7434 };
7435 Some(format!("{}({})", prefix, rendered.join(", ")))
7436 }
7437 }
7438 }
7439}
7440
7441struct FluentSyntax {
7444 target: CompileTarget,
7445 create_node: fn(&str) -> String,
7447 create_text: fn(&str) -> String,
7449 with_css: fn(&str) -> String,
7451 with_class: fn(&str) -> String,
7453 with_id: fn(&str) -> String,
7455 with_child: fn(&str) -> String,
7457}
7458
7459const CPP_SYNTAX: FluentSyntax = FluentSyntax {
7460 target: CompileTarget::Cpp,
7461 create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
7465 create_text: |s| alloc::format!("Dom::create_text(String(\"{s}\"))"),
7466 with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
7467 with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
7468 with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
7469 with_child: |c| alloc::format!(".with_child({c})"),
7470};
7471
7472const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
7473 target: CompileTarget::Python,
7474 create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
7476 create_text: |s| alloc::format!("azul.Dom.create_text(\"{s}\")"),
7477 with_css: |s| alloc::format!(".with_css(\"{s}\")"),
7478 with_class: |s| alloc::format!(".with_class(\"{s}\")"),
7479 with_id: |s| alloc::format!(".with_id(\"{s}\")"),
7480 with_child: |c| alloc::format!(".with_child({c})"),
7481};
7482
7483#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
7488fn compile_node_fluent(
7489 node: &XmlNode,
7490 syntax: &FluentSyntax,
7491 component_map: &ComponentMap,
7492 css: &Css,
7493 mut matcher: CssMatcher,
7494) -> Result<String, CompileError> {
7495 use azul_css::css::CssDeclaration;
7496
7497 let component_name = normalize_casing(&node.node_type);
7498 let node_type_tag = tag_to_node_type_tag(&component_name);
7499 let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7500
7501 let ctor = analyze_node_ctor(&component_name, node);
7508 let mut s = ctor.render_fluent(&syntax.target).map_or_else(|| (syntax.create_node)(safe_container_tag(&tag_dbg)), |expr| expr);
7509
7510 matcher.path.push(CssPathSelector::Type(node_type_tag));
7511 let ids: Vec<String> = node.attributes.get_key("id")
7512 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7513 .unwrap_or_default();
7514 matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7515 let classes: Vec<String> = node.attributes.get_key("class")
7516 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7517 .unwrap_or_default();
7518 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7519
7520 let blocks = get_css_blocks(css, &matcher);
7522 if !blocks.is_empty() {
7523 let inline_css = css_blocks_to_inline_string(&blocks);
7524 if !inline_css.is_empty() {
7525 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7526 s.push_str(&(syntax.with_css)(&esc));
7527 }
7528 }
7529 for id in &ids {
7530 s.push_str(&(syntax.with_id)(&id.replace('\\', "\\\\").replace('"', "\\\"")));
7531 }
7532 for class in &classes {
7533 s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7534 }
7535
7536 let mut caption_skipped = false;
7539 for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7540 match child {
7541 XmlNodeChild::Element(child_node) => {
7542 if ctor.skip_caption()
7543 && !caption_skipped
7544 && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7545 {
7546 caption_skipped = true;
7547 continue;
7548 }
7549 let mut m = matcher.clone();
7550 m.path.push(CssPathSelector::Children);
7551 m.indices_in_parent.push(child_idx);
7552 m.children_length.push(node.children.len());
7553 let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7554 s.push_str(&(syntax.with_child)(&child_src));
7555 }
7556 XmlNodeChild::Text(text) => {
7557 if ctor.consumes_text() {
7558 continue;
7559 }
7560 let text = text.trim();
7561 if !text.is_empty() {
7562 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7563 s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7564 }
7565 }
7566 }
7567 }
7568
7569 Ok(s)
7570}
7571
7572#[allow(clippy::result_large_err)] fn compile_body_fluent<'a>(
7575 body_node: &'a XmlNode,
7576 syntax: &FluentSyntax,
7577 component_map: &'a ComponentMap,
7578 css: &Css,
7579 mut matcher: CssMatcher,
7580) -> Result<String, CompileError> {
7581 let mut s = (syntax.create_node)("Body");
7582 matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7583 let classes: Vec<String> = body_node.attributes.get_key("class")
7584 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7585 .unwrap_or_default();
7586 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7587
7588 let blocks = get_css_blocks(css, &matcher);
7589 if !blocks.is_empty() {
7590 let inline_css = css_blocks_to_inline_string(&blocks);
7591 if !inline_css.is_empty() {
7592 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7593 s.push_str(&(syntax.with_css)(&esc));
7594 }
7595 }
7596 for class in &classes {
7597 s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7598 }
7599
7600 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7601 match child {
7602 XmlNodeChild::Element(child_node) => {
7603 let mut m = matcher.clone();
7604 m.path.push(CssPathSelector::Children);
7605 m.indices_in_parent.push(child_idx);
7606 m.children_length.push(body_node.children.len());
7607 let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7608 s.push_str(&(syntax.with_child)(&child_src));
7609 }
7610 XmlNodeChild::Text(text) => {
7611 let text = text.trim();
7612 if !text.is_empty() {
7613 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7614 s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7615 }
7616 }
7617 }
7618 }
7619 Ok(s)
7620}
7621
7622#[allow(clippy::result_large_err)] fn parse_page_style_and_body(
7626 root_nodes: &[XmlNodeChild],
7627) -> Result<(Css, &XmlNode), CompileError> {
7628 let html_node = get_html_node(root_nodes)?;
7629 let body_node = get_body_node(html_node.children.as_ref())?;
7630 let mut global_style = Css::empty();
7631 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
7632 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
7633 let text = style_node.get_text_content();
7634 if !text.is_empty() {
7635 global_style = azul_css::parser2::new_from_str(&text).0;
7636 }
7637 }
7638 }
7639 global_style.sort_by_specificity();
7640 Ok((global_style, body_node))
7641}
7642
7643fn body_matcher(body_node: &XmlNode) -> CssMatcher {
7644 CssMatcher {
7645 path: Vec::new(),
7646 indices_in_parent: vec![0],
7647 children_length: vec![body_node.children.as_ref().len()],
7648 }
7649}
7650
7651#[allow(clippy::result_large_err)] pub fn str_to_cpp_code<'a>(
7657 root_nodes: &'a [XmlNodeChild],
7658 component_map: &'a ComponentMap,
7659) -> Result<String, CompileError> {
7660 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7661 let render = compile_body_fluent(body_node, &CPP_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7662 Ok(alloc::format!(
7663 "// Auto-generated UI source code (C++). Build:\n\
7664 // clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
7665 #include \"azul20.hpp\"\n\
7666 using namespace azul;\n\n\
7667 struct Data {{}};\n\n\
7668 AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n \
7669 return {render};\n}}\n\n\
7670 int main() {{\n \
7671 RefAny data = RefAny::create(Data{{}});\n \
7672 WindowCreateOptions window = WindowCreateOptions::create(render);\n \
7673 App app = App::create(std::move(data), AppConfig::default_());\n \
7674 app.run(std::move(window));\n \
7675 return 0;\n}}\n"
7676 ))
7677}
7678
7679#[allow(clippy::result_large_err)] pub fn str_to_python_code<'a>(
7685 root_nodes: &'a [XmlNodeChild],
7686 component_map: &'a ComponentMap,
7687) -> Result<String, CompileError> {
7688 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7689 let render = compile_body_fluent(body_node, &PYTHON_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7690 Ok(alloc::format!(
7691 "# Auto-generated UI source code (Python). Run: python3 main.py\n\
7692 import azul\n\n\
7693 class Data:\n pass\n\n\
7694 def render(data, info):\n return (\n {}\n )\n\n\
7695 def main():\n \
7696 app = azul.App.create(Data(), azul.AppConfig.create())\n \
7697 window = azul.WindowCreateOptions.create(render)\n \
7698 app.run(window)\n\n\
7699 if __name__ == \"__main__\":\n main()\n",
7700 render.replace("\r\n", "\n ")
7701 ))
7702}
7703
7704fn c_creator_suffix(tag_dbg: &str) -> String {
7715 let mut chars = tag_dbg.chars();
7716 chars.next().map_or_else(|| "Div".to_string(), |first| {
7717 let rest: String = chars.as_str().to_lowercase();
7718 alloc::format!("{first}{rest}")
7719 })
7720}
7721
7722#[allow(clippy::result_large_err)] fn compile_node_c(
7724 node: &XmlNode,
7725 component_map: &ComponentMap,
7726 css: &Css,
7727 mut matcher: CssMatcher,
7728 counter: &mut usize,
7729 out: &mut String,
7730) -> Result<String, CompileError> {
7731 let _ = component_map;
7732 let component_name = normalize_casing(&node.node_type);
7733 let node_type_tag = tag_to_node_type_tag(&component_name);
7734 let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7735
7736 let var = alloc::format!("n{}", *counter);
7737 *counter += 1;
7738 let ctor = analyze_node_ctor(&component_name, node);
7739 match ctor.render_c() {
7740 Some(expr) => { let _ = writeln!(out, " AzDom {var} = {expr};"); },
7741 None => { let _ = writeln!(out,
7742 " AzDom {} = AzDom_create{}();",
7743 var,
7744 c_creator_suffix(safe_container_tag(&tag_dbg))
7745 ); },
7746 }
7747
7748 matcher.path.push(CssPathSelector::Type(node_type_tag));
7749 let ids: Vec<String> = node.attributes.get_key("id")
7750 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7751 .unwrap_or_default();
7752 matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7753 let classes: Vec<String> = node.attributes.get_key("class")
7754 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7755 .unwrap_or_default();
7756 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7757
7758 let blocks = get_css_blocks(css, &matcher);
7759 if !blocks.is_empty() {
7760 let inline_css = css_blocks_to_inline_string(&blocks);
7761 if !inline_css.is_empty() {
7762 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7763 let _ = writeln!(out, " {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
7764 }
7765 }
7766 for id in &ids {
7767 let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
7768 let _ = writeln!(out, " {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
7769 }
7770 for class in &classes {
7771 let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
7772 let _ = writeln!(out, " {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));");
7773 }
7774
7775 let mut caption_skipped = false;
7776 for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7777 match child {
7778 XmlNodeChild::Element(child_node) => {
7779 if ctor.skip_caption()
7780 && !caption_skipped
7781 && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7782 {
7783 caption_skipped = true;
7784 continue;
7785 }
7786 let mut m = matcher.clone();
7787 m.path.push(CssPathSelector::Children);
7788 m.indices_in_parent.push(child_idx);
7789 m.children_length.push(node.children.len());
7790 let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
7791 let _ = writeln!(out, " AzDom_addChild(&{var}, {child_var});");
7792 }
7793 XmlNodeChild::Text(text) => {
7794 if ctor.consumes_text() {
7795 continue;
7796 }
7797 let text = text.trim();
7798 if !text.is_empty() {
7799 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7800 let _ = writeln!(out,
7801 " AzDom_addChild(&{var}, AzDom_createText(AZ_STR(\"{esc}\")));"
7802 );
7803 }
7804 }
7805 }
7806 }
7807 Ok(var)
7808}
7809
7810#[allow(clippy::result_large_err)] pub fn str_to_c_code<'a>(
7816 root_nodes: &'a [XmlNodeChild],
7817 component_map: &'a ComponentMap,
7818) -> Result<String, CompileError> {
7819 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7820 let mut body = String::new();
7821 let mut counter = 0usize;
7822
7823 let root = alloc::format!("n{counter}");
7825 counter += 1;
7826 let _ = writeln!(body, " AzDom {root} = AzDom_createBody();");
7827
7828 let mut matcher = body_matcher(body_node);
7829 matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7830 let classes: Vec<String> = body_node.attributes.get_key("class")
7831 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7832 .unwrap_or_default();
7833 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7834 let blocks = get_css_blocks(&global_style, &matcher);
7835 if !blocks.is_empty() {
7836 let inline_css = css_blocks_to_inline_string(&blocks);
7837 if !inline_css.is_empty() {
7838 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7839 let _ = writeln!(body, " {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));");
7840 }
7841 }
7842 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7843 match child {
7844 XmlNodeChild::Element(child_node) => {
7845 let mut m = matcher.clone();
7846 m.path.push(CssPathSelector::Children);
7847 m.indices_in_parent.push(child_idx);
7848 m.children_length.push(body_node.children.len());
7849 let child_var = compile_node_c(child_node, component_map, &global_style, m, &mut counter, &mut body)?;
7850 let _ = writeln!(body, " AzDom_addChild(&{root}, {child_var});");
7851 }
7852 XmlNodeChild::Text(text) => {
7853 let text = text.trim();
7854 if !text.is_empty() {
7855 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7856 let _ = writeln!(body,
7857 " AzDom_addChild(&{root}, AzDom_createText(AZ_STR(\"{esc}\")));"
7858 );
7859 }
7860 }
7861 }
7862 }
7863
7864 Ok(alloc::format!(
7865 "/* Auto-generated UI source code (C). Build:\n\
7866 * clang -I <azul>/target/codegen main.c -lazul\n */\n\
7867 #include \"azul.h\"\n\
7868 #include <string.h>\n\
7869 #define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
7870 AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
7871 {body} return {root};\n}}\n\n\
7872 int main(void) {{\n \
7873 AzString data_type = AZ_STR(\"Data\");\n \
7874 AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n \
7875 AzApp app = AzApp_create(data, AzAppConfig_create());\n \
7876 AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n \
7877 AzApp_run(&app, window);\n \
7878 AzApp_delete(&app);\n \
7879 return 0;\n}}\n"
7880 ))
7881}
7882
7883#[cfg(test)]
7884mod tests {
7885 use super::*;
7886 use crate::dom::{Dom, NodeType};
7887
7888 #[test]
7889 fn test_inline_span_parsing() {
7890 let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
7894
7895 let expected_dom = Dom::create_p().with_children(
7905 vec![
7906 Dom::create_text("Text before "),
7907 Dom::create_node(NodeType::Span)
7908 .with_children(vec![Dom::create_text("inline text")].into()),
7909 Dom::create_text(" text after."),
7910 ]
7911 .into(),
7912 );
7913
7914 assert_eq!(expected_dom.children.as_ref().len(), 3);
7916
7917 match &expected_dom.children.as_ref()[1].root.node_type {
7919 NodeType::Span => {}
7920 other => panic!("Expected Span, got {:?}", other),
7921 }
7922
7923 assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
7925
7926 println!("Test passed: Inline span parsing structure is correct");
7927 }
7928
7929 #[test]
7930 fn test_xml_node_structure() {
7931 let node = XmlNode {
7935 node_type: "p".into(),
7936 attributes: XmlAttributeMap {
7937 inner: StringPairVec::from_const_slice(&[]),
7938 },
7939 children: vec![
7940 XmlNodeChild::Text("Before ".into()),
7941 XmlNodeChild::Element(XmlNode {
7942 node_type: "span".into(),
7943 children: vec![XmlNodeChild::Text("inline".into())].into(),
7944 ..Default::default()
7945 }),
7946 XmlNodeChild::Text(" after".into()),
7947 ]
7948 .into(),
7949 };
7950
7951 assert_eq!(node.children.as_ref().len(), 3);
7953 assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
7954 assert_eq!(
7955 node.children.as_ref()[1]
7956 .as_element()
7957 .unwrap()
7958 .node_type
7959 .as_str(),
7960 "span"
7961 );
7962 assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
7963
7964 let span = node.children.as_ref()[1].as_element().unwrap();
7966 assert_eq!(span.children.as_ref().len(), 1);
7967 assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
7968
7969 println!("Test passed: XmlNode structure preserves text nodes correctly");
7970 }
7971
7972 #[test]
7973 fn test_img_tag_becomes_image_node_with_src_tag() {
7974 use crate::resources::DecodedImage;
7979 use crate::window::{AzStringPair, StringPairVec};
7980
7981 let img_node = XmlNode {
7982 node_type: "img".into(),
7983 attributes: XmlAttributeMap::from(StringPairVec::from_vec(alloc::vec![
7984 AzStringPair {
7985 key: "src".into(),
7986 value: "cat.jpg".into()
7987 },
7988 AzStringPair {
7989 key: "width".into(),
7990 value: "300".into()
7991 },
7992 AzStringPair {
7993 key: "height".into(),
7994 value: "169".into()
7995 },
7996 ])),
7997 children: Vec::new().into(),
7998 };
7999
8000 let component_map = ComponentMap::default();
8001 let dom = xml_node_to_dom_fast(&img_node, &component_map, false, 0)
8002 .expect("xml_node_to_dom_fast for <img> should succeed");
8003
8004 match dom.root.get_node_type() {
8005 NodeType::Image(image_ref) => match image_ref.as_ref().get_data() {
8006 DecodedImage::NullImage {
8007 tag, width, height, ..
8008 } => {
8009 assert_eq!(
8010 core::str::from_utf8(tag).unwrap(),
8011 "cat.jpg",
8012 "image tag must carry the src string"
8013 );
8014 assert_eq!(*width, 300, "width attribute should set intrinsic width");
8015 assert_eq!(*height, 169, "height attribute should set intrinsic height");
8016 }
8017 other => panic!("expected NullImage carrying the src tag, got {:?}", other),
8018 },
8019 other => panic!("expected NodeType::Image for <img>, got {:?}", other),
8020 }
8021
8022 println!("Test passed: <img src=\"cat.jpg\"> -> NodeType::Image tagged \"cat.jpg\"");
8023 }
8024
8025 #[test]
8026 fn test_tag_to_node_type_img_is_image() {
8027 match tag_to_node_type("img") {
8029 NodeType::Image(_) => {}
8030 other => panic!("tag_to_node_type(\"img\") should be Image, got {:?}", other),
8031 }
8032 }
8033
8034 fn nested_divs(depth: usize) -> XmlNode {
8036 let mut node = XmlNode {
8037 node_type: "div".into(),
8038 ..Default::default()
8039 };
8040 for _ in 0..depth {
8041 node = XmlNode {
8042 node_type: "div".into(),
8043 children: vec![XmlNodeChild::Element(node)].into(),
8044 ..Default::default()
8045 };
8046 }
8047 node
8048 }
8049
8050 #[test]
8055 fn extract_css_urls_unicode_import_no_panic() {
8056 let mut res = Vec::new();
8057 Xml::extract_css_urls("İ@import 'x'", &mut res);
8058 assert_eq!(res.len(), 1, "should find the one @import target");
8059 assert_eq!(res[0].url.as_str(), "x");
8060 }
8061
8062 #[test]
8064 fn extract_css_urls_is_case_insensitive() {
8065 let mut res = Vec::new();
8066 Xml::extract_css_urls("body { background: URL(http://e.com/a.png); }", &mut res);
8067 assert_eq!(res.len(), 1);
8068 assert_eq!(res[0].url.as_str(), "http://e.com/a.png");
8069
8070 let mut res2 = Vec::new();
8071 Xml::extract_css_urls("@IMPORT \"theme.css\";", &mut res2);
8072 assert_eq!(res2.len(), 1);
8073 assert_eq!(res2[0].url.as_str(), "theme.css");
8074 }
8075
8076 #[test]
8079 fn scan_external_resources_deep_nesting_ok() {
8080 let xml = Xml {
8081 root: vec![XmlNodeChild::Element(nested_divs(2000))].into(),
8082 };
8083 drop(xml.scan_external_resources());
8085 }
8086
8087 #[test]
8091 fn xml_node_to_dom_fast_deep_nesting_ok() {
8092 let deep = nested_divs(2000);
8093 let component_map = ComponentMap::default();
8094 let dom = xml_node_to_dom_fast(&deep, &component_map, false, 0);
8095 assert!(dom.is_ok(), "deep DOM build must not overflow the stack");
8096
8097 let mut builder = CompactDomBuilder::new();
8098 let fast = xml_node_to_fast_dom(&deep, &component_map, false, &mut builder, 0);
8099 assert!(fast.is_ok(), "deep FastDom build must not overflow the stack");
8100 }
8101
8102 #[test]
8106 fn component_field_type_parse_depth_capped() {
8107 let deep = format!("{}Bool{}", "Option<".repeat(4000), ">".repeat(4000));
8108 assert!(
8109 ComponentFieldType::parse(&deep).is_none(),
8110 "over-deep type string must be rejected, not overflow"
8111 );
8112
8113 let shallow = format!("{}Bool{}", "Option<".repeat(8), ">".repeat(8));
8114 assert!(
8115 ComponentFieldType::parse(&shallow).is_some(),
8116 "ordinary nesting must still parse"
8117 );
8118 }
8119
8120 #[test]
8123 fn prepare_string_entity_decoding() {
8124 assert_eq!(prepare_string("a & b"), "a & b");
8125 assert_eq!(prepare_string("&lt;"), "<");
8127 assert_eq!(prepare_string(""hi""), "\"hi\"");
8128 assert_eq!(prepare_string("AB"), "AB");
8129 assert_eq!(prepare_string("A"), "A");
8130 assert_eq!(prepare_string("<tag>"), "<tag>");
8132 }
8133}
8134
8135#[cfg(test)]
8136#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
8137mod autotest_generated {
8138 use super::*;
8139 use crate::dom::{NodeData, NodeType};
8140 use azul_css::css::{CssNthChildPattern, CssNthChildSelector};
8141
8142 fn attrs(kv: &[(&str, &str)]) -> XmlAttributeMap {
8145 XmlAttributeMap::from(StringPairVec::from_vec(
8146 kv.iter()
8147 .map(|(k, v)| AzStringPair {
8148 key: AzString::from(*k),
8149 value: AzString::from(*v),
8150 })
8151 .collect::<Vec<_>>(),
8152 ))
8153 }
8154
8155 fn node(tag: &str, kv: &[(&str, &str)], children: Vec<XmlNodeChild>) -> XmlNode {
8156 XmlNode {
8157 node_type: tag.into(),
8158 attributes: attrs(kv),
8159 children: children.into(),
8160 }
8161 }
8162
8163 fn txt(s: &str) -> XmlNodeChild {
8164 XmlNodeChild::Text(AzString::from(s))
8165 }
8166
8167 fn elem(n: XmlNode) -> XmlNodeChild {
8168 XmlNodeChild::Element(n)
8169 }
8170
8171 fn doc(css: &str, body_children: Vec<XmlNodeChild>) -> Vec<XmlNodeChild> {
8173 let style = node("style", &[], vec![txt(css)]);
8174 let head = node("head", &[], vec![elem(style)]);
8175 let body = node("body", &[], body_children);
8176 vec![elem(node("html", &[], vec![elem(head), elem(body)]))]
8177 }
8178
8179 fn no_args() -> ComponentArgumentVec {
8180 ComponentArgumentVec::from_const_slice(&[])
8181 }
8182
8183 fn dm(name: &str, fields: Vec<ComponentDataField>) -> ComponentDataModel {
8184 ComponentDataModel {
8185 name: AzString::from(name),
8186 description: AzString::from_const_str(""),
8187 fields: fields.into(),
8188 }
8189 }
8190
8191 fn user_def(css: &str, fields: Vec<ComponentDataField>) -> ComponentDef {
8192 ComponentDef {
8193 id: ComponentId::new("mylib", "widget"),
8194 display_name: AzString::from_const_str("Widget"),
8195 description: AzString::from_const_str(""),
8196 css: AzString::from(css),
8197 source: ComponentSource::UserDefined,
8198 data_model: dm("WidgetData", fields),
8199 render_fn: user_defined_render_fn,
8200 compile_fn: user_defined_compile_fn,
8201 render_fn_source: None.into(),
8202 compile_fn_source: None.into(),
8203 }
8204 }
8205
8206 const LONG: usize = 200_000;
8209
8210 #[test]
8215 fn extract_url_value_empty_and_whitespace() {
8216 assert_eq!(Xml::extract_url_value(""), None);
8217 assert_eq!(Xml::extract_url_value(" "), None);
8218 assert_eq!(Xml::extract_url_value("\t\n\r "), None);
8219 }
8220
8221 #[test]
8222 fn extract_url_value_valid_minimal() {
8223 assert_eq!(
8224 Xml::extract_url_value("a.png)"),
8225 Some("a.png".to_string()),
8226 "unquoted url terminated by ')'"
8227 );
8228 assert_eq!(
8229 Xml::extract_url_value("\"a.png\")"),
8230 Some("a.png".to_string()),
8231 "double-quoted url"
8232 );
8233 assert_eq!(
8234 Xml::extract_url_value("'a.png')"),
8235 Some("a.png".to_string()),
8236 "single-quoted url"
8237 );
8238 }
8239
8240 #[test]
8241 fn extract_url_value_leading_trailing_junk_is_trimmed() {
8242 assert_eq!(
8244 Xml::extract_url_value(" a.png )tail"),
8245 Some("a.png".to_string())
8246 );
8247 }
8248
8249 #[test]
8250 fn extract_url_value_garbage_returns_none() {
8251 assert_eq!(Xml::extract_url_value("\"unterminated"), None);
8253 assert_eq!(Xml::extract_url_value("'unterminated"), None);
8254 assert_eq!(Xml::extract_url_value("no-closing-paren"), None);
8255 assert_eq!(Xml::extract_url_value("\u{0}\u{1}\u{7f}"), None);
8256 }
8257
8258 #[test]
8259 fn extract_url_value_boundary_numbers() {
8260 for s in [
8261 "0)",
8262 "-0)",
8263 "9223372036854775807)",
8264 "-9223372036854775808)",
8265 "NaN)",
8266 "inf)",
8267 "1e400)",
8268 ] {
8269 let got = Xml::extract_url_value(s);
8270 assert!(got.is_some(), "numeric-looking url {s:?} is still a url");
8271 }
8272 assert_eq!(Xml::extract_url_value("0)"), Some("0".to_string()));
8273 }
8274
8275 #[test]
8276 fn extract_url_value_unicode_no_panic() {
8277 assert_eq!(
8279 Xml::extract_url_value("\u{1F600}\u{0301})"),
8280 Some("\u{1F600}\u{0301}".to_string())
8281 );
8282 assert_eq!(Xml::extract_url_value("\"\u{130}\")"), Some("\u{130}".to_string()));
8283 assert_eq!(Xml::extract_url_value("\u{1F600}"), None);
8284 }
8285
8286 #[test]
8287 fn extract_url_value_extremely_long_terminates() {
8288 let s = "a".repeat(LONG);
8289 assert_eq!(Xml::extract_url_value(&s), None, "no ')' anywhere => None");
8290 let s2 = format!("{})", "b".repeat(LONG));
8291 assert_eq!(Xml::extract_url_value(&s2).map(|v| v.len()), Some(LONG));
8292 }
8293
8294 #[test]
8295 fn extract_url_value_nested_brackets_no_stack_overflow() {
8296 let s = "(".repeat(10_000);
8298 assert_eq!(Xml::extract_url_value(&s), None);
8299 let s2 = format!("{}{}", "(".repeat(10_000), ")");
8300 assert_eq!(Xml::extract_url_value(&s2), Some("(".repeat(10_000)));
8301 }
8302
8303 #[test]
8308 fn extract_quoted_string_empty_whitespace_garbage() {
8309 assert_eq!(Xml::extract_quoted_string(""), None);
8310 assert_eq!(Xml::extract_quoted_string(" "), None);
8311 assert_eq!(Xml::extract_quoted_string("\t\n"), None);
8312 assert_eq!(Xml::extract_quoted_string("bare"), None);
8313 assert_eq!(Xml::extract_quoted_string(" \"x\""), None);
8315 }
8316
8317 #[test]
8318 fn extract_quoted_string_valid_minimal_and_empty_quotes() {
8319 assert_eq!(Xml::extract_quoted_string("\"x\""), Some("x".to_string()));
8320 assert_eq!(Xml::extract_quoted_string("'x'"), Some("x".to_string()));
8321 assert_eq!(Xml::extract_quoted_string("\"\""), Some(String::new()));
8323 assert_eq!(Xml::extract_quoted_string("''"), Some(String::new()));
8324 }
8325
8326 #[test]
8327 fn extract_quoted_string_unterminated_is_none() {
8328 assert_eq!(Xml::extract_quoted_string("\"abc"), None);
8329 assert_eq!(Xml::extract_quoted_string("'abc"), None);
8330 assert_eq!(Xml::extract_quoted_string("\"abc'"), None);
8332 }
8333
8334 #[test]
8335 fn extract_quoted_string_boundary_numbers_and_unicode() {
8336 assert_eq!(Xml::extract_quoted_string("\"0\""), Some("0".to_string()));
8337 assert_eq!(Xml::extract_quoted_string("\"-0\""), Some("-0".to_string()));
8338 assert_eq!(Xml::extract_quoted_string("\"NaN\""), Some("NaN".to_string()));
8339 assert_eq!(
8340 Xml::extract_quoted_string("\"\u{1F600}\u{0301}\""),
8341 Some("\u{1F600}\u{0301}".to_string())
8342 );
8343 }
8344
8345 #[test]
8346 fn extract_quoted_string_extremely_long_terminates() {
8347 let unterminated = format!("\"{}", "x".repeat(LONG));
8348 assert_eq!(Xml::extract_quoted_string(&unterminated), None);
8349 let terminated = format!("\"{}\"", "x".repeat(LONG));
8350 assert_eq!(
8351 Xml::extract_quoted_string(&terminated).map(|s| s.len()),
8352 Some(LONG)
8353 );
8354 }
8355
8356 #[test]
8361 fn parse_srcset_empty_and_whitespace_yield_no_urls() {
8362 assert!(Xml::parse_srcset("").is_empty());
8363 assert!(Xml::parse_srcset(" ").is_empty());
8364 assert!(Xml::parse_srcset("\t\n").is_empty());
8365 assert!(Xml::parse_srcset(",,,").is_empty(), "all-empty entries dropped");
8366 }
8367
8368 #[test]
8369 fn parse_srcset_valid_minimal() {
8370 assert_eq!(
8371 Xml::parse_srcset("a.png 1x, b.png 2x"),
8372 vec!["a.png".to_string(), "b.png".to_string()]
8373 );
8374 assert_eq!(Xml::parse_srcset("a.png"), vec!["a.png".to_string()]);
8376 }
8377
8378 #[test]
8379 fn parse_srcset_garbage_and_boundary_numbers() {
8380 assert_eq!(Xml::parse_srcset("0, -0, NaN"), vec!["0", "-0", "NaN"]);
8381 assert_eq!(Xml::parse_srcset("\u{0}\u{7f} 1x"), vec!["\u{0}\u{7f}".to_string()]);
8383 }
8384
8385 #[test]
8386 fn parse_srcset_unicode_no_panic() {
8387 assert_eq!(
8388 Xml::parse_srcset("\u{1F600}.png 1x, \u{130}.png 2x"),
8389 vec!["\u{1F600}.png".to_string(), "\u{130}.png".to_string()]
8390 );
8391 }
8392
8393 #[test]
8394 fn parse_srcset_extremely_long_terminates() {
8395 let s = "a.png 1x,".repeat(20_000);
8396 assert_eq!(Xml::parse_srcset(&s).len(), 20_000);
8397 let one_huge = "a".repeat(LONG);
8398 assert_eq!(Xml::parse_srcset(&one_huge).len(), 1);
8399 }
8400
8401 #[test]
8406 fn looks_like_resource_edges() {
8407 assert!(!Xml::looks_like_resource(""));
8408 assert!(!Xml::looks_like_resource(" "));
8409 assert!(!Xml::looks_like_resource("/about"));
8410 assert!(Xml::looks_like_resource("/a.PNG"), "case-insensitive");
8411 assert!(Xml::looks_like_resource("x.pdf"));
8412 assert!(!Xml::looks_like_resource("x.png?v=1"));
8415 assert!(!Xml::looks_like_resource(&"a".repeat(LONG)));
8416 }
8417
8418 #[test]
8419 fn guess_kind_from_url_covers_every_bucket() {
8420 use ExternalResourceKind::*;
8421 assert_eq!(Xml::guess_kind_from_url(""), Unknown);
8422 assert_eq!(Xml::guess_kind_from_url("a.PNG"), Image);
8423 assert_eq!(Xml::guess_kind_from_url("a.woff2"), Font);
8424 assert_eq!(Xml::guess_kind_from_url("a.css"), Stylesheet);
8425 assert_eq!(Xml::guess_kind_from_url("a.mjs"), Script);
8426 assert_eq!(Xml::guess_kind_from_url("a.webm"), Video);
8427 assert_eq!(Xml::guess_kind_from_url("a.flac"), Audio);
8428 assert_eq!(Xml::guess_kind_from_url("a.ico"), Icon);
8429 assert_eq!(Xml::guess_kind_from_url("a.png?v=1"), Image);
8431 assert_eq!(Xml::guess_kind_from_url("\u{1F600}"), Unknown);
8432 }
8433
8434 #[test]
8435 fn guess_mime_from_url_empty_and_garbage() {
8436 assert_eq!(Xml::guess_mime_from_url("", ""), None);
8437 assert_eq!(Xml::guess_mime_from_url(" ", ""), None);
8438 assert_eq!(Xml::guess_mime_from_url("\u{0}\u{7f}", ""), None);
8439 assert_eq!(Xml::guess_mime_from_url("\u{1F600}", ""), None);
8440 }
8441
8442 #[test]
8443 fn guess_mime_from_url_valid_minimal_and_category_fallback() {
8444 let m = Xml::guess_mime_from_url("a.PNG", "").expect("png is a known extension");
8445 assert_eq!(m.inner.as_str(), "image/png");
8446 let m = Xml::guess_mime_from_url("a.png?v=1", "").expect("query string stripped");
8447 assert_eq!(m.inner.as_str(), "image/png");
8448 let m = Xml::guess_mime_from_url("/no-ext", "image").expect("category fallback");
8450 assert_eq!(m.inner.as_str(), "image/*");
8451 assert_eq!(Xml::guess_mime_from_url("/no-ext", "bogus"), None);
8453 }
8454
8455 #[test]
8456 fn guess_mime_from_url_boundary_numbers_and_long() {
8457 assert_eq!(Xml::guess_mime_from_url("0", ""), None);
8458 assert_eq!(Xml::guess_mime_from_url("-0", ""), None);
8459 assert_eq!(Xml::guess_mime_from_url("NaN", ""), None);
8460 assert_eq!(Xml::guess_mime_from_url("inf", ""), None);
8461 let long = format!("{}.png", "a".repeat(LONG));
8462 assert_eq!(
8463 Xml::guess_mime_from_url(&long, "").map(|m| m.inner.as_str().to_string()),
8464 Some("image/png".to_string())
8465 );
8466 }
8467
8468 #[test]
8473 fn extract_css_urls_empty_and_garbage_no_panic() {
8474 let mut v = Vec::new();
8475 Xml::extract_css_urls("", &mut v);
8476 Xml::extract_css_urls(" ", &mut v);
8477 Xml::extract_css_urls("\u{0}\u{7f}\u{1F600}", &mut v);
8478 Xml::extract_css_urls("url(", &mut v);
8479 Xml::extract_css_urls("@import", &mut v);
8480 Xml::extract_css_urls("@import url(", &mut v);
8481 assert!(v.is_empty(), "no well-formed url in any of those inputs");
8482 }
8483
8484 #[test]
8485 fn extract_css_urls_valid_minimal() {
8486 let mut v = Vec::new();
8487 Xml::extract_css_urls("a { background: url('x.png'); }", &mut v);
8488 assert_eq!(v.len(), 1);
8489 assert_eq!(v[0].url.as_str(), "x.png");
8490 assert_eq!(v[0].kind, ExternalResourceKind::Image);
8491 assert_eq!(v[0].source_attribute.as_str(), "url()");
8492 }
8493
8494 #[test]
8495 fn extract_css_urls_import_is_tagged_as_stylesheet() {
8496 let mut v = Vec::new();
8497 Xml::extract_css_urls("@import url(theme.css);", &mut v);
8498 assert_eq!(v.len(), 1);
8499 assert_eq!(v[0].url.as_str(), "theme.css");
8500 assert_eq!(v[0].kind, ExternalResourceKind::Stylesheet);
8501 assert_eq!(v[0].source_attribute.as_str(), "@import");
8502 }
8503
8504 #[test]
8505 fn extract_css_urls_multibyte_before_url_no_panic() {
8506 let mut v = Vec::new();
8508 Xml::extract_css_urls("\u{130}\u{1F600} URL(\"a.css\") \u{0301}", &mut v);
8509 assert_eq!(v.len(), 1);
8510 assert_eq!(v[0].url.as_str(), "a.css");
8511 }
8512
8513 #[test]
8514 fn extract_css_urls_extremely_long_terminates() {
8515 let mut v = Vec::new();
8518 Xml::extract_css_urls(&"url(".repeat(2_000), &mut v);
8519 assert!(v.is_empty());
8521
8522 let mut v2 = Vec::new();
8523 Xml::extract_css_urls(&"url(a.png)".repeat(2_000), &mut v2);
8524 assert_eq!(v2.len(), 2_000);
8525 }
8526
8527 #[test]
8528 fn scan_node_on_empty_and_extreme_nodes_no_panic() {
8529 let mut v = Vec::new();
8530 Xml::scan_node(&XmlNode::default(), &mut v);
8531 Xml::scan_node(&node("", &[], vec![]), &mut v);
8532 Xml::scan_node(&node(&"a".repeat(10_000), &[("style", "url(x.png)")], vec![]), &mut v);
8533 assert_eq!(v.len(), 1, "only the inline style url()");
8534 assert_eq!(v[0].url.as_str(), "x.png");
8535 }
8536
8537 #[test]
8538 fn scan_node_img_srcset_and_background() {
8539 let mut v = Vec::new();
8540 Xml::scan_node(
8541 &node(
8542 "IMG",
8543 &[("src", "a.png"), ("srcset", "b.png 1x, c.png 2x"), ("background", "d.gif")],
8544 vec![],
8545 ),
8546 &mut v,
8547 );
8548 let urls: Vec<&str> = v.iter().map(|r| r.url.as_str()).collect();
8549 assert_eq!(urls, vec!["a.png", "b.png", "c.png", "d.gif"]);
8550 assert!(v.iter().all(|r| r.kind == ExternalResourceKind::Image));
8551 }
8552
8553 #[test]
8554 fn scan_external_resources_on_empty_document() {
8555 let xml = Xml {
8556 root: Vec::new().into(),
8557 };
8558 assert_eq!(xml.scan_external_resources().as_ref().len(), 0);
8559 }
8560
8561 #[test]
8562 fn scan_external_resources_finds_every_element_kind() {
8563 let xml = Xml {
8564 root: vec![
8565 elem(node("img", &[("src", "i.png")], vec![])),
8566 elem(node("link", &[("href", "s.css"), ("rel", "stylesheet")], vec![])),
8567 elem(node("script", &[("src", "s.js")], vec![])),
8568 elem(node("video", &[("src", "v.mp4"), ("poster", "p.jpg")], vec![])),
8569 elem(node("audio", &[("src", "a.mp3")], vec![])),
8570 elem(node("a", &[("href", "f.pdf")], vec![])),
8571 elem(node("a", &[("href", "/page")], vec![])),
8572 ]
8573 .into(),
8574 };
8575 let res = xml.scan_external_resources();
8576 let mut urls: Vec<&str> = res.as_ref().iter().map(|r| r.url.as_str()).collect();
8577 urls.sort_unstable();
8578 assert_eq!(
8579 urls,
8580 vec!["a.mp3", "f.pdf", "i.png", "p.jpg", "s.css", "s.js", "v.mp4"],
8581 "`/page` is not a resource and must be skipped"
8582 );
8583 }
8584
8585 #[test]
8590 fn mime_type_hint_new_no_panic_and_fields_match_args() {
8591 for s in ["", " ", "text/css", "\u{1F600}", "\u{0}"] {
8592 assert_eq!(MimeTypeHint::new(s).inner.as_str(), s, "new() stores verbatim");
8593 }
8594 let long = "x".repeat(LONG);
8595 assert_eq!(MimeTypeHint::new(&long).inner.as_str().len(), LONG);
8596 }
8597
8598 #[test]
8599 fn mime_type_hint_from_extension_edges() {
8600 assert_eq!(
8601 MimeTypeHint::from_extension("").inner.as_str(),
8602 "application/octet-stream"
8603 );
8604 assert_eq!(
8605 MimeTypeHint::from_extension("PnG").inner.as_str(),
8606 "image/png",
8607 "extension match is case-insensitive"
8608 );
8609 assert_eq!(MimeTypeHint::from_extension("jpeg").inner.as_str(), "image/jpeg");
8610 assert_eq!(MimeTypeHint::from_extension("woff2").inner.as_str(), "font/woff2");
8611 assert_eq!(
8612 MimeTypeHint::from_extension("\u{1F600}").inner.as_str(),
8613 "application/octet-stream"
8614 );
8615 assert_eq!(
8616 MimeTypeHint::from_extension(&"z".repeat(LONG)).inner.as_str(),
8617 "application/octet-stream"
8618 );
8619 }
8620
8621 #[test]
8626 fn component_id_builtin_and_new_invariants() {
8627 let b = ComponentId::builtin("div");
8628 assert_eq!(b.collection.as_str(), "builtin");
8629 assert_eq!(b.name.as_str(), "div");
8630
8631 let c = ComponentId::new("", "");
8632 assert_eq!(c.collection.as_str(), "");
8633 assert_eq!(c.name.as_str(), "");
8634
8635 let u = ComponentId::new("\u{1F600}", "\u{130}");
8636 assert_eq!(u.collection.as_str(), "\u{1F600}");
8637 assert_eq!(u.name.as_str(), "\u{130}");
8638 }
8639
8640 #[test]
8641 fn component_id_qualified_name_roundtrips_through_the_map_lookup() {
8642 assert_eq!(ComponentId::builtin("div").qualified_name(), "builtin:div");
8643 assert_eq!(ComponentId::new("", "").qualified_name(), ":");
8644 assert_eq!(ComponentId::new("a", "b:c").qualified_name(), "a:b:c");
8647 }
8648
8649 #[test]
8654 fn component_field_type_box_new_as_ref_and_clone() {
8655 let b = ComponentFieldTypeBox::new(ComponentFieldType::Bool);
8656 assert!(!b.ptr.is_null());
8657 assert_eq!(*b.as_ref(), ComponentFieldType::Bool);
8658
8659 let c = b.clone();
8660 assert_eq!(*c.as_ref(), ComponentFieldType::Bool);
8661 assert_ne!(b.ptr, c.ptr, "clone must deep-copy, not alias");
8662 assert_eq!(b, c, "PartialEq compares pointees");
8663 drop(c);
8664 assert_eq!(*b.as_ref(), ComponentFieldType::Bool, "original survives");
8665 }
8666
8667 #[test]
8668 fn component_field_type_box_nested_deeply_drops_cleanly() {
8669 let mut t = ComponentFieldType::Bool;
8670 for _ in 0..64 {
8671 t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(t));
8672 }
8673 assert_eq!(t.format(), format!("{}Bool{}", "Option<".repeat(64), ">".repeat(64)));
8674 drop(t);
8675 }
8676
8677 #[test]
8678 fn component_field_value_box_new_as_ref_and_clone() {
8679 let v = ComponentFieldValueBox::new(ComponentFieldValue::I32(i32::MIN));
8680 assert!(!v.ptr.is_null());
8681 assert_eq!(*v.as_ref(), ComponentFieldValue::I32(i32::MIN));
8682 let c = v.clone();
8683 assert_ne!(v.ptr, c.ptr);
8684 assert_eq!(v, c);
8685 }
8686
8687 #[test]
8692 fn component_field_type_parse_empty_whitespace_garbage() {
8693 assert_eq!(ComponentFieldType::parse(""), None);
8694 assert_eq!(ComponentFieldType::parse(" "), None);
8695 assert_eq!(ComponentFieldType::parse("\t\n"), None);
8696 assert_eq!(ComponentFieldType::parse("lowercase"), None);
8697 assert_eq!(ComponentFieldType::parse("\u{0}\u{7f}"), None);
8698 assert_eq!(ComponentFieldType::parse("Option<>"), None, "empty inner rejected");
8699 assert_eq!(ComponentFieldType::parse("Vec<>"), None);
8700 assert_eq!(ComponentFieldType::parse("Option<lowercase>"), None);
8701 }
8702
8703 #[test]
8704 fn component_field_type_parse_valid_minimal_and_trimming() {
8705 assert_eq!(ComponentFieldType::parse("String"), Some(ComponentFieldType::String));
8706 assert_eq!(
8707 ComponentFieldType::parse(" String "),
8708 Some(ComponentFieldType::String),
8709 "leading/trailing whitespace is trimmed"
8710 );
8711 assert_eq!(ComponentFieldType::parse("bool"), Some(ComponentFieldType::Bool));
8712 assert_eq!(ComponentFieldType::parse("usize"), Some(ComponentFieldType::Usize));
8713 assert_eq!(
8714 ComponentFieldType::parse("StructRef(Foo)"),
8715 Some(ComponentFieldType::StructRef(AzString::from("Foo")))
8716 );
8717 assert_eq!(
8718 ComponentFieldType::parse("EnumRef(Foo)"),
8719 Some(ComponentFieldType::EnumRef(AzString::from("Foo")))
8720 );
8721 assert_eq!(
8722 ComponentFieldType::parse("RefAny"),
8723 Some(ComponentFieldType::RefAny(AzString::from("")))
8724 );
8725 }
8726
8727 #[test]
8728 fn component_field_type_parse_leading_trailing_junk_is_rejected_or_absorbed() {
8729 assert_eq!(
8732 ComponentFieldType::parse("String;garbage"),
8733 Some(ComponentFieldType::StructRef(AzString::from("String;garbage")))
8734 );
8735 assert_eq!(ComponentFieldType::parse("string;garbage"), None);
8737 }
8738
8739 #[test]
8740 fn component_field_type_parse_boundary_numbers() {
8741 for s in ["0", "-0", "9223372036854775807", "-9223372036854775808", "1e400", "inf"] {
8742 assert_eq!(
8743 ComponentFieldType::parse(s),
8744 None,
8745 "numeric literal {s:?} is not a type name"
8746 );
8747 }
8748 assert_eq!(
8751 ComponentFieldType::parse("NaN"),
8752 Some(ComponentFieldType::StructRef(AzString::from("NaN")))
8753 );
8754 }
8755
8756 #[test]
8757 fn component_field_type_parse_unicode_no_panic() {
8758 assert_eq!(ComponentFieldType::parse("\u{1F600}"), None, "emoji is not uppercase");
8759 assert_eq!(
8761 ComponentFieldType::parse("\u{0391}bc"),
8762 Some(ComponentFieldType::StructRef(AzString::from("\u{0391}bc")))
8763 );
8764 }
8765
8766 #[test]
8767 fn component_field_type_parse_depth_boundary_is_exact() {
8768 let ok = format!(
8770 "{}Bool{}",
8771 "Option<".repeat(MAX_TYPE_PARSE_DEPTH),
8772 ">".repeat(MAX_TYPE_PARSE_DEPTH)
8773 );
8774 assert!(
8775 ComponentFieldType::parse(&ok).is_some(),
8776 "exactly MAX_TYPE_PARSE_DEPTH wrappers must still parse"
8777 );
8778
8779 let too_deep = format!(
8780 "{}Bool{}",
8781 "Option<".repeat(MAX_TYPE_PARSE_DEPTH + 1),
8782 ">".repeat(MAX_TYPE_PARSE_DEPTH + 1)
8783 );
8784 assert_eq!(
8785 ComponentFieldType::parse(&too_deep),
8786 None,
8787 "one wrapper past the cap must be rejected, not overflow"
8788 );
8789 }
8790
8791 #[test]
8792 fn component_field_type_parse_depth_direct_call_honors_start_depth() {
8793 assert_eq!(
8794 ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH),
8795 Some(ComponentFieldType::Bool),
8796 "depth == cap is still allowed"
8797 );
8798 assert_eq!(
8799 ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH + 1),
8800 None
8801 );
8802 assert_eq!(
8803 ComponentFieldType::parse_depth("Bool", usize::MAX),
8804 None,
8805 "usize::MAX start depth must not overflow, just refuse"
8806 );
8807 }
8808
8809 #[test]
8810 fn component_field_type_parse_nested_recursion_does_not_stack_overflow() {
8811 let bomb = format!("{}Bool{}", "Vec<".repeat(50_000), ">".repeat(50_000));
8812 assert_eq!(ComponentFieldType::parse(&bomb), None);
8813 }
8814
8815 #[test]
8816 fn component_field_type_parse_extremely_long_terminates() {
8817 let long = format!("A{}", "b".repeat(LONG));
8819 assert_eq!(
8820 ComponentFieldType::parse(&long),
8821 Some(ComponentFieldType::StructRef(AzString::from(long.as_str())))
8822 );
8823 }
8824
8825 #[test]
8826 fn component_field_type_round_trip_representative() {
8827 let representative = vec![
8828 ComponentFieldType::String,
8829 ComponentFieldType::Bool,
8830 ComponentFieldType::I32,
8831 ComponentFieldType::I64,
8832 ComponentFieldType::U32,
8833 ComponentFieldType::U64,
8834 ComponentFieldType::Usize,
8835 ComponentFieldType::F32,
8836 ComponentFieldType::F64,
8837 ComponentFieldType::ColorU,
8838 ComponentFieldType::CssProperty,
8839 ComponentFieldType::ImageRef,
8840 ComponentFieldType::FontRef,
8841 ComponentFieldType::StyledDom,
8842 ComponentFieldType::StructRef(AzString::from("Foo")),
8843 ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::Bool)),
8844 ComponentFieldType::VecType(ComponentFieldTypeBox::new(ComponentFieldType::I32)),
8845 ComponentFieldType::RefAny(AzString::from("")),
8846 ComponentFieldType::RefAny(AzString::from("Hint")),
8847 ComponentFieldType::Callback(ComponentCallbackSignature {
8848 return_type: AzString::from("Update"),
8849 args: Vec::new().into(),
8850 }),
8851 ];
8852 for x in representative {
8853 let s = x.format();
8854 assert_eq!(
8855 ComponentFieldType::parse(&s),
8856 Some(x.clone()),
8857 "parse(format({x:?})) must round-trip"
8858 );
8859 }
8860 }
8861
8862 #[test]
8863 fn component_field_type_round_trip_edge_values() {
8864 for x in [
8866 ComponentFieldType::StructRef(AzString::from("\u{0391}\u{1F600}")),
8867 ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
8868 ComponentFieldType::VecType(ComponentFieldTypeBox::new(
8869 ComponentFieldType::StructRef(AzString::from("Foo")),
8870 )),
8871 )),
8872 ] {
8873 assert_eq!(ComponentFieldType::parse(&x.format()), Some(x.clone()));
8874 }
8875 }
8876
8877 #[test]
8878 fn component_field_type_format_is_an_idempotent_normalization() {
8879 let e = ComponentFieldType::EnumRef(AzString::from("Role"));
8883 let once = e.format();
8884 assert_eq!(once, "Role");
8885 let reparsed = ComponentFieldType::parse(&once).expect("parses");
8886 assert_eq!(
8887 reparsed,
8888 ComponentFieldType::StructRef(AzString::from("Role")),
8889 "EnumRef is lossy through format() — it comes back as StructRef"
8890 );
8891 assert_eq!(reparsed.format(), once, "but the normalization is idempotent");
8892 }
8893
8894 #[test]
8895 fn component_field_type_display_matches_format() {
8896 let t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::F64));
8897 assert_eq!(format!("{t}"), t.format());
8898 assert_eq!(format!("{t}"), "Option<F64>");
8899 }
8900
8901 #[test]
8902 fn component_field_type_format_no_panic_on_empty_payloads() {
8903 let t = ComponentFieldType::Callback(ComponentCallbackSignature {
8904 return_type: AzString::from(""),
8905 args: Vec::new().into(),
8906 });
8907 assert_eq!(t.format(), "Callback()");
8908 assert_eq!(ComponentFieldType::parse("Callback()"), Some(t));
8910 }
8911
8912 fn named(name: &str, v: ComponentFieldValue) -> ComponentFieldNamedValue {
8917 ComponentFieldNamedValue {
8918 name: AzString::from(name),
8919 value: v,
8920 }
8921 }
8922
8923 fn named_vec() -> ComponentFieldNamedValueVec {
8924 vec![
8925 named("a", ComponentFieldValue::String(AzString::from("x"))),
8926 named("b", ComponentFieldValue::Bool(true)),
8927 named("", ComponentFieldValue::U64(u64::MAX)),
8928 named("\u{1F600}", ComponentFieldValue::String(AzString::from("emoji"))),
8929 ]
8930 .into()
8931 }
8932
8933 #[test]
8934 fn named_value_vec_get_field_valid_minimal() {
8935 let v = named_vec();
8936 assert_eq!(
8937 v.get_field("a"),
8938 Some(&ComponentFieldValue::String(AzString::from("x")))
8939 );
8940 assert_eq!(v.get_field("b"), Some(&ComponentFieldValue::Bool(true)));
8941 }
8942
8943 #[test]
8944 fn named_value_vec_get_field_empty_whitespace_garbage_unicode() {
8945 let v = named_vec();
8946 assert_eq!(v.get_field(""), Some(&ComponentFieldValue::U64(u64::MAX)));
8948 assert_eq!(v.get_field(" "), None);
8949 assert_eq!(v.get_field("\t\n"), None);
8950 assert_eq!(v.get_field("\u{0}\u{7f}"), None);
8951 assert!(v.get_field("\u{1F600}").is_some());
8952 assert_eq!(v.get_field(" a "), None, "no trimming: lookup is exact");
8953 assert_eq!(v.get_field("a;garbage"), None);
8954 }
8955
8956 #[test]
8957 fn named_value_vec_get_field_on_empty_vec_and_long_key() {
8958 let empty = ComponentFieldNamedValueVec::from_const_slice(&[]);
8959 assert_eq!(empty.get_field("a"), None);
8960 assert_eq!(empty.get_string("a"), None);
8961 assert_eq!(named_vec().get_field(&"z".repeat(LONG)), None);
8962 }
8963
8964 #[test]
8965 fn named_value_vec_get_string_only_matches_string_variant() {
8966 let v = named_vec();
8967 assert_eq!(v.get_string("a").map(AzString::as_str), Some("x"));
8968 assert_eq!(v.get_string("b"), None, "Bool is not a String");
8969 assert_eq!(v.get_string(""), None, "U64 is not a String");
8970 assert_eq!(v.get_string("missing"), None);
8971 }
8972
8973 #[test]
8974 fn named_value_vec_boundary_numeric_keys() {
8975 let v: ComponentFieldNamedValueVec = vec![
8976 named("0", ComponentFieldValue::I32(0)),
8977 named("-0", ComponentFieldValue::I32(i32::MIN)),
8978 named("9223372036854775807", ComponentFieldValue::I64(i64::MAX)),
8979 named("NaN", ComponentFieldValue::F32(f32::NAN)),
8980 ]
8981 .into();
8982 assert_eq!(v.get_field("0"), Some(&ComponentFieldValue::I32(0)));
8983 assert_eq!(v.get_field("-0"), Some(&ComponentFieldValue::I32(i32::MIN)));
8984 assert_eq!(
8985 v.get_field("9223372036854775807"),
8986 Some(&ComponentFieldValue::I64(i64::MAX))
8987 );
8988 assert!(matches!(v.get_field("NaN"), Some(ComponentFieldValue::F32(f)) if f.is_nan()));
8990 }
8991
8992 fn model_with_text() -> ComponentDataModel {
8997 dm(
8998 "M",
8999 vec![
9000 data_field(
9001 "text",
9002 ComponentFieldType::String,
9003 Some(ComponentDefaultValue::String(AzString::from("hi"))),
9004 "",
9005 ),
9006 data_field("count", ComponentFieldType::U32, Some(ComponentDefaultValue::U32(3)), ""),
9007 data_field("required_one", ComponentFieldType::String, None, ""),
9008 ],
9009 )
9010 }
9011
9012 #[test]
9013 fn data_model_get_field_valid_minimal_and_missing() {
9014 let m = model_with_text();
9015 assert!(m.get_field("text").is_some());
9016 assert!(m.get_field("count").is_some());
9017 assert!(m.get_field("missing").is_none());
9018 assert!(m.get_field("").is_none());
9019 assert!(m.get_field(" ").is_none());
9020 assert!(m.get_field(" text ").is_none(), "exact match, no trimming");
9021 assert!(m.get_field("\u{1F600}").is_none());
9022 assert!(m.get_field(&"z".repeat(LONG)).is_none());
9023 }
9024
9025 #[test]
9026 fn data_model_get_field_on_empty_model() {
9027 let m = dm("Empty", Vec::new());
9028 assert!(m.get_field("anything").is_none());
9029 assert!(m.get_default_string("anything").is_none());
9030 }
9031
9032 #[test]
9033 fn data_model_get_default_string_only_for_string_defaults() {
9034 let m = model_with_text();
9035 assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
9036 assert_eq!(m.get_default_string("count"), None, "U32 default is not a String");
9037 assert_eq!(m.get_default_string("required_one"), None, "no default at all");
9038 assert_eq!(m.get_default_string("missing"), None);
9039 }
9040
9041 #[test]
9042 fn data_model_required_flag_follows_default_presence() {
9043 let m = model_with_text();
9044 assert!(!m.get_field("text").unwrap().required);
9045 assert!(
9046 m.get_field("required_one").unwrap().required,
9047 "a field with no default must be marked required"
9048 );
9049 }
9050
9051 #[test]
9052 fn data_model_with_default_overrides_and_preserves_len() {
9053 let m = model_with_text();
9054 let before = m.fields.as_ref().len();
9055 let m = m.with_default("text", ComponentDefaultValue::String(AzString::from("bye")));
9056 assert_eq!(m.fields.as_ref().len(), before, "len is preserved");
9057 assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("bye"));
9058 }
9059
9060 #[test]
9061 fn data_model_with_default_on_missing_field_is_a_no_op() {
9062 let m = model_with_text();
9063 let m = m.with_default("nope", ComponentDefaultValue::Bool(true));
9064 assert_eq!(m.fields.as_ref().len(), 3);
9065 assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
9066 assert!(m.get_field("nope").is_none(), "no field is inserted");
9067 }
9068
9069 #[test]
9070 fn data_model_with_default_extreme_names_and_values_no_panic() {
9071 let m = model_with_text()
9072 .with_default("", ComponentDefaultValue::None)
9073 .with_default(&"z".repeat(10_000), ComponentDefaultValue::F64(f64::NAN))
9074 .with_default("count", ComponentDefaultValue::Usize(usize::MAX))
9075 .with_default("text", ComponentDefaultValue::I64(i64::MIN));
9076 assert_eq!(m.fields.as_ref().len(), 3);
9077 assert!(matches!(
9078 m.get_field("count").unwrap().default_value,
9079 OptionComponentDefaultValue::Some(ComponentDefaultValue::Usize(usize::MAX))
9080 ));
9081 assert_eq!(
9082 m.get_default_string("text"),
9083 None,
9084 "text is now an I64 default, no longer a String"
9085 );
9086 }
9087
9088 #[test]
9089 fn data_model_with_default_fills_only_the_first_match() {
9090 let m = dm(
9092 "Dup",
9093 vec![
9094 data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("1"))), ""),
9095 data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("2"))), ""),
9096 ],
9097 )
9098 .with_default("x", ComponentDefaultValue::String(AzString::from("3")));
9099 let vals: Vec<&str> = m
9100 .fields
9101 .as_ref()
9102 .iter()
9103 .filter_map(|f| match &f.default_value {
9104 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s.as_str()),
9105 _ => None,
9106 })
9107 .collect();
9108 assert_eq!(vals, vec!["3", "2"], "only the first duplicate is overridden");
9109 }
9110
9111 #[test]
9116 fn component_source_create_is_user_defined() {
9117 assert_eq!(ComponentSource::create(), ComponentSource::UserDefined);
9118 assert_eq!(ComponentSource::default(), ComponentSource::UserDefined);
9119 }
9120
9121 #[test]
9122 fn component_map_create_is_empty_and_all_lookups_return_none() {
9123 let m = ComponentMap::create();
9124 assert_eq!(m.libraries.as_ref().len(), 0);
9125 assert!(m.get("builtin", "div").is_none());
9126 assert!(m.get_unqualified("div").is_none());
9127 assert!(m.get_by_qualified_name("builtin:div").is_none());
9128 assert!(m.all_components().is_empty());
9129 assert!(m.get_exportable_libraries().is_empty());
9130 }
9131
9132 #[test]
9133 fn component_map_with_builtin_invariants() {
9134 let m = ComponentMap::with_builtin();
9135 assert_eq!(m.libraries.as_ref().len(), 1);
9136 let lib = &m.libraries.as_ref()[0];
9137 assert_eq!(lib.name.as_str(), "builtin");
9138 assert!(!lib.exportable, "builtins must never be exportable");
9139 assert!(!lib.modifiable, "builtins must never be modifiable");
9140 assert_eq!(
9141 m.all_components().len(),
9142 lib.components.as_ref().len(),
9143 "all_components must see every registered component"
9144 );
9145 assert!(
9146 m.get_exportable_libraries().is_empty(),
9147 "the builtin library is not exportable"
9148 );
9149 }
9150
9151 #[test]
9152 fn component_map_get_valid_minimal() {
9153 let m = ComponentMap::with_builtin();
9154 assert!(m.get("builtin", "div").is_some());
9155 assert!(m.get("builtin", "if").is_some());
9156 assert!(m.get("builtin", "for").is_some());
9157 assert!(m.get("builtin", "map").is_some());
9158 assert_eq!(
9159 m.get("builtin", "div").unwrap().id.qualified_name(),
9160 "builtin:div"
9161 );
9162 }
9163
9164 #[test]
9165 fn component_map_get_empty_whitespace_garbage_unicode() {
9166 let m = ComponentMap::with_builtin();
9167 assert!(m.get("", "").is_none());
9168 assert!(m.get("builtin", "").is_none());
9169 assert!(m.get("", "div").is_none());
9170 assert!(m.get("builtin", " ").is_none());
9171 assert!(m.get("builtin", " div ").is_none(), "no trimming");
9172 assert!(m.get("builtin", "DIV").is_none(), "lookup is case-sensitive");
9173 assert!(m.get("builtin", "\u{1F600}").is_none());
9174 assert!(m.get("builtin", "\u{0}\u{7f}").is_none());
9175 assert!(m.get("builtin", "div;garbage").is_none());
9176 }
9177
9178 #[test]
9179 fn component_map_get_extremely_long_name_terminates() {
9180 let m = ComponentMap::with_builtin();
9181 assert!(m.get(&"a".repeat(LONG), &"b".repeat(LONG)).is_none());
9182 assert!(m.get_unqualified(&"b".repeat(LONG)).is_none());
9183 assert!(m.get_by_qualified_name(&"c".repeat(LONG)).is_none());
9184 }
9185
9186 #[test]
9187 fn component_map_get_unqualified_only_searches_builtin() {
9188 let lib = ComponentLibrary {
9189 name: AzString::from("mylib"),
9190 version: AzString::from("1.0.0"),
9191 description: AzString::from(""),
9192 components: vec![user_def("", Vec::new())].into(),
9193 exportable: true,
9194 modifiable: true,
9195 data_models: Vec::new().into(),
9196 enum_models: Vec::new().into(),
9197 };
9198 let libs: ComponentLibraryVec = vec![lib].into();
9199 let m = ComponentMap::from_libraries(&libs);
9200
9201 assert!(m.get("mylib", "widget").is_some());
9202 assert!(
9203 m.get_unqualified("widget").is_none(),
9204 "unqualified lookup must NOT reach non-builtin libraries"
9205 );
9206 assert!(m.get_by_qualified_name("mylib:widget").is_some());
9207 assert_eq!(m.get_exportable_libraries().len(), 1);
9208 assert_eq!(m.all_components().len(), 1);
9209 }
9210
9211 #[test]
9212 fn component_map_get_by_qualified_name_boundary_forms() {
9213 let m = ComponentMap::with_builtin();
9214 assert!(m.get_by_qualified_name("div").is_some());
9216 assert!(m.get_by_qualified_name("builtin:div").is_some());
9218 assert!(m.get_by_qualified_name("builtin:div:extra").is_none());
9220 assert!(m.get_by_qualified_name(":").is_none());
9221 assert!(m.get_by_qualified_name(":div").is_none());
9222 assert!(m.get_by_qualified_name("builtin:").is_none());
9223 assert!(m.get_by_qualified_name("").is_none());
9224 assert!(m.get_by_qualified_name(" ").is_none());
9225 }
9226
9227 #[test]
9228 fn component_map_from_libraries_clones_without_losing_entries() {
9229 let src = ComponentMap::with_builtin();
9230 let copy = ComponentMap::from_libraries(&src.libraries);
9231 assert_eq!(copy.all_components().len(), src.all_components().len());
9232 assert!(copy.get_unqualified("div").is_some());
9233 }
9234
9235 #[test]
9236 fn register_builtin_components_is_stable_across_calls() {
9237 let a = register_builtin_components();
9238 let b = register_builtin_components();
9239 assert_eq!(a.components.as_ref().len(), b.components.as_ref().len());
9240 assert!(a.components.as_ref().len() > 50);
9241 assert_eq!(a.name.as_str(), "builtin");
9242 assert_eq!(a.version.as_str(), "1.0.0");
9243 assert!(a
9245 .components
9246 .as_ref()
9247 .iter()
9248 .all(|c| c.id.collection.as_str() == "builtin"));
9249 assert!(a
9250 .components
9251 .as_ref()
9252 .iter()
9253 .all(|c| c.source == ComponentSource::Builtin));
9254 }
9255
9256 #[test]
9261 fn xml_node_child_as_text_and_as_element_are_mutually_exclusive() {
9262 let t = txt("hello");
9263 assert_eq!(t.as_text(), Some("hello"));
9264 assert!(t.as_element().is_none());
9265
9266 let e = elem(node("div", &[], vec![]));
9267 assert!(e.as_text().is_none());
9268 assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("div"));
9269 }
9270
9271 #[test]
9272 fn xml_node_child_as_text_edge_values() {
9273 assert_eq!(txt("").as_text(), Some(""), "an empty text node is still text");
9274 assert_eq!(txt(" ").as_text(), Some(" "), "no trimming in the getter");
9275 assert_eq!(txt("\u{1F600}\u{0301}").as_text(), Some("\u{1F600}\u{0301}"));
9276 assert_eq!(txt("\u{0}").as_text(), Some("\u{0}"));
9277 }
9278
9279 #[test]
9280 fn xml_node_child_as_element_mut_allows_mutation_and_rejects_text() {
9281 let mut e = elem(node("div", &[], vec![]));
9282 e.as_element_mut().expect("is an element").node_type = "span".into();
9283 assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("span"));
9284
9285 let mut t = txt("x");
9286 assert!(t.as_element_mut().is_none(), "text nodes have no element");
9287 }
9288
9289 #[test]
9290 fn xml_node_create_and_with_children_invariants() {
9291 let n = XmlNode::create("div");
9292 assert_eq!(n.node_type.as_str(), "div");
9293 assert_eq!(n.children.as_ref().len(), 0);
9294 assert_eq!(n.attributes.as_ref().len(), 0);
9295
9296 let n = n.with_children(vec![txt("a"), elem(XmlNode::create("b"))]);
9297 assert_eq!(n.children.as_ref().len(), 2);
9298 assert_eq!(n.node_type.as_str(), "div", "tag survives with_children");
9299
9300 let n = n.with_children(Vec::new());
9302 assert_eq!(n.children.as_ref().len(), 0);
9303 }
9304
9305 #[test]
9306 fn xml_node_create_extreme_tag_names_no_panic() {
9307 assert_eq!(XmlNode::create("").node_type.as_str(), "");
9308 assert_eq!(XmlNode::create("\u{1F600}").node_type.as_str(), "\u{1F600}");
9309 assert_eq!(
9310 XmlNode::create(&*"a".repeat(10_000)).node_type.as_str().len(),
9311 10_000
9312 );
9313 }
9314
9315 #[test]
9316 fn xml_node_get_text_content_concatenates_only_direct_text() {
9317 let n = node(
9318 "p",
9319 &[],
9320 vec![
9321 txt("a "),
9322 elem(node("span", &[], vec![txt("IGNORED")])),
9323 txt("b"),
9324 ],
9325 );
9326 assert_eq!(
9327 n.get_text_content(),
9328 "a b",
9329 "only DIRECT text children, nested element text is not included"
9330 );
9331 assert_eq!(XmlNode::default().get_text_content(), "");
9332 assert_eq!(node("p", &[], vec![txt(""), txt("")]).get_text_content(), "");
9333 }
9334
9335 #[test]
9336 fn xml_node_has_only_text_children_true_false_and_empty() {
9337 assert!(
9338 XmlNode::default().has_only_text_children(),
9339 "vacuously true for a childless node — callers must pair this with a text check"
9340 );
9341 assert!(node("p", &[], vec![txt("a"), txt("b")]).has_only_text_children());
9342 assert!(!node("p", &[], vec![txt("a"), elem(XmlNode::create("b"))]).has_only_text_children());
9343 assert!(!node("p", &[], vec![elem(XmlNode::create("b"))]).has_only_text_children());
9344 }
9345
9346 #[test]
9351 fn get_html_node_empty_input_is_no_html_node() {
9352 assert_eq!(get_html_node(&[]), Err(DomXmlParseError::NoHtmlNode));
9353 assert_eq!(get_html_node(&[txt("just text")]), Err(DomXmlParseError::NoHtmlNode));
9354 assert_eq!(
9355 get_html_node(&[elem(XmlNode::create("div"))]),
9356 Err(DomXmlParseError::NoHtmlNode)
9357 );
9358 }
9359
9360 #[test]
9361 fn get_html_node_valid_minimal_and_case_insensitive() {
9362 let roots = vec![elem(XmlNode::create("HTML"))];
9363 assert!(get_html_node(&roots).is_ok(), "tag casing is normalized");
9364 }
9365
9366 #[test]
9367 fn get_html_node_rejects_multiple_roots() {
9368 let roots = vec![elem(XmlNode::create("html")), elem(XmlNode::create("html"))];
9369 assert_eq!(
9370 get_html_node(&roots),
9371 Err(DomXmlParseError::MultipleHtmlRootNodes)
9372 );
9373 }
9374
9375 #[test]
9376 fn get_body_node_empty_and_missing() {
9377 assert_eq!(get_body_node(&[]), Err(DomXmlParseError::NoBodyInHtml));
9378 assert_eq!(
9379 get_body_node(&[elem(XmlNode::create("head"))]),
9380 Err(DomXmlParseError::NoBodyInHtml)
9381 );
9382 }
9383
9384 #[test]
9385 fn get_body_node_direct_and_nested() {
9386 let direct = vec![elem(XmlNode::create("body"))];
9387 assert!(get_body_node(&direct).is_ok());
9388
9389 let nested = vec![elem(node(
9391 "head",
9392 &[],
9393 vec![elem(node("div", &[], vec![elem(XmlNode::create("BODY"))]))],
9394 ))];
9395 assert!(
9396 get_body_node(&nested).is_ok(),
9397 "the recursive fallback finds a nested body"
9398 );
9399 }
9400
9401 fn wrap_divs(depth: usize, inner: XmlNode) -> XmlNode {
9403 let mut n = inner;
9404 for _ in 0..depth {
9405 n = node("div", &[], vec![elem(n)]);
9406 }
9407 n
9408 }
9409
9410 #[test]
9411 fn get_body_node_deep_nesting_is_depth_capped_not_stack_overflowing() {
9412 let ok = vec![elem(wrap_divs(
9414 MAX_XML_NESTING_DEPTH - 2,
9415 XmlNode::create("body"),
9416 ))];
9417 assert!(get_body_node(&ok).is_ok(), "body within the depth cap is found");
9418
9419 let too_deep = vec![elem(wrap_divs(2_000, XmlNode::create("body")))];
9421 assert_eq!(
9422 get_body_node(&too_deep),
9423 Err(DomXmlParseError::NoBodyInHtml),
9424 "past MAX_XML_NESTING_DEPTH the search gives up instead of crashing"
9425 );
9426 }
9427
9428 #[test]
9429 fn find_node_by_type_empty_garbage_unicode() {
9430 assert!(find_node_by_type(&[], "div").is_none());
9431 assert!(find_node_by_type(&[txt("x")], "div").is_none());
9432
9433 let roots = vec![elem(XmlNode::create("div"))];
9434 assert!(find_node_by_type(&roots, "").is_none());
9435 assert!(find_node_by_type(&roots, " ").is_none());
9436 assert!(find_node_by_type(&roots, "\u{1F600}").is_none());
9437 assert!(find_node_by_type(&roots, &"z".repeat(LONG)).is_none());
9438 assert!(find_node_by_type(&roots, "DIV").is_some());
9442 }
9443
9444 #[test]
9445 fn find_node_by_type_valid_minimal_and_recursive() {
9446 let roots = vec![elem(node(
9447 "html",
9448 &[],
9449 vec![elem(node("head", &[], vec![elem(XmlNode::create("STYLE"))]))],
9450 ))];
9451 assert!(find_node_by_type(&roots, "html").is_some());
9452 assert!(
9453 find_node_by_type(&roots, "style").is_some(),
9454 "search recurses into the whole tree"
9455 );
9456 assert!(find_node_by_type(&roots, "body").is_none());
9457 }
9458
9459 #[test]
9460 fn find_node_by_type_prefers_the_shallowest_match() {
9461 let roots = vec![
9462 elem(node("div", &[], vec![elem(XmlNode::create("span"))])),
9463 elem(node("span", &[("id", "shallow")], vec![])),
9464 ];
9465 let found = find_node_by_type(&roots, "span").expect("found");
9466 assert_eq!(
9467 found.attributes.get_key("id").map(AzString::as_str),
9468 Some("shallow"),
9469 "direct children are scanned before recursing"
9470 );
9471 }
9472
9473 #[test]
9474 fn find_attribute_valid_minimal_and_missing() {
9475 let n = node("a", &[("href", "x"), ("id", "y")], vec![]);
9476 assert_eq!(find_attribute(&n, "href").map(AzString::as_str), Some("x"));
9477 assert_eq!(find_attribute(&n, "id").map(AzString::as_str), Some("y"));
9478 assert!(find_attribute(&n, "missing").is_none());
9479 assert!(find_attribute(&n, "").is_none());
9480 assert!(find_attribute(&n, " ").is_none());
9481 assert!(find_attribute(&XmlNode::default(), "href").is_none());
9482 assert!(find_attribute(&n, &"z".repeat(LONG)).is_none());
9483 }
9484
9485 #[test]
9486 fn find_attribute_compares_against_the_normalized_key() {
9487 let n = node("button", &[("aria-label", "Save")], vec![]);
9490 assert_eq!(
9491 find_attribute(&n, "aria_label").map(AzString::as_str),
9492 Some("Save")
9493 );
9494 assert!(
9495 find_attribute(&n, "aria-label").is_none(),
9496 "the hyphenated spelling never matches (keys are normalized first)"
9497 );
9498 }
9499
9500 #[test]
9501 fn find_attribute_unicode_keys_no_panic() {
9502 let n = node("div", &[("\u{130}", "v"), ("\u{1F600}", "w")], vec![]);
9503 assert!(find_attribute(&n, "\u{1F600}").is_some(), "emoji keys pass through");
9504 assert!(find_attribute(&n, "\u{130}").is_none());
9506 }
9507
9508 #[test]
9513 fn normalize_casing_documented_forms() {
9514 assert_eq!(normalize_casing("abcDef"), "abc_def");
9515 assert_eq!(normalize_casing("AbcDef"), "abc_def");
9516 assert_eq!(normalize_casing("abc-def"), "abc_def");
9517 assert_eq!(normalize_casing("abc_def"), "abc_def");
9518 }
9519
9520 #[test]
9521 fn normalize_casing_edge_inputs() {
9522 assert_eq!(normalize_casing(""), "");
9523 assert_eq!(normalize_casing("---"), "", "separators alone produce no words");
9524 assert_eq!(normalize_casing("___"), "");
9525 assert_eq!(normalize_casing("A"), "a");
9526 assert_eq!(
9527 normalize_casing("ABC"),
9528 "a_b_c",
9529 "every uppercase char starts a new word"
9530 );
9531 assert_eq!(normalize_casing("h1"), "h1");
9532 assert_eq!(normalize_casing(" "), " ", "whitespace is not a separator");
9533 }
9534
9535 #[test]
9536 fn normalize_casing_unicode_and_long_no_panic() {
9537 assert_eq!(normalize_casing("\u{130}"), "i\u{307}");
9539 assert_eq!(normalize_casing("\u{1F600}"), "\u{1F600}");
9540 assert_eq!(normalize_casing(&"a".repeat(50_000)).len(), 50_000);
9541 assert_eq!(normalize_casing(&"A".repeat(50_000)).len(), 50_000 * 2 - 1);
9543 }
9544
9545 #[test]
9550 fn get_item_empty_hierarchy_returns_the_root() {
9551 let mut root = node("div", &[("id", "root")], vec![]);
9552 let got = get_item(&[], &mut root).expect("empty hierarchy => root");
9553 assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("root"));
9554 }
9555
9556 #[test]
9557 fn get_item_walks_nested_elements() {
9558 let mut root = node(
9559 "a",
9560 &[],
9561 vec![elem(node("b", &[], vec![elem(node("c", &[("id", "deep")], vec![]))]))],
9562 );
9563 let got = get_item(&[0, 0], &mut root).expect("a > b > c");
9564 assert_eq!(got.node_type.as_str(), "c");
9565 assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("deep"));
9566 }
9567
9568 #[test]
9569 fn get_item_out_of_bounds_and_text_nodes_return_none() {
9570 let mut root = node("a", &[], vec![txt("hello"), elem(XmlNode::create("b"))]);
9571 assert!(get_item(&[5], &mut root).is_none(), "out of bounds => None");
9572 assert!(
9573 get_item(&[usize::MAX], &mut root).is_none(),
9574 "usize::MAX index must not panic"
9575 );
9576 assert!(
9577 get_item(&[0], &mut root).is_none(),
9578 "index 0 is a TEXT node — not traversable"
9579 );
9580 assert!(get_item(&[1], &mut root).is_some());
9581 assert!(
9582 get_item(&[1, 0], &mut root).is_none(),
9583 "descending past a leaf => None"
9584 );
9585 }
9586
9587 #[test]
9588 fn get_item_deep_hierarchy_terminates() {
9589 let mut root = wrap_divs(400, node("div", &[("id", "bottom")], vec![]));
9591 let path = vec![0usize; 400];
9592 let got = get_item(&path, &mut root).expect("reaches the bottom");
9593 assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("bottom"));
9594 }
9595
9596 #[test]
9601 fn decode_numeric_entity_valid_minimal() {
9602 assert_eq!(decode_numeric_entity("#65"), Some('A'));
9603 assert_eq!(decode_numeric_entity("#x41"), Some('A'));
9604 assert_eq!(decode_numeric_entity("#X41"), Some('A'), "uppercase X accepted");
9605 assert_eq!(decode_numeric_entity("#x1F600"), Some('\u{1F600}'));
9606 }
9607
9608 #[test]
9609 fn decode_numeric_entity_empty_whitespace_garbage() {
9610 assert_eq!(decode_numeric_entity(""), None);
9611 assert_eq!(decode_numeric_entity(" "), None);
9612 assert_eq!(decode_numeric_entity("\t\n"), None);
9613 assert_eq!(decode_numeric_entity("amp"), None, "named entity is not numeric");
9614 assert_eq!(decode_numeric_entity("#"), None);
9615 assert_eq!(decode_numeric_entity("#x"), None);
9616 assert_eq!(decode_numeric_entity("#zz"), None);
9617 assert_eq!(decode_numeric_entity("# 65"), None);
9618 assert_eq!(decode_numeric_entity("#65junk"), None);
9619 assert_eq!(decode_numeric_entity("\u{1F600}"), None);
9620 }
9621
9622 #[test]
9623 fn decode_numeric_entity_boundary_code_points() {
9624 assert_eq!(decode_numeric_entity("#0"), Some('\u{0}'), "NUL is a valid char");
9625 assert_eq!(decode_numeric_entity("#x10FFFF"), Some('\u{10FFFF}'), "max scalar");
9626 assert_eq!(
9627 decode_numeric_entity("#x110000"),
9628 None,
9629 "one past the max scalar value"
9630 );
9631 assert_eq!(
9632 decode_numeric_entity("#xD800"),
9633 None,
9634 "a lone surrogate is not a char"
9635 );
9636 assert_eq!(
9637 decode_numeric_entity("#4294967295"),
9638 None,
9639 "u32::MAX is not a scalar value"
9640 );
9641 assert_eq!(
9642 decode_numeric_entity("#4294967296"),
9643 None,
9644 "one past u32::MAX must not wrap — it must fail to parse"
9645 );
9646 assert_eq!(decode_numeric_entity("#-1"), None, "negative is rejected by u32");
9647 assert_eq!(
9648 decode_numeric_entity("#xFFFFFFFFFFFF"),
9649 None,
9650 "hex overflow is rejected, not truncated"
9651 );
9652 }
9653
9654 #[test]
9655 fn decode_numeric_entity_extremely_long_terminates() {
9656 let s = format!("#{}", "9".repeat(LONG));
9657 assert_eq!(s.len(), LONG + 1);
9658 assert_eq!(decode_numeric_entity(&s), None, "overflows u32 => None");
9659 }
9660
9661 #[test]
9666 fn decode_entities_leaves_unrecognized_sequences_verbatim() {
9667 assert_eq!(decode_entities(""), "");
9668 assert_eq!(decode_entities("&"), "&", "a bare '&' at EOF must not panic");
9669 assert_eq!(decode_entities("&;"), "&;", "empty entity body is not decoded");
9670 assert_eq!(decode_entities("&bogus;"), "&bogus;");
9671 assert_eq!(decode_entities(" "), " ", " is deliberately kept");
9672 assert_eq!(
9674 decode_entities("&averyveryverylongbody;"),
9675 "&averyveryverylongbody;"
9676 );
9677 }
9678
9679 #[test]
9680 fn decode_entities_single_pass_prevents_double_decoding() {
9681 assert_eq!(decode_entities("&lt;"), "<", "& must not re-open an entity");
9682 assert_eq!(decode_entities("<>&"'"), "<>&\"'");
9683 }
9684
9685 #[test]
9686 fn decode_entities_unicode_and_long_input_terminate() {
9687 assert_eq!(decode_entities("\u{1F600}\u{0301}\u{130}"), "\u{1F600}\u{0301}\u{130}");
9688 let amps = "&".repeat(20_000);
9691 assert_eq!(decode_entities(&s).len(), 20_000);
9692 }
9693
9694 #[test]
9695 fn prepare_string_empty_and_whitespace() {
9696 assert_eq!(prepare_string(""), "");
9697 assert_eq!(prepare_string(" "), "");
9698 assert_eq!(prepare_string("\t\n\r \n"), "");
9699 }
9700
9701 #[test]
9702 fn prepare_string_nbsp_becomes_a_space_that_survives_trim() {
9703 assert_eq!(prepare_string(" "), " ");
9704 assert_eq!(prepare_string("a b"), "a b");
9705 }
9706
9707 #[test]
9708 fn prepare_string_collapses_a_blank_line_into_a_single_return() {
9709 assert_eq!(prepare_string("a\n\nb"), "a\nb");
9710 assert_eq!(prepare_string("a\n\n\n\nb"), "a\nb", "runs of blanks collapse");
9711 }
9712
9713 #[test]
9714 fn prepare_string_unicode_and_long_input_no_panic() {
9715 assert_eq!(prepare_string("\u{1F600}"), "\u{1F600}");
9716 assert_eq!(prepare_string(" \u{130}\u{0301} "), "\u{130}\u{0301}");
9717 assert_eq!(prepare_string(&"x".repeat(LONG)).len(), LONG);
9718 }
9719
9720 #[test]
9724 fn prepare_string_joins_wrapped_lines_with_a_space() {
9725 assert_eq!(
9726 prepare_string("Hello\nworld"),
9727 "Hello world",
9728 "a single newline between words must collapse to a space, not vanish"
9729 );
9730 assert_eq!(prepare_string("a\nb\nc"), "a b c");
9731 }
9732
9733 #[test]
9738 fn parse_bool_valid_minimal_and_everything_else_is_none() {
9739 assert_eq!(parse_bool("true"), Some(true));
9740 assert_eq!(parse_bool("false"), Some(false));
9741
9742 for s in [
9743 "", " ", "\t\n", " true", "true ", "TRUE", "True", "FALSE", "1", "0", "-0", "yes",
9744 "no", "NaN", "inf", "9223372036854775807", "\u{1F600}", "true;garbage",
9745 ] {
9746 assert_eq!(parse_bool(s), None, "{s:?} must not parse as a bool");
9747 }
9748 assert_eq!(parse_bool(&"a".repeat(LONG)), None);
9749 }
9750
9751 fn args(kv: &[(&str, &str)]) -> ComponentArgumentVec {
9756 kv.iter()
9757 .map(|(n, t)| ComponentArgument {
9758 name: AzString::from(*n),
9759 arg_type: AzString::from(*t),
9760 })
9761 .collect::<Vec<_>>()
9762 .into()
9763 }
9764
9765 #[test]
9766 fn split_dynamic_string_empty_and_plain() {
9767 assert!(split_dynamic_string("").is_empty());
9768 assert_eq!(
9769 split_dynamic_string("abc"),
9770 vec![DynamicItem::Str("abc".to_string())]
9771 );
9772 }
9773
9774 #[test]
9775 fn split_dynamic_string_format_spec_is_split_on_the_first_colon() {
9776 assert_eq!(
9777 split_dynamic_string("{a:?}"),
9778 vec![DynamicItem::Var {
9779 name: "a".to_string(),
9780 format_spec: Some("?".to_string()),
9781 }]
9782 );
9783 assert_eq!(
9784 split_dynamic_string("{a:x:y}"),
9785 vec![DynamicItem::Var {
9786 name: "a".to_string(),
9787 format_spec: Some("x:y".to_string()),
9788 }],
9789 "only the FIRST colon separates name from spec"
9790 );
9791 }
9792
9793 #[test]
9794 fn split_dynamic_string_unterminated_var_stays_literal() {
9795 assert_eq!(
9797 split_dynamic_string("{unterminated"),
9798 vec![DynamicItem::Str("{unterminated".to_string())]
9799 );
9800 assert_eq!(
9802 split_dynamic_string("{a b}"),
9803 vec![DynamicItem::Str("{a b}".to_string())]
9804 );
9805 }
9806
9807 #[test]
9808 fn split_dynamic_string_unicode_and_long_input_terminate() {
9809 assert_eq!(
9810 split_dynamic_string("\u{1F600}{v}\u{130}"),
9811 vec![
9812 DynamicItem::Str("\u{1F600}".to_string()),
9813 DynamicItem::Var {
9814 name: "v".to_string(),
9815 format_spec: None
9816 },
9817 DynamicItem::Str("\u{130}".to_string()),
9818 ]
9819 );
9820 let bomb = "{a".repeat(50_000);
9823 let out = split_dynamic_string(&bomb);
9824 assert!(out.len() <= 2, "a never-closed variable collapses, got {}", out.len());
9825
9826 let braces = "{".repeat(100_000);
9827 assert!(split_dynamic_string(&braces).len() <= 1);
9828 }
9829
9830 #[test]
9831 fn format_args_dynamic_documented_example() {
9832 let vars = args(&[("a", "value1"), ("b", "value2")]);
9833 assert_eq!(
9834 format_args_dynamic("hello {a}, {b}{{ {c} }}", &vars),
9835 "hello value1, value2{ {c} }"
9836 );
9837 }
9838
9839 #[test]
9840 fn format_args_dynamic_unknown_var_is_preserved_verbatim() {
9841 let empty = no_args();
9842 assert_eq!(format_args_dynamic("{c}", &empty), "{c}");
9843 assert_eq!(
9844 format_args_dynamic("{c:?}", &empty),
9845 "{c:?}",
9846 "the format spec is re-attached when the var is unresolved"
9847 );
9848 assert_eq!(format_args_dynamic("{{}}", &empty), "{{}}");
9850 }
9851
9852 #[test]
9853 fn format_args_dynamic_variable_names_are_normalized() {
9854 let vars = args(&[("my_var", "V")]);
9855 assert_eq!(format_args_dynamic("{myVar}", &vars), "V", "camelCase is normalized");
9856 assert_eq!(format_args_dynamic("{ my-var }", &vars), "{ my-var }",
9857 "whitespace inside the braces aborts the variable scan entirely");
9858 assert_eq!(format_args_dynamic("{my-var}", &vars), "V");
9859 }
9860
9861 #[test]
9862 fn format_args_dynamic_edge_values_no_panic() {
9863 let empty = no_args();
9864 assert_eq!(format_args_dynamic("", &empty), "");
9865 assert_eq!(format_args_dynamic(" ", &empty), " ");
9866 assert_eq!(format_args_dynamic("\u{1F600}", &empty), "\u{1F600}");
9867 assert_eq!(format_args_dynamic(&"x".repeat(50_000), &empty).len(), 50_000);
9868 }
9869
9870 #[test]
9871 fn combine_and_replace_dynamic_items_on_empty_input() {
9872 assert_eq!(combine_and_replace_dynamic_items(&[], &no_args()), "");
9873 }
9874
9875 #[test]
9880 fn format_args_for_rust_code_empty_and_single_items() {
9881 assert_eq!(format_args_for_rust_code(""), "AzString::from_const_str(\"\")");
9882 assert_eq!(format_args_for_rust_code("hi"), "AzString::from_const_str(\"hi\")");
9883 assert_eq!(format_args_for_rust_code("{a}"), "a", "a lone var becomes a bare ident");
9884 assert_eq!(
9885 format_args_for_rust_code("{a:?}"),
9886 "format!(\"{:?}\", a).into()"
9887 );
9888 }
9889
9890 #[test]
9891 fn format_args_for_rust_code_multi_item_builds_a_format_call() {
9892 assert_eq!(
9893 format_args_for_rust_code("x={a} y={b}"),
9894 "format!(\"x={a} y={b}\", a, b).into()"
9895 );
9896 }
9897
9898 #[test]
9899 fn format_args_for_rust_code_escapes_quotes_in_literals() {
9900 let out = format_args_for_rust_code("say \"hi\" {a}");
9901 assert!(
9902 out.contains("say \\\"hi\\\""),
9903 "double quotes must be escaped for the emitted literal, got {out}"
9904 );
9905 }
9906
9907 #[test]
9908 fn compile_and_format_dynamic_items_edge_values() {
9909 assert_eq!(
9910 compile_and_format_dynamic_items(&[]),
9911 "AzString::from_const_str(\"\")"
9912 );
9913 assert_eq!(
9914 compile_and_format_dynamic_items(&[DynamicItem::Str(String::new())]),
9915 "AzString::from_const_str(\"\")"
9916 );
9917 assert_eq!(
9918 compile_and_format_dynamic_items(&[DynamicItem::Var {
9919 name: " spaced ".to_string(),
9920 format_spec: None,
9921 }]),
9922 "spaced",
9923 "the var name is trimmed + normalized"
9924 );
9925 }
9926
9927 #[test]
9932 fn cap_first_edge_inputs() {
9933 assert_eq!(cap_first(""), "", "empty input must not panic");
9934 assert_eq!(cap_first("h1"), "H1");
9935 assert_eq!(cap_first("button"), "Button");
9936 assert_eq!(cap_first("A"), "A", "already-uppercase is idempotent");
9937 assert_eq!(cap_first("\u{1F600}x"), "\u{1F600}x", "emoji has no uppercase form");
9938 assert_eq!(cap_first("\u{df}x"), "SSx");
9940 assert_eq!(cap_first(&"a".repeat(10_000)).len(), 10_000);
9941 }
9942
9943 #[test]
9944 fn camel_to_snake_documented_forms() {
9945 assert_eq!(camel_to_snake("ButtonNoA11y"), "button_no_a11y");
9946 assert_eq!(camel_to_snake("PWithText"), "p_with_text");
9947 assert_eq!(camel_to_snake("ANoA11y"), "a_no_a11y");
9948 assert_eq!(camel_to_snake("H1WithText"), "h1_with_text");
9949 assert_eq!(camel_to_snake("Div"), "div");
9950 }
9951
9952 #[test]
9953 fn camel_to_snake_edge_inputs() {
9954 assert_eq!(camel_to_snake(""), "");
9955 assert_eq!(camel_to_snake("A"), "a");
9956 assert_eq!(camel_to_snake("AB"), "ab", "an all-caps run is not split");
9957 assert_eq!(camel_to_snake("ABc"), "a_bc", "a caps run splits before the last cap");
9958 assert_eq!(camel_to_snake("\u{1F600}"), "\u{1F600}");
9959 assert_eq!(camel_to_snake(&"a".repeat(10_000)).len(), 10_000);
9960 }
9961
9962 #[test]
9963 fn esc_lit_escapes_backslash_before_quote() {
9964 assert_eq!(esc_lit(""), "");
9965 assert_eq!(esc_lit("plain"), "plain");
9966 assert_eq!(esc_lit("a\"b"), "a\\\"b");
9967 assert_eq!(esc_lit("a\\b"), "a\\\\b");
9968 assert_eq!(esc_lit("\\\""), "\\\\\\\"");
9970 assert_eq!(esc_lit("\u{1F600}"), "\u{1F600}");
9971 }
9972
9973 #[test]
9974 fn c_creator_suffix_edge_inputs() {
9975 assert_eq!(c_creator_suffix(""), "Div", "empty debug name falls back to Div");
9976 assert_eq!(c_creator_suffix("Div"), "Div");
9977 assert_eq!(c_creator_suffix("H1"), "H1");
9978 assert_eq!(c_creator_suffix("BlockQuote"), "Blockquote");
9979 assert_eq!(c_creator_suffix("FigCaption"), "Figcaption");
9980 assert_eq!(c_creator_suffix("\u{1F600}"), "\u{1F600}");
9981 }
9982
9983 #[test]
9988 fn safe_container_tag_falls_back_to_div_for_arg_taking_widgets() {
9989 assert_eq!(safe_container_tag(""), "Div");
9990 assert_eq!(safe_container_tag("Div"), "Div");
9991 assert_eq!(safe_container_tag("Span"), "Span");
9992 assert_eq!(safe_container_tag("H1"), "H1");
9993 assert_eq!(safe_container_tag("Button"), "Div");
9995 assert_eq!(safe_container_tag("Input"), "Div");
9996 assert_eq!(safe_container_tag("A"), "Div");
9997 assert_eq!(safe_container_tag("\u{1F600}"), "Div");
9998 assert_eq!(safe_container_tag(&"z".repeat(10_000)), "Div");
9999 }
10000
10001 #[test]
10008 fn safe_container_tag_matches_the_real_nodetype_debug_names() {
10009 for tag in [
10010 "blockquote",
10011 "figcaption",
10012 "thead",
10013 "tbody",
10014 "tfoot",
10015 "colgroup",
10016 ] {
10017 let dbg = format!("{:?}", tag_to_node_type(tag));
10018 assert_ne!(
10019 safe_container_tag(&dbg),
10020 "Div",
10021 "<{tag}> (NodeType debug name {dbg:?}) is a pure container and must keep \
10022 its own creator instead of degrading to a div"
10023 );
10024 }
10025 }
10026
10027 #[test]
10032 fn fmt_f32_lit_zero_and_negative() {
10033 assert_eq!(fmt_f32_lit(0.0), "0.0", "an integral value gains a decimal point");
10034 assert_eq!(fmt_f32_lit(-0.0), "-0.0");
10035 assert_eq!(fmt_f32_lit(-1.0), "-1.0");
10036 assert_eq!(fmt_f32_lit(1.5), "1.5");
10037 assert_eq!(fmt_f32_lit(-1.5), "-1.5");
10038 }
10039
10040 #[test]
10041 fn fmt_f32_lit_min_max_stay_parseable_float_literals() {
10042 for f in [f32::MAX, f32::MIN, f32::MIN_POSITIVE, f32::EPSILON] {
10043 let s = fmt_f32_lit(f);
10044 assert_eq!(
10045 s.parse::<f32>(),
10046 Ok(f),
10047 "{f:e} must round-trip through its emitted literal ({s})"
10048 );
10049 }
10050 }
10051
10052 #[test]
10053 fn fmt_f32_lit_nan_inf_produce_a_defined_result_and_do_not_panic() {
10054 assert_eq!(fmt_f32_lit(f32::NAN), "NaN");
10058 assert_eq!(fmt_f32_lit(f32::INFINITY), "inf");
10059 assert_eq!(fmt_f32_lit(f32::NEG_INFINITY), "-inf");
10060 }
10061
10062 #[test]
10068 fn node_direct_text_trims_and_skips_elements() {
10069 assert_eq!(node_direct_text(&XmlNode::default()), "");
10070 assert_eq!(node_direct_text(&node("p", &[], vec![txt(" Go ")])), "Go");
10071 assert_eq!(
10072 node_direct_text(&node(
10073 "p",
10074 &[],
10075 vec![txt("a"), elem(node("b", &[], vec![txt("IGNORED")])), txt("b")]
10076 )),
10077 "a b",
10078 "direct text children are joined with a single space"
10079 );
10080 assert_eq!(
10081 node_direct_text(&node("p", &[], vec![txt(" "), txt("\t\n")])),
10082 "",
10083 "whitespace-only children are dropped"
10084 );
10085 }
10086
10087 #[test]
10088 fn node_aria_label_ignores_empty_and_whitespace() {
10089 assert_eq!(node_aria_label(&XmlNode::default()), None);
10090 assert_eq!(node_aria_label(&node("b", &[("aria-label", "")], vec![])), None);
10091 assert_eq!(node_aria_label(&node("b", &[("aria-label", " ")], vec![])), None);
10092 assert_eq!(
10093 node_aria_label(&node("b", &[("aria-label", " Save ")], vec![])),
10094 Some("Save".to_string())
10095 );
10096 }
10097
10098 #[test]
10099 fn node_attr_or_returns_the_default_when_absent() {
10100 let n = node("a", &[("href", "/x"), ("empty", "")], vec![]);
10101 assert_eq!(node_attr_or(&n, "href", "FALLBACK"), "/x");
10102 assert_eq!(node_attr_or(&n, "missing", "FALLBACK"), "FALLBACK");
10103 assert_eq!(
10104 node_attr_or(&n, "empty", "FALLBACK"),
10105 "",
10106 "a present-but-empty attribute wins over the default"
10107 );
10108 assert_eq!(node_attr_or(&XmlNode::default(), "x", ""), "");
10109 }
10110
10111 #[test]
10112 fn node_attr_f32_zero_negative_and_defaults() {
10113 let n = node(
10114 "meter",
10115 &[("zero", "0"), ("negzero", "-0"), ("neg", "-2.5"), ("pad", " 1.5 ")],
10116 vec![],
10117 );
10118 assert_eq!(node_attr_f32(&n, "zero", 9.0), 0.0);
10119 assert!(node_attr_f32(&n, "negzero", 9.0).is_sign_negative());
10120 assert_eq!(node_attr_f32(&n, "neg", 9.0), -2.5);
10121 assert_eq!(node_attr_f32(&n, "pad", 9.0), 1.5, "the value is trimmed first");
10122 assert_eq!(node_attr_f32(&n, "missing", 9.0), 9.0);
10123 }
10124
10125 #[test]
10126 fn node_attr_f32_unparsable_falls_back_and_min_max_saturate() {
10127 let n = node(
10128 "meter",
10129 &[
10130 ("junk", "abc"),
10131 ("empty", ""),
10132 ("huge", "1e400"),
10133 ("tiny", "-1e400"),
10134 ("big", "340282350000000000000000000000000000000"),
10135 ],
10136 vec![],
10137 );
10138 assert_eq!(node_attr_f32(&n, "junk", 7.0), 7.0);
10139 assert_eq!(node_attr_f32(&n, "empty", 7.0), 7.0);
10140 assert!(
10141 node_attr_f32(&n, "huge", 7.0).is_infinite(),
10142 "an out-of-range literal saturates to inf, it does not panic"
10143 );
10144 assert_eq!(node_attr_f32(&n, "tiny", 7.0), f32::NEG_INFINITY);
10145 assert_eq!(node_attr_f32(&n, "big", 7.0), f32::MAX);
10146 }
10147
10148 #[test]
10149 fn node_attr_f32_accepts_nan_and_inf_spellings() {
10150 let n = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10153 assert!(node_attr_f32(&n, "value", 0.0).is_nan());
10154 assert_eq!(node_attr_f32(&n, "max", 1.0), f32::INFINITY);
10155 }
10156
10157 #[test]
10158 fn node_attr_f32_nan_default_is_returned_verbatim() {
10159 assert!(node_attr_f32(&XmlNode::default(), "x", f32::NAN).is_nan());
10160 assert_eq!(node_attr_f32(&XmlNode::default(), "x", f32::INFINITY), f32::INFINITY);
10161 }
10162
10163 #[test]
10164 fn first_caption_text_edges() {
10165 assert_eq!(first_caption_text(&XmlNode::default()), None);
10166 assert_eq!(
10167 first_caption_text(&node("table", &[], vec![elem(node("caption", &[], vec![]))])),
10168 None,
10169 "an empty caption yields None"
10170 );
10171 assert_eq!(
10172 first_caption_text(&node(
10173 "table",
10174 &[],
10175 vec![elem(node("CAPTION", &[], vec![txt(" Hi ")]))]
10176 )),
10177 Some("Hi".to_string()),
10178 "the tag match is ASCII-case-insensitive and the text is trimmed"
10179 );
10180 }
10181
10182 #[test]
10187 fn analyze_node_ctor_plain_for_unknown_and_empty_tags() {
10188 assert!(matches!(analyze_node_ctor("div", &XmlNode::default()), NodeCtor::Plain));
10189 assert!(matches!(analyze_node_ctor("", &XmlNode::default()), NodeCtor::Plain));
10190 assert!(matches!(
10191 analyze_node_ctor("\u{1F600}", &XmlNode::default()),
10192 NodeCtor::Plain
10193 ));
10194 let plain = analyze_node_ctor("div", &XmlNode::default());
10195 assert_eq!(plain.render_rust(), None);
10196 assert_eq!(plain.render_c(), None);
10197 assert_eq!(plain.render_fluent(&CompileTarget::Cpp), None);
10198 assert!(!plain.consumes_text());
10199 assert!(!plain.skip_caption());
10200 }
10201
10202 #[test]
10203 fn analyze_node_ctor_with_text_tier_requires_actual_text() {
10204 assert!(matches!(analyze_node_ctor("p", &XmlNode::default()), NodeCtor::Plain));
10207 let mixed = node("p", &[], vec![txt("a"), elem(XmlNode::create("span"))]);
10209 assert!(matches!(analyze_node_ctor("p", &mixed), NodeCtor::Plain));
10210
10211 let pure = node("p", &[], vec![txt(" Hello ")]);
10212 let ctor = analyze_node_ctor("p", &pure);
10213 assert!(ctor.consumes_text(), "the text is folded into the constructor");
10214 assert_eq!(
10215 ctor.render_rust().as_deref(),
10216 Some("Dom::create_p_with_text(AzString::from(\"Hello\"))")
10217 );
10218 assert_eq!(
10219 ctor.render_c().as_deref(),
10220 Some("AzDom_createPWithText(AZ_STR(\"Hello\"))")
10221 );
10222 assert_eq!(
10223 ctor.render_fluent(&CompileTarget::Python).as_deref(),
10224 Some("azul.Dom.create_p_with_text(\"Hello\")")
10225 );
10226 }
10227
10228 #[test]
10229 fn analyze_node_ctor_button_with_and_without_aria() {
10230 let plain_btn = node("button", &[], vec![txt("Go")]);
10231 assert_eq!(
10232 analyze_node_ctor("button", &plain_btn).render_rust().as_deref(),
10233 Some("Dom::create_button_no_a11y(AzString::from(\"Go\"))")
10234 );
10235
10236 let aria_btn = node("button", &[("aria-label", "Save")], vec![txt("Go")]);
10237 assert_eq!(
10238 analyze_node_ctor("button", &aria_btn).render_rust().as_deref(),
10239 Some(
10240 "Dom::create_button(AzString::from(\"Go\"), \
10241 SmallAriaInfo::label(AzString::from(\"Save\")))"
10242 )
10243 );
10244 }
10245
10246 #[test]
10247 fn analyze_node_ctor_escapes_quotes_and_backslashes_in_text() {
10248 let btn = node("button", &[], vec![txt("say \"hi\"\\now")]);
10249 let rust = analyze_node_ctor("button", &btn).render_rust().expect("semantic");
10250 assert!(
10251 rust.contains("say \\\"hi\\\"\\\\now"),
10252 "quotes and backslashes must be escaped for the literal, got {rust}"
10253 );
10254 }
10255
10256 #[test]
10257 fn analyze_node_ctor_anchor_uses_option_string_when_it_has_no_text() {
10258 let bare = node("a", &[], vec![]);
10259 assert_eq!(
10260 analyze_node_ctor("a", &bare).render_rust().as_deref(),
10261 Some("Dom::create_a_no_a11y(AzString::from(\"\"), OptionString::None)"),
10262 "a missing href defaults to an empty string, missing text to OptionString::None"
10263 );
10264
10265 let full = node("a", &[("href", "/x")], vec![txt("Home")]);
10266 assert_eq!(
10267 analyze_node_ctor("a", &full).render_rust().as_deref(),
10268 Some(
10269 "Dom::create_a_no_a11y(AzString::from(\"/x\"), \
10270 OptionString::Some(AzString::from(\"Home\")))"
10271 )
10272 );
10273 assert_eq!(
10274 analyze_node_ctor("a", &full).render_c().as_deref(),
10275 Some("AzDom_createANoA11y(AZ_STR(\"/x\"), AzOptionString_some(AZ_STR(\"Home\")))")
10276 );
10277 }
10278
10279 #[test]
10280 fn analyze_node_ctor_table_aria_form_skips_the_literal_caption() {
10281 let t = node(
10282 "table",
10283 &[("aria-label", "Prices")],
10284 vec![elem(node("caption", &[], vec![txt("Q1")]))],
10285 );
10286 let ctor = analyze_node_ctor("table", &t);
10287 assert!(ctor.skip_caption(), "the aria form injects its own caption child");
10288 assert_eq!(
10289 ctor.render_rust().as_deref(),
10290 Some(
10291 "Dom::create_table(AzString::from(\"Q1\"), \
10292 SmallAriaInfo::label(AzString::from(\"Prices\")))"
10293 )
10294 );
10295
10296 let plain = analyze_node_ctor("table", &node("table", &[], vec![]));
10297 assert!(!plain.skip_caption());
10298 assert_eq!(plain.render_rust().as_deref(), Some("Dom::create_table_no_a11y()"));
10299 }
10300
10301 #[test]
10302 fn analyze_node_ctor_scalar_widgets_use_defaults_and_emit_float_literals() {
10303 let p = node("progress", &[], vec![]);
10304 assert_eq!(
10305 analyze_node_ctor("progress", &p).render_rust().as_deref(),
10306 Some("Dom::create_progress_no_a11y(0.0, 1.0)"),
10307 "missing value/max fall back to 0.0 / 1.0 as float literals"
10308 );
10309
10310 let m = node("meter", &[("value", "5"), ("min", "-1"), ("max", "10")], vec![]);
10311 assert_eq!(
10312 analyze_node_ctor("meter", &m).render_c().as_deref(),
10313 Some("AzDom_createMeterNoA11y(5.0f, -1.0f, 10.0f)")
10314 );
10315 }
10316
10317 #[test]
10320 fn analyze_node_ctor_non_finite_attributes_emit_non_finite_literals() {
10321 let p = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10322 let rust = analyze_node_ctor("progress", &p).render_rust().expect("semantic");
10323 assert_eq!(rust, "Dom::create_progress_no_a11y(NaN, inf)");
10324 }
10325
10326 #[test]
10327 fn ctor_arg_render_targets_are_distinct() {
10328 let s = CtorArg::Str("a\"b".to_string());
10329 assert_eq!(s.render_rust(), "AzString::from(\"a\\\"b\")");
10330 assert_eq!(s.render_c(), "AZ_STR(\"a\\\"b\")");
10331 assert_eq!(s.render_cpp(), "String(\"a\\\"b\")");
10332 assert_eq!(s.render_python(), "\"a\\\"b\"");
10333
10334 assert_eq!(CtorArg::OptNone.render_rust(), "OptionString::None");
10335 assert_eq!(CtorArg::OptNone.render_c(), "AzOptionString_none()");
10336 assert_eq!(CtorArg::OptNone.render_cpp(), "OptionString::none()");
10337 assert_eq!(CtorArg::OptNone.render_python(), "azul.OptionString.none()");
10338
10339 assert_eq!(CtorArg::Float(0.0).render_rust(), "0.0");
10340 assert_eq!(CtorArg::Float(0.0).render_c(), "0.0f");
10341 assert_eq!(CtorArg::Float(f32::NAN).render_c(), "NaNf");
10342 }
10343
10344 #[test]
10345 fn node_ctor_render_fluent_returns_none_for_non_fluent_targets() {
10346 let ctor = analyze_node_ctor("p", &node("p", &[], vec![txt("x")]));
10347 assert!(ctor.render_fluent(&CompileTarget::Rust).is_none());
10348 assert!(ctor.render_fluent(&CompileTarget::C).is_none());
10349 assert!(ctor.render_fluent(&CompileTarget::Cpp).is_some());
10350 assert!(ctor.render_fluent(&CompileTarget::Python).is_some());
10351 }
10352
10353 #[test]
10358 fn format_component_args_empty_and_ordering() {
10359 assert_eq!(format_component_args(&no_args()), "");
10360 assert_eq!(
10362 format_component_args(&args(&[("a", "u32"), ("b", "String")])),
10363 "b: String, a: u32"
10364 );
10365 }
10366
10367 #[test]
10368 fn format_component_args_edge_values_no_panic() {
10369 let a = args(&[("", ""), ("\u{1F600}", "\u{130}")]);
10370 let out = format_component_args(&a);
10371 assert!(out.contains(": "), "still emits `name: type` pairs, got {out:?}");
10372 }
10373
10374 #[test]
10375 fn compile_component_emits_a_render_fn() {
10376 let ca = ComponentArguments {
10377 args: args(&[("count", "u32")]),
10378 accepts_text: false,
10379 };
10380 let out = compile_component("MyWidget", &ca, "Dom::create_div()");
10381 assert!(out.contains("pub fn render(count: u32) -> Dom {"), "got:\n{out}");
10382 assert!(out.contains("#[inline]"), "a one-line body is inlined");
10383 }
10384
10385 #[test]
10386 fn compile_component_accepts_text_prepends_the_text_param() {
10387 let ca = ComponentArguments {
10388 args: args(&[("count", "u32")]),
10389 accepts_text: true,
10390 };
10391 let out = compile_component("my-widget", &ca, "Dom::create_div()");
10392 assert!(
10393 out.contains("pub fn render(text: AzString, count: u32) -> Dom {"),
10394 "got:\n{out}"
10395 );
10396
10397 let ca_no_args = ComponentArguments {
10398 args: no_args(),
10399 accepts_text: true,
10400 };
10401 let out = compile_component("w", &ca_no_args, "Dom::create_div()");
10402 assert!(
10403 out.contains("pub fn render(text: AzString) -> Dom {"),
10404 "no trailing comma when there are no extra args, got:\n{out}"
10405 );
10406 }
10407
10408 #[test]
10409 fn compile_component_empty_name_and_body_no_panic() {
10410 let ca = ComponentArguments::default();
10411 let out = compile_component("", &ca, "");
10412 assert!(out.contains("pub fn render() -> Dom {"), "got:\n{out}");
10413 }
10414
10415 #[test]
10416 fn compile_components_of_an_empty_list_is_empty() {
10417 assert_eq!(compile_components(Vec::new()), "");
10418 }
10419
10420 #[test]
10425 fn parse_svg_float_none_empty_whitespace_garbage() {
10426 assert_eq!(parse_svg_float(None), None);
10427 let empty = AzString::from("");
10428 assert_eq!(parse_svg_float(Some(&empty)), None);
10429 let ws = AzString::from(" \t\n");
10430 assert_eq!(parse_svg_float(Some(&ws)), None);
10431 let junk = AzString::from("10px");
10432 assert_eq!(parse_svg_float(Some(&junk)), None, "units are not stripped");
10433 let uni = AzString::from("\u{1F600}");
10434 assert_eq!(parse_svg_float(Some(&uni)), None);
10435 }
10436
10437 #[test]
10438 fn parse_svg_float_valid_and_boundary_numbers() {
10439 let padded = AzString::from(" 1.5 ");
10440 assert_eq!(parse_svg_float(Some(&padded)), Some(1.5), "value is trimmed");
10441 let zero = AzString::from("0");
10442 assert_eq!(parse_svg_float(Some(&zero)), Some(0.0));
10443 let negzero = AzString::from("-0");
10444 assert!(parse_svg_float(Some(&negzero)).unwrap().is_sign_negative());
10445 let huge = AzString::from("1e400");
10446 assert!(
10447 parse_svg_float(Some(&huge)).unwrap().is_infinite(),
10448 "overflow saturates to inf rather than erroring"
10449 );
10450 let nan = AzString::from("NaN");
10451 assert!(parse_svg_float(Some(&nan)).unwrap().is_nan());
10452 let inf = AzString::from("-inf");
10453 assert_eq!(parse_svg_float(Some(&inf)), Some(f32::NEG_INFINITY));
10454 }
10455
10456 #[test]
10457 fn parse_svg_points_rejects_degenerate_input() {
10458 assert!(parse_svg_points("", false).is_none());
10459 assert!(parse_svg_points(" ", false).is_none());
10460 assert!(parse_svg_points("garbage", false).is_none());
10461 assert!(parse_svg_points("1 2", false).is_none(), "a single point is not a line");
10462 assert!(
10463 parse_svg_points("1 2 3", false).is_none(),
10464 "an odd coordinate count is rejected"
10465 );
10466 assert!(parse_svg_points("\u{1F600}", false).is_none());
10467 }
10468
10469 #[test]
10470 fn parse_svg_points_valid_minimal_and_close() {
10471 let open = parse_svg_points("0,0 10,0", false).expect("two points => one line");
10472 assert_eq!(open.rings.as_ref().len(), 1);
10473 assert_eq!(open.rings.as_ref()[0].items.as_ref().len(), 1);
10474
10475 let closed = parse_svg_points("0,0 10,0 10,10", true).expect("triangle");
10477 assert_eq!(
10478 closed.rings.as_ref()[0].items.as_ref().len(),
10479 3,
10480 "2 segments + 1 closing segment"
10481 );
10482
10483 let already = parse_svg_points("0,0 10,0 0,0", true).expect("closed ring");
10485 assert_eq!(already.rings.as_ref()[0].items.as_ref().len(), 2);
10486 }
10487
10488 #[test]
10489 fn parse_svg_points_skips_unparsable_tokens_and_handles_boundaries() {
10490 let p = parse_svg_points("0,0 junk 10,0", false).expect("junk token dropped");
10492 assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 1);
10493
10494 let nan = parse_svg_points("NaN,0 1,1", false).expect("NaN is a parseable f32");
10495 assert_eq!(nan.rings.as_ref()[0].items.as_ref().len(), 1);
10496 }
10497
10498 #[test]
10499 fn parse_svg_points_extremely_long_terminates() {
10500 let pts = "1,2 ".repeat(20_000);
10501 let p = parse_svg_points(&pts, false).expect("20k points");
10502 assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 19_999);
10503 }
10504
10505 #[test]
10510 fn compact_dom_builder_new_and_with_capacity_start_empty() {
10511 for b in [
10512 CompactDomBuilder::new(),
10513 CompactDomBuilder::with_capacity(0),
10514 CompactDomBuilder::with_capacity(1),
10515 CompactDomBuilder::with_capacity(4096),
10516 ] {
10517 let fd = b.finish();
10518 assert_eq!(fd.node_hierarchy.as_ref().len(), 0);
10519 assert_eq!(fd.node_data.as_ref().len(), 0);
10520 assert_eq!(fd.css.as_ref().len(), 0);
10521 }
10522 assert_eq!(
10523 CompactDomBuilder::default().finish().node_data.as_ref().len(),
10524 0
10525 );
10526 }
10527
10528 #[test]
10529 fn compact_dom_builder_close_node_on_an_empty_stack_is_a_no_op() {
10530 let mut b = CompactDomBuilder::new();
10531 b.close_node();
10532 b.close_node();
10533 assert_eq!(b.finish().node_hierarchy.as_ref().len(), 0, "no panic, no nodes");
10534 }
10535
10536 #[test]
10537 fn compact_dom_builder_keeps_hierarchy_and_data_parallel() {
10538 let mut b = CompactDomBuilder::new();
10539 b.open_node(NodeData::create_node(NodeType::Html));
10540 b.add_leaf(NodeData::create_text("a"));
10541 b.add_leaf(NodeData::create_text("b"));
10542 b.close_node();
10543 let fd = b.finish();
10544 assert_eq!(fd.node_data.as_ref().len(), 3);
10545 assert_eq!(
10546 fd.node_hierarchy.as_ref().len(),
10547 fd.node_data.as_ref().len(),
10548 "the two arenas must stay the same length"
10549 );
10550 }
10551
10552 #[test]
10553 fn compact_dom_builder_unclosed_node_still_finishes() {
10554 let mut b = CompactDomBuilder::new();
10555 b.open_node(NodeData::create_node(NodeType::Div));
10556 let fd = b.finish();
10558 assert_eq!(fd.node_hierarchy.as_ref().len(), 1);
10559 assert_eq!(
10560 fd.node_hierarchy.as_ref()[0].last_child, 0,
10561 "last_child stays unset when close_node() is never called"
10562 );
10563 }
10564
10565 #[test]
10566 fn compact_dom_builder_add_css_accepts_zero_and_usize_max_node_ids() {
10567 let mut b = CompactDomBuilder::new();
10568 b.add_css(0, Css::empty());
10569 b.add_css(usize::MAX, Css::empty());
10570 let fd = b.finish();
10571 assert_eq!(fd.css.as_ref().len(), 2);
10572 assert_eq!(fd.css.as_ref()[0].node_id, 0);
10573 assert_eq!(
10574 fd.css.as_ref()[1].node_id,
10575 usize::MAX,
10576 "an out-of-range node id is stored verbatim (no bounds check, no panic)"
10577 );
10578 }
10579
10580 #[test]
10585 fn xml_node_to_dom_fast_depth_zero_builds_children() {
10586 let map = ComponentMap::default();
10587 let n = node("div", &[], vec![txt("hi"), elem(XmlNode::create("span"))]);
10588 let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10589 assert_eq!(dom.children.as_ref().len(), 2);
10590 }
10591
10592 #[test]
10593 fn xml_node_to_dom_fast_at_and_past_the_depth_cap_truncates_instead_of_panicking() {
10594 let map = ComponentMap::default();
10595 let n = node("div", &[], vec![txt("hi")]);
10596
10597 let at_cap = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH).expect("ok");
10598 assert!(
10599 at_cap.children.as_ref().is_empty(),
10600 "at the cap the node is emitted without children"
10601 );
10602
10603 let saturated = xml_node_to_dom_fast(&n, &map, false, usize::MAX)
10604 .expect("usize::MAX depth must not overflow when computing depth + 1");
10605 assert!(saturated.children.as_ref().is_empty());
10606
10607 let below = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH - 1).expect("ok");
10608 assert_eq!(below.children.as_ref().len(), 1, "one below the cap still recurses");
10609 }
10610
10611 #[test]
10612 fn xml_node_to_fast_dom_at_the_depth_cap_still_emits_the_node() {
10613 let map = ComponentMap::default();
10614 let n = node("div", &[], vec![txt("hi")]);
10615
10616 let mut b = CompactDomBuilder::new();
10617 xml_node_to_fast_dom(&n, &map, false, &mut b, usize::MAX).expect("no overflow");
10618 let fd = b.finish();
10619 assert_eq!(
10620 fd.node_data.as_ref().len(),
10621 1,
10622 "the node itself is still opened+closed, only its children are dropped"
10623 );
10624
10625 let mut b2 = CompactDomBuilder::new();
10626 xml_node_to_fast_dom(&n, &map, false, &mut b2, 0).expect("ok");
10627 assert_eq!(b2.finish().node_data.as_ref().len(), 2, "node + text child");
10628 }
10629
10630 #[test]
10631 fn apply_xml_node_attributes_extreme_tabindex_does_not_panic() {
10632 let map = ComponentMap::default();
10633 for v in [
10634 "0",
10635 "-1",
10636 "2147483647",
10637 "9223372036854775807",
10638 "-9223372036854775808",
10639 "99999999999999999999999999999999",
10640 "abc",
10641 "",
10642 "\u{1F600}",
10643 ] {
10644 let n = node("div", &[("tabindex", v), ("focusable", "true")], vec![]);
10645 assert!(
10646 xml_node_to_dom_fast(&n, &map, false, 0).is_ok(),
10647 "tabindex={v:?} must not panic"
10648 );
10649 }
10650 }
10651
10652 #[test]
10653 fn apply_xml_node_attributes_img_width_height_garbage_falls_back_to_zero() {
10654 let map = ComponentMap::default();
10655 let n = node(
10656 "img",
10657 &[("src", "a.png"), ("width", "-5"), ("height", "not-a-number")],
10658 vec![],
10659 );
10660 let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10661 match dom.root.get_node_type() {
10662 NodeType::Image(_) => {}
10663 other => panic!("expected an Image node, got {other:?}"),
10664 }
10665 }
10666
10667 #[test]
10672 fn set_stringified_attributes_zero_tabs_and_empty_attrs() {
10673 let mut s = String::new();
10674 set_stringified_attributes(&mut s, &attrs(&[]), &no_args(), 0);
10675 assert_eq!(s, "", "nothing to emit for an attribute-less node");
10676 }
10677
10678 #[test]
10679 fn set_stringified_attributes_splits_ids_and_classes_on_whitespace() {
10680 let mut s = String::new();
10681 set_stringified_attributes(
10682 &mut s,
10683 &attrs(&[("id", "a b"), ("class", "c\td")]),
10684 &no_args(),
10685 0,
10686 );
10687 assert!(s.contains(".with_id(\"a\")"), "got {s:?}");
10688 assert!(s.contains(".with_id(\"b\")"));
10689 assert!(s.contains(".with_class(\"c\")"));
10690 assert!(s.contains(".with_class(\"d\")"));
10691 }
10692
10693 #[test]
10694 fn set_stringified_attributes_tabindex_boundaries() {
10695 let cases: &[(&str, &str)] = &[
10696 ("0", "TabIndex::Auto"),
10697 ("5", "TabIndex::OverrideInParent(5)"),
10698 ("-1", "TabIndex::NoKeyboardFocus"),
10699 ];
10700 for (val, expected) in cases {
10701 let mut s = String::new();
10702 set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10703 assert!(s.contains(expected), "tabindex={val:?} => {s:?}");
10704 }
10705
10706 for val in ["abc", "", "99999999999999999999999999999999", "1.5"] {
10708 let mut s = String::new();
10709 set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10710 assert!(
10711 !s.contains("TabIndex"),
10712 "tabindex={val:?} must be ignored, got {s:?}"
10713 );
10714 }
10715 }
10716
10717 #[test]
10718 fn set_stringified_attributes_focusable_only_accepts_exact_true_false() {
10719 let mut s = String::new();
10720 set_stringified_attributes(&mut s, &attrs(&[("focusable", "true")]), &no_args(), 0);
10721 assert!(s.contains("TabIndex::Auto"));
10722
10723 let mut s = String::new();
10724 set_stringified_attributes(&mut s, &attrs(&[("focusable", "false")]), &no_args(), 0);
10725 assert!(s.contains("TabIndex::NoKeyboardFocus"));
10726
10727 let mut s = String::new();
10728 set_stringified_attributes(&mut s, &attrs(&[("focusable", "TRUE")]), &no_args(), 0);
10729 assert!(s.is_empty(), "casing other than `true`/`false` is ignored, got {s:?}");
10730 }
10731
10732 #[test]
10733 fn set_stringified_attributes_large_tab_depth_does_not_overflow() {
10734 let mut s = String::new();
10737 set_stringified_attributes(&mut s, &attrs(&[("id", "x")]), &no_args(), 1_000);
10738 assert!(s.contains(".with_id(\"x\")"));
10739 assert!(s.len() > 4_000, "the 1000-level indent is actually emitted");
10740 }
10741
10742 fn refs(v: &[CssPathSelector]) -> Vec<&CssPathSelector> {
10747 v.iter().collect()
10748 }
10749
10750 #[test]
10751 fn group_matches_global_matches_at_any_index() {
10752 let a = vec![CssPathSelector::Global];
10753 assert!(group_matches(&refs(&a), &[], 0, 0));
10754 assert!(
10755 group_matches(&refs(&a), &[], usize::MAX, usize::MAX),
10756 "usize::MAX indices must not overflow"
10757 );
10758 }
10759
10760 #[test]
10761 fn group_matches_type_class_id() {
10762 let div = vec![CssPathSelector::Type(NodeTypeTag::Div)];
10763 let p = vec![CssPathSelector::Type(NodeTypeTag::P)];
10764 assert!(group_matches(&refs(&div), &refs(&div), 0, 1));
10765 assert!(!group_matches(&refs(&div), &refs(&p), 0, 1));
10766 assert!(!group_matches(&refs(&div), &[], 0, 1), "an empty haystack never matches");
10767
10768 let cls = vec![CssPathSelector::Class(AzString::from("x"))];
10769 assert!(group_matches(&refs(&cls), &refs(&cls), 0, 1));
10770 let id = vec![CssPathSelector::Id(AzString::from("x"))];
10771 assert!(!group_matches(&refs(&id), &refs(&cls), 0, 1), "an id is not a class");
10772 }
10773
10774 #[test]
10775 fn group_matches_first_and_last_pseudo_at_boundaries() {
10776 let first = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::First)];
10777 assert!(group_matches(&refs(&first), &[], 0, 10));
10778 assert!(!group_matches(&refs(&first), &[], 1, 10));
10779
10780 let last = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last)];
10781 assert!(group_matches(&refs(&last), &[], 9, 10));
10782 assert!(!group_matches(&refs(&last), &[], 8, 10));
10783 assert!(
10784 group_matches(&refs(&last), &[], 0, 0),
10785 "parent_children == 0 saturates to 0, so index 0 counts as last"
10786 );
10787 }
10788
10789 #[test]
10790 fn group_matches_nth_child_even_odd_and_number() {
10791 let even = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10792 CssNthChildSelector::Even,
10793 ))];
10794 assert!(group_matches(&refs(&even), &[], 0, 0));
10795 assert!(!group_matches(&refs(&even), &[], 1, 0));
10796 assert!(
10797 !group_matches(&refs(&even), &[], usize::MAX, 0),
10798 "usize::MAX is odd"
10799 );
10800
10801 let odd = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10802 CssNthChildSelector::Odd,
10803 ))];
10804 assert!(group_matches(&refs(&odd), &[], 1, 0));
10805 assert!(!group_matches(&refs(&odd), &[], 2, 0));
10806
10807 let n = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10808 CssNthChildSelector::Number(u32::MAX),
10809 ))];
10810 assert!(group_matches(&refs(&n), &[], u32::MAX as usize, 0));
10811 assert!(!group_matches(&refs(&n), &[], 0, 0));
10812 }
10813
10814 #[test]
10815 fn group_matches_nth_child_pattern_zero_repeat_does_not_divide_by_zero() {
10816 let zero = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10817 CssNthChildSelector::Pattern(CssNthChildPattern {
10818 pattern_repeat: 0,
10819 offset: 0,
10820 }),
10821 ))];
10822 assert!(group_matches(&refs(&zero), &[], 0, 0));
10824 assert!(!group_matches(&refs(&zero), &[], 5, 0));
10825
10826 let offset_past = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10827 CssNthChildSelector::Pattern(CssNthChildPattern {
10828 pattern_repeat: 2,
10829 offset: u32::MAX,
10830 }),
10831 ))];
10832 assert!(
10833 group_matches(&refs(&offset_past), &[], 0, 0),
10834 "index - offset saturates to 0 rather than underflowing"
10835 );
10836 }
10837
10838 #[test]
10839 fn group_matches_structural_combinators_never_match() {
10840 for sel in [
10841 CssPathSelector::Children,
10842 CssPathSelector::DirectChildren,
10843 CssPathSelector::AdjacentSibling,
10844 CssPathSelector::GeneralSibling,
10845 ] {
10846 let a = vec![sel.clone()];
10847 assert!(
10848 !group_matches(&refs(&a), &refs(&a), 0, 1),
10849 "{sel:?} is a combinator, not a matchable group member"
10850 );
10851 }
10852 }
10853
10854 #[test]
10855 fn css_matcher_empty_path_never_matches() {
10856 let m = CssMatcher {
10857 path: Vec::new(),
10858 indices_in_parent: vec![0],
10859 children_length: vec![0],
10860 };
10861 let path = CssPath {
10862 selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10863 };
10864 assert!(!m.matches(&path), "an empty matcher path can never match");
10865
10866 let m2 = CssMatcher {
10867 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10868 indices_in_parent: vec![0],
10869 children_length: vec![0],
10870 };
10871 let empty_path = CssPath {
10872 selectors: Vec::new().into(),
10873 };
10874 assert!(!m2.matches(&empty_path), "an empty CSS path can never match");
10875 }
10876
10877 #[test]
10878 fn css_matcher_get_hash_is_deterministic_and_path_sensitive() {
10879 let a = CssMatcher {
10880 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10881 indices_in_parent: vec![0],
10882 children_length: vec![0],
10883 };
10884 let b = CssMatcher {
10885 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10886 indices_in_parent: vec![9],
10887 children_length: vec![9],
10888 };
10889 let c = CssMatcher {
10890 path: vec![CssPathSelector::Type(NodeTypeTag::Div)],
10891 indices_in_parent: vec![0],
10892 children_length: vec![0],
10893 };
10894 assert_eq!(a.get_hash(), a.get_hash(), "stable across calls");
10895 assert_eq!(
10896 a.get_hash(),
10897 b.get_hash(),
10898 "the hash covers only `path`, not the sibling indices"
10899 );
10900 assert_ne!(a.get_hash(), c.get_hash());
10901
10902 let empty = CssMatcher {
10903 path: Vec::new(),
10904 indices_in_parent: Vec::new(),
10905 children_length: Vec::new(),
10906 };
10907 let _ = empty.get_hash(); }
10909
10910 #[test]
10911 fn css_matcher_mismatched_bookkeeping_vec_lengths_bail_out() {
10912 let m = CssMatcher {
10914 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10915 indices_in_parent: Vec::new(),
10916 children_length: Vec::new(),
10917 };
10918 let path = CssPath {
10919 selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10920 };
10921 assert!(
10922 !m.matches(&path),
10923 "a desynced matcher must return false, not index out of bounds"
10924 );
10925 }
10926
10927 #[test]
10928 fn get_css_blocks_and_inline_string_on_empty_css() {
10929 let m = CssMatcher {
10930 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10931 indices_in_parent: vec![0],
10932 children_length: vec![0],
10933 };
10934 assert!(get_css_blocks(&Css::empty(), &m).is_empty());
10935 assert_eq!(css_blocks_to_inline_string(&[]), "");
10936 }
10937
10938 #[test]
10943 fn str_to_dom_rejects_documents_without_html_or_body() {
10944 let map = ComponentMap::with_builtin();
10945 assert_eq!(
10946 str_to_dom(&[], &map, None).unwrap_err(),
10947 DomXmlParseError::NoHtmlNode
10948 );
10949 let html_only = vec![elem(XmlNode::create("html"))];
10950 assert_eq!(
10951 str_to_dom(&html_only, &map, None).unwrap_err(),
10952 DomXmlParseError::NoBodyInHtml
10953 );
10954 assert!(str_to_dom_unstyled(&[], &map).is_err());
10955 }
10956
10957 #[test]
10958 fn str_to_dom_valid_minimal() {
10959 let map = ComponentMap::with_builtin();
10960 let d = doc("body { color: red; }", vec![elem(node("div", &[("id", "x")], vec![]))]);
10961 assert!(str_to_dom(&d, &map, None).is_ok());
10962 assert!(str_to_dom_unstyled(&d, &map).is_ok());
10963 }
10964
10965 #[test]
10966 fn str_to_dom_max_width_edge_values_do_not_panic() {
10967 let map = ComponentMap::with_builtin();
10968 let d = doc("", vec![elem(XmlNode::create("div"))]);
10969 for w in [
10970 Some(0.0f32),
10971 Some(-0.0),
10972 Some(-1.0),
10973 Some(f32::MAX),
10974 Some(f32::MIN),
10975 Some(f32::NAN),
10976 Some(f32::INFINITY),
10977 Some(f32::NEG_INFINITY),
10978 None,
10979 ] {
10980 assert!(
10981 str_to_dom(&d, &map, w).is_ok(),
10982 "max_width={w:?} is formatted straight into a CSS string and must not panic"
10983 );
10984 }
10985 }
10986
10987 #[test]
10988 fn str_to_dom_deeply_nested_body_is_depth_capped_not_stack_overflowing() {
10989 let map = ComponentMap::with_builtin();
10990 let deep = wrap_divs(2_000, node("div", &[("id", "bottom")], vec![]));
10991 let d = doc("", vec![elem(deep)]);
10992 assert!(
10993 str_to_dom(&d, &map, None).is_ok(),
10994 "children past MAX_XML_NESTING_DEPTH are dropped, not crashed on"
10995 );
10996 }
10997
10998 #[test]
10999 fn parse_page_style_and_body_and_body_matcher() {
11000 let d = doc("body { color: red; }", vec![elem(XmlNode::create("div"))]);
11001 let (css, body) = parse_page_style_and_body(&d).expect("well-formed page");
11002 assert_eq!(body.node_type.as_str(), "body");
11003 assert!(!css.rules.as_ref().is_empty(), "the <style> block is parsed");
11004
11005 let m = body_matcher(body);
11006 assert!(m.path.is_empty(), "the matcher starts with an empty path");
11007 assert_eq!(m.indices_in_parent, vec![0]);
11008 assert_eq!(m.children_length, vec![body.children.as_ref().len()]);
11009 }
11010
11011 #[test]
11012 fn parse_page_style_and_body_with_no_style_block() {
11013 let head = node("head", &[], vec![]);
11014 let body = node("body", &[], vec![]);
11015 let d = vec![elem(node("html", &[], vec![elem(head), elem(body)]))];
11016 let (css, body) = parse_page_style_and_body(&d).expect("ok");
11017 assert!(css.rules.as_ref().is_empty());
11018 assert_eq!(body.children.as_ref().len(), 0);
11019 }
11020
11021 #[test]
11026 fn str_to_rust_code_empty_input_is_an_error_not_a_panic() {
11027 let map = ComponentMap::with_builtin();
11028 assert!(matches!(
11029 str_to_rust_code(&[], "", &map),
11030 Err(CompileError::Xml(DomXmlParseError::NoHtmlNode))
11031 ));
11032 assert!(str_to_c_code(&[], &map).is_err());
11033 assert!(str_to_cpp_code(&[], &map).is_err());
11034 assert!(str_to_python_code(&[], &map).is_err());
11035 }
11036
11037 #[test]
11038 fn str_to_rust_code_whitespace_and_text_only_roots_are_errors() {
11039 let map = ComponentMap::with_builtin();
11040 for roots in [vec![txt(" ")], vec![txt("\t\n")], vec![txt("garbage")]] {
11041 assert!(
11042 str_to_rust_code(&roots, "", &map).is_err(),
11043 "a document with no <html> element must be rejected"
11044 );
11045 }
11046 }
11047
11048 #[test]
11049 fn str_to_rust_code_valid_minimal() {
11050 let map = ComponentMap::with_builtin();
11051 let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
11052 let src = str_to_rust_code(&d, "// imports", &map).expect("compiles");
11053 assert!(src.contains("Dom::create_body()"), "got:\n{src}");
11054 assert!(src.contains("Dom::create_p_with_text(AzString::from(\"Hi\"))"));
11055 assert!(src.contains("// imports"), "the imports blob is spliced in");
11056 assert!(src.contains("fn main()"));
11057 }
11058
11059 #[test]
11060 fn str_to_c_cpp_python_code_valid_minimal() {
11061 let map = ComponentMap::with_builtin();
11062 let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
11063
11064 let c = str_to_c_code(&d, &map).expect("compiles");
11065 assert!(c.contains("#include \"azul.h\""), "got:\n{c}");
11066 assert!(c.contains("AzDom n0 = AzDom_createBody();"));
11067 assert!(c.contains("AzDom_createPWithText(AZ_STR(\"Hi\"))"));
11068
11069 let cpp = str_to_cpp_code(&d, &map).expect("compiles");
11070 assert!(cpp.contains("#include \"azul20.hpp\""), "got:\n{cpp}");
11071 assert!(cpp.contains("Dom::create_p_with_text(String(\"Hi\"))"));
11072
11073 let py = str_to_python_code(&d, &map).expect("compiles");
11074 assert!(py.contains("import azul"), "got:\n{py}");
11075 assert!(py.contains("azul.Dom.create_p_with_text(\"Hi\")"));
11076 }
11077
11078 #[test]
11079 fn compile_targets_escape_quotes_in_text_content() {
11080 let map = ComponentMap::with_builtin();
11081 let d = doc("", vec![elem(node("div", &[], vec![txt("say \"hi\"")]))]);
11082
11083 let rust = str_to_rust_code(&d, "", &map).expect("compiles");
11084 assert!(rust.contains("say \\\"hi\\\""), "got:\n{rust}");
11085 let c = str_to_c_code(&d, &map).expect("compiles");
11086 assert!(c.contains("say \\\"hi\\\""), "got:\n{c}");
11087 }
11088
11089 #[test]
11090 fn compile_body_node_to_rust_code_on_an_empty_body() {
11091 let map = ComponentMap::with_builtin();
11092 let body = node("body", &[], vec![]);
11093 let mut extra = VecContents::default();
11094 let mut blocks = BTreeMap::new();
11095 let out = compile_body_node_to_rust_code(
11096 &body,
11097 &map,
11098 &mut extra,
11099 &mut blocks,
11100 &Css::empty(),
11101 body_matcher(&body),
11102 )
11103 .expect("ok");
11104 assert_eq!(out, "Dom::create_body()", "no children => no .with_children()");
11105 }
11106
11107 #[test]
11108 fn compile_body_node_to_rust_code_skips_whitespace_only_text_children() {
11109 let map = ComponentMap::with_builtin();
11110 let body = node("body", &[], vec![txt(" \n\t ")]);
11111 let mut extra = VecContents::default();
11112 let mut blocks = BTreeMap::new();
11113 let out = compile_body_node_to_rust_code(
11114 &body,
11115 &map,
11116 &mut extra,
11117 &mut blocks,
11118 &Css::empty(),
11119 body_matcher(&body),
11120 )
11121 .expect("ok");
11122 assert!(
11123 !out.contains("create_text"),
11124 "a whitespace-only text child emits nothing, got:\n{out}"
11125 );
11126 }
11127
11128 #[test]
11133 fn builtin_render_fn_for_a_text_and_a_textless_element() {
11134 let map = ComponentMap::with_builtin();
11135 let div = map.get_unqualified("div").expect("builtin div");
11136 assert!(matches!(
11137 builtin_render_fn(div, &div.data_model, &map),
11138 ResultStyledDomRenderDomError::Ok(_)
11139 ));
11140
11141 let p = map.get_unqualified("p").expect("builtin p");
11142 assert!(matches!(
11143 builtin_render_fn(p, &p.data_model, &map),
11144 ResultStyledDomRenderDomError::Ok(_)
11145 ));
11146 }
11147
11148 #[test]
11149 fn builtin_compile_fn_ignores_indent_so_usize_max_is_safe() {
11150 let map = ComponentMap::with_builtin();
11151 let div = map.get_unqualified("div").expect("builtin div");
11152 for indent in [0usize, 1, 1024, usize::MAX] {
11153 match builtin_compile_fn(div, &CompileTarget::Rust, &div.data_model, indent) {
11154 ResultStringCompileError::Ok(s) => assert_eq!(
11155 s.as_str(),
11156 "Dom::create_node(NodeType::Div)",
11157 "indent is unused by builtin_compile_fn (indent={indent})"
11158 ),
11159 ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11160 }
11161 }
11162 }
11163
11164 #[test]
11165 fn builtin_compile_fn_emits_text_and_escapes_it() {
11166 let map = ComponentMap::with_builtin();
11167 let p = map.get_unqualified("p").expect("builtin p");
11168 let data = p
11169 .data_model
11170 .clone()
11171 .with_default("text", ComponentDefaultValue::String(AzString::from("a\"b\\c")));
11172
11173 match builtin_compile_fn(p, &CompileTarget::Rust, &data, 0) {
11174 ResultStringCompileError::Ok(s) => {
11175 assert!(s.as_str().contains("a\\\"b\\\\c"), "got {}", s.as_str());
11176 }
11177 ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11178 }
11179 }
11180
11181 #[test]
11182 fn builtin_compile_fn_covers_every_target() {
11183 let map = ComponentMap::with_builtin();
11184 let div = map.get_unqualified("div").expect("builtin div");
11185 for target in [
11186 CompileTarget::Rust,
11187 CompileTarget::C,
11188 CompileTarget::Cpp,
11189 CompileTarget::Python,
11190 ] {
11191 match builtin_compile_fn(div, &target, &div.data_model, 0) {
11192 ResultStringCompileError::Ok(s) => {
11193 assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11194 }
11195 ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11196 }
11197 }
11198 }
11199
11200 fn every_default_kind() -> Vec<ComponentDataField> {
11205 use ComponentDefaultValue as D;
11206 vec![
11207 data_field("s", ComponentFieldType::String, Some(D::String(AzString::from("txt"))), ""),
11208 data_field("b", ComponentFieldType::Bool, Some(D::Bool(true)), ""),
11209 data_field("i32", ComponentFieldType::I32, Some(D::I32(i32::MIN)), ""),
11210 data_field("i64", ComponentFieldType::I64, Some(D::I64(i64::MIN)), ""),
11211 data_field("u32", ComponentFieldType::U32, Some(D::U32(u32::MAX)), ""),
11212 data_field("u64", ComponentFieldType::U64, Some(D::U64(u64::MAX)), ""),
11213 data_field("us", ComponentFieldType::Usize, Some(D::Usize(usize::MAX)), ""),
11214 data_field("f32", ComponentFieldType::F32, Some(D::F32(f32::NAN)), ""),
11215 data_field("f64", ComponentFieldType::F64, Some(D::F64(f64::INFINITY)), ""),
11216 data_field(
11217 "c",
11218 ComponentFieldType::ColorU,
11219 Some(D::ColorU(ColorU { r: 0, g: 0, b: 0, a: 0 })),
11220 "",
11221 ),
11222 data_field("cb", ComponentFieldType::StyledDom, Some(D::CallbackFnPointer(AzString::from("on_click"))), ""),
11223 data_field("j", ComponentFieldType::String, Some(D::Json(AzString::from("{}"))), ""),
11224 data_field("none", ComponentFieldType::String, Some(D::None), ""),
11225 data_field("missing", ComponentFieldType::String, None, ""),
11226 ]
11227 }
11228
11229 #[test]
11230 fn user_defined_render_fn_handles_every_default_value_kind() {
11231 let map = ComponentMap::with_builtin();
11232 let def = user_def("", every_default_kind());
11233 assert!(matches!(
11234 user_defined_render_fn(&def, &def.data_model, &map),
11235 ResultStyledDomRenderDomError::Ok(_)
11236 ));
11237 }
11238
11239 #[test]
11240 fn user_defined_render_fn_on_an_empty_model_and_with_css() {
11241 let map = ComponentMap::with_builtin();
11242 let empty = user_def("", Vec::new());
11243 assert!(matches!(
11244 user_defined_render_fn(&empty, &empty.data_model, &map),
11245 ResultStyledDomRenderDomError::Ok(_)
11246 ));
11247
11248 let styled = user_def(".widget { color: red; }", Vec::new());
11249 assert!(matches!(
11250 user_defined_render_fn(&styled, &styled.data_model, &map),
11251 ResultStyledDomRenderDomError::Ok(_)
11252 ));
11253 }
11254
11255 #[test]
11256 fn user_defined_render_fn_unknown_sub_component_renders_a_placeholder() {
11257 let map = ComponentMap::create(); let def = user_def(
11259 "",
11260 vec![data_field(
11261 "child",
11262 ComponentFieldType::StyledDom,
11263 Some(ComponentDefaultValue::ComponentInstance(ComponentInstanceDefault {
11264 library: AzString::from("nope"),
11265 component: AzString::from("missing"),
11266 field_overrides: Vec::new().into(),
11267 })),
11268 "",
11269 )],
11270 );
11271 assert!(
11272 matches!(
11273 user_defined_render_fn(&def, &def.data_model, &map),
11274 ResultStyledDomRenderDomError::Ok(_)
11275 ),
11276 "an unresolvable sub-component must render a placeholder, not error out"
11277 );
11278 }
11279
11280 #[test]
11281 fn user_defined_compile_fn_indent_zero_and_every_target() {
11282 let def = user_def("", every_default_kind());
11283 for target in [
11284 CompileTarget::Rust,
11285 CompileTarget::C,
11286 CompileTarget::Cpp,
11287 CompileTarget::Python,
11288 ] {
11289 match user_defined_compile_fn(&def, &target, &def.data_model, 0) {
11290 ResultStringCompileError::Ok(s) => {
11291 assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11292 }
11293 ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11294 }
11295 }
11296 }
11297
11298 #[test]
11299 fn user_defined_compile_fn_indent_scales_the_leading_whitespace() {
11300 let def = user_def("", Vec::new());
11303 let mut prev = 0usize;
11304 for indent in [0usize, 1, 2, 8] {
11305 match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, indent) {
11306 ResultStringCompileError::Ok(s) => {
11307 let len = s.as_str().len();
11308 assert!(len > prev, "indent={indent} must widen the output");
11309 prev = len;
11310 }
11311 ResultStringCompileError::Err(e) => panic!("indent={indent}: {e:?}"),
11312 }
11313 }
11314 }
11315
11316 #[test]
11317 fn user_defined_compile_fn_escapes_string_defaults() {
11318 let def = user_def(
11319 "",
11320 vec![data_field(
11321 "s",
11322 ComponentFieldType::String,
11323 Some(ComponentDefaultValue::String(AzString::from("a\"b\\c"))),
11324 "",
11325 )],
11326 );
11327 match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, 0) {
11328 ResultStringCompileError::Ok(s) => {
11329 assert!(s.as_str().contains("a\\\"b\\\\c"), "got:\n{}", s.as_str());
11330 }
11331 ResultStringCompileError::Err(e) => panic!("{e:?}"),
11332 }
11333 }
11334
11335 #[test]
11336 fn push_scalar_field_appends_one_div_per_call() {
11337 let mut children: Vec<Dom> = Vec::new();
11338 push_scalar_field(&mut children, "n", &i64::MIN);
11339 push_scalar_field(&mut children, "", &f32::NAN);
11340 push_scalar_field(&mut children, "\u{1F600}", &usize::MAX);
11341 assert_eq!(children.len(), 3);
11342 }
11343
11344 #[test]
11349 fn builtin_if_for_map_component_defs_are_well_formed() {
11350 for (def, model, field) in [
11351 (builtin_if_component(), "IfData", "condition"),
11352 (builtin_for_component(), "ForData", "count"),
11353 (builtin_map_component(), "MapData", "data_json"),
11354 ] {
11355 assert_eq!(def.id.collection.as_str(), "builtin");
11356 assert_eq!(def.data_model.name.as_str(), model);
11357 assert!(
11358 def.data_model.get_field(field).is_some(),
11359 "{model} must expose `{field}`"
11360 );
11361 }
11362 }
11363
11364 #[test]
11365 fn builtin_if_render_fn_defaults_to_the_else_branch() {
11366 let map = ComponentMap::create();
11367 let def = builtin_if_component();
11368 let empty = dm("IfData", Vec::new());
11370 assert!(matches!(
11371 builtin_if_render_fn(&def, &empty, &map),
11372 ResultStyledDomRenderDomError::Ok(_)
11373 ));
11374
11375 let truthy = def
11376 .data_model
11377 .clone()
11378 .with_default("condition", ComponentDefaultValue::Bool(true));
11379 assert!(matches!(
11380 builtin_if_render_fn(&def, &truthy, &map),
11381 ResultStyledDomRenderDomError::Ok(_)
11382 ));
11383 }
11384
11385 #[test]
11386 fn builtin_for_render_fn_handles_zero_and_a_wrongly_typed_count() {
11387 let map = ComponentMap::create();
11388 let def = builtin_for_component();
11389
11390 let zero = def
11391 .data_model
11392 .clone()
11393 .with_default("count", ComponentDefaultValue::U32(0));
11394 assert!(matches!(
11395 builtin_for_render_fn(&def, &zero, &map),
11396 ResultStyledDomRenderDomError::Ok(_)
11397 ));
11398
11399 let wrong_type = def
11401 .data_model
11402 .clone()
11403 .with_default("count", ComponentDefaultValue::String(AzString::from("9")));
11404 assert!(matches!(
11405 builtin_for_render_fn(&def, &wrong_type, &map),
11406 ResultStyledDomRenderDomError::Ok(_)
11407 ));
11408 }
11409
11410 #[test]
11411 fn builtin_map_render_fn_defaults_to_an_empty_json_array() {
11412 let map = ComponentMap::create();
11413 let def = builtin_map_component();
11414 assert!(matches!(
11415 builtin_map_render_fn(&def, &dm("MapData", Vec::new()), &map),
11416 ResultStyledDomRenderDomError::Ok(_)
11417 ));
11418 let garbage = def
11419 .data_model
11420 .clone()
11421 .with_default("data_json", ComponentDefaultValue::String(AzString::from("{{{")));
11422 assert!(
11423 matches!(
11424 builtin_map_render_fn(&def, &garbage, &map),
11425 ResultStyledDomRenderDomError::Ok(_)
11426 ),
11427 "malformed JSON must not panic — it is only echoed into a label"
11428 );
11429 }
11430
11431 #[test]
11432 fn structural_builtin_compile_fns_ignore_indent_entirely() {
11433 let cases: [(ComponentDef, ComponentCompileFn); 3] = [
11434 (builtin_if_component(), builtin_if_compile_fn),
11435 (builtin_for_component(), builtin_for_compile_fn),
11436 (builtin_map_component(), builtin_map_compile_fn),
11437 ];
11438 for (def, f) in cases {
11439 for target in [
11440 CompileTarget::Rust,
11441 CompileTarget::C,
11442 CompileTarget::Cpp,
11443 CompileTarget::Python,
11444 ] {
11445 for indent in [0usize, usize::MAX] {
11446 match f(&def, &target, &def.data_model, indent) {
11447 ResultStringCompileError::Ok(s) => {
11448 assert!(!s.as_str().is_empty(), "{target:?}/{indent} emitted nothing");
11449 }
11450 ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11451 }
11452 }
11453 }
11454 }
11455 }
11456
11457 #[test]
11462 fn data_field_required_is_the_inverse_of_having_a_default() {
11463 let with = data_field(
11464 "x",
11465 ComponentFieldType::String,
11466 Some(ComponentDefaultValue::String(AzString::from("v"))),
11467 "d",
11468 );
11469 assert!(!with.required);
11470 assert_eq!(with.description.as_str(), "d");
11471
11472 let without = data_field("x", ComponentFieldType::String, None, "");
11473 assert!(without.required);
11474 assert!(matches!(
11475 without.default_value,
11476 OptionComponentDefaultValue::None
11477 ));
11478 }
11479
11480 #[test]
11481 fn builtin_data_model_unknown_tag_is_empty() {
11482 assert!(builtin_data_model("").is_empty());
11483 assert!(builtin_data_model("div").is_empty());
11484 assert!(builtin_data_model("\u{1F600}").is_empty());
11485 assert!(builtin_data_model(&"z".repeat(10_000)).is_empty());
11486 }
11487
11488 #[test]
11489 fn builtin_data_model_known_tags_expose_their_attributes() {
11490 let a = builtin_data_model("a");
11491 assert!(
11492 a.iter().any(|f| f.name.as_str() == "href"),
11493 "<a> must expose href"
11494 );
11495 let img = builtin_data_model("img");
11497 let src = img
11498 .iter()
11499 .find(|f| f.name.as_str() == "src")
11500 .expect("img has src");
11501 assert!(src.required, "<img src> must be a required field");
11502 assert_eq!(builtin_data_model("image").len(), img.len());
11504 }
11505
11506 #[test]
11507 fn builtin_component_def_default_text_controls_the_text_field() {
11508 let with_text = builtin_component_def("p", "Paragraph", Some("Hi"), "");
11509 assert_eq!(
11510 with_text.data_model.get_default_string("text").map(AzString::as_str),
11511 Some("Hi")
11512 );
11513 assert_eq!(with_text.data_model.name.as_str(), "ParagraphData");
11514 assert_eq!(with_text.id.qualified_name(), "builtin:p");
11515
11516 let no_text = builtin_component_def("div", "Div", None, "");
11517 assert!(
11518 no_text.data_model.get_field("text").is_none(),
11519 "a `None` default_text means the element has no text field at all"
11520 );
11521
11522 let empty_text = builtin_component_def("span", "Span", Some(""), "");
11524 assert!(empty_text.data_model.get_field("text").is_some());
11525 assert_eq!(
11526 empty_text.data_model.get_default_string("text").map(AzString::as_str),
11527 Some("")
11528 );
11529 }
11530
11531 #[test]
11536 fn xml_attrs_to_data_model_overrides_defaults_from_attributes() {
11537 let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11538 let model = xml_attrs_to_data_model(&base, &attrs(&[("href", "/x")]), None);
11539 assert_eq!(
11540 model.get_default_string("href").map(AzString::as_str),
11541 Some("/x")
11542 );
11543 assert_eq!(
11544 model.get_default_string("text").map(AzString::as_str),
11545 Some("Link text"),
11546 "un-supplied fields keep their defaults"
11547 );
11548 assert_eq!(
11549 model.fields.as_ref().len(),
11550 base.fields.as_ref().len(),
11551 "no field is added or dropped"
11552 );
11553 }
11554
11555 #[test]
11556 fn xml_attrs_to_data_model_text_content_is_prepared_and_empty_text_is_ignored() {
11557 let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11558
11559 let with_text = xml_attrs_to_data_model(&base, &attrs(&[]), Some(" Hello & bye "));
11560 assert_eq!(
11561 with_text.get_default_string("text").map(AzString::as_str),
11562 Some("Hello & bye"),
11563 "text content is trimmed and entity-decoded"
11564 );
11565
11566 let blank = xml_attrs_to_data_model(&base, &attrs(&[]), Some(" \n\t "));
11567 assert_eq!(
11568 blank.get_default_string("text").map(AzString::as_str),
11569 Some("Link text"),
11570 "whitespace-only text content leaves the default intact"
11571 );
11572 }
11573
11574 #[test]
11575 fn xml_attrs_to_data_model_ignores_unknown_attributes() {
11576 let base = builtin_component_def("a", "Link", Some(""), "").data_model;
11577 let before = base.fields.as_ref().len();
11578 let model = xml_attrs_to_data_model(
11579 &base,
11580 &attrs(&[("data-nonsense", "1"), ("", ""), ("\u{1F600}", "x")]),
11581 None,
11582 );
11583 assert_eq!(
11584 model.fields.as_ref().len(),
11585 before,
11586 "unknown attributes must not create fields"
11587 );
11588 }
11589
11590 #[test]
11595 fn dom_xml_into_styled_dom_matches_the_from_impl() {
11596 let via_method: StyledDom = DomXml::default().into_styled_dom();
11597 let via_from: StyledDom = DomXml::default().into();
11598 assert_eq!(
11599 via_method, via_from,
11600 "into_styled_dom() must be exactly the From<DomXml> impl"
11601 );
11602 }
11603
11604 fn pos() -> XmlTextPos {
11609 XmlTextPos {
11610 row: u32::MAX,
11611 col: 0,
11612 }
11613 }
11614
11615 #[test]
11616 fn xml_text_pos_display_is_non_empty_for_edge_values() {
11617 assert_eq!(
11618 format!("{}", XmlTextPos { row: 0, col: 0 }),
11619 "line 0:0",
11620 "a zero position is still rendered"
11621 );
11622 assert_eq!(
11623 format!(
11624 "{}",
11625 XmlTextPos {
11626 row: u32::MAX,
11627 col: u32::MAX
11628 }
11629 ),
11630 "line 4294967295:4294967295"
11631 );
11632 }
11633
11634 #[test]
11635 fn xml_stream_error_display_covers_every_variant() {
11636 let variants = vec![
11637 XmlStreamError::UnexpectedEndOfStream,
11638 XmlStreamError::InvalidName,
11639 XmlStreamError::NonXmlChar(NonXmlCharError {
11640 ch: u32::MAX,
11641 pos: pos(),
11642 }),
11643 XmlStreamError::InvalidChar(InvalidCharError {
11644 expected: u8::MAX,
11645 got: 0,
11646 pos: pos(),
11647 }),
11648 XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
11649 expected: 0,
11650 got: Vec::<u8>::new().into(),
11651 pos: pos(),
11652 }),
11653 XmlStreamError::InvalidQuote(InvalidQuoteError { got: 0, pos: pos() }),
11654 XmlStreamError::InvalidSpace(InvalidSpaceError { got: 0, pos: pos() }),
11655 XmlStreamError::InvalidString(InvalidStringError {
11656 got: AzString::from(""),
11657 pos: pos(),
11658 }),
11659 XmlStreamError::InvalidReference,
11660 XmlStreamError::InvalidExternalID,
11661 XmlStreamError::InvalidCommentData,
11662 XmlStreamError::InvalidCommentEnd,
11663 XmlStreamError::InvalidCharacterData,
11664 ];
11665 for v in &variants {
11666 let s = format!("{v}");
11667 assert!(!s.is_empty(), "{v:?} must render a non-empty message");
11668 }
11669 assert!(format!("{}", variants[2]).contains("None"));
11671 }
11672
11673 #[test]
11674 fn xml_parse_error_display_covers_every_variant() {
11675 let te = XmlTextError {
11676 stream_error: XmlStreamError::InvalidName,
11677 pos: pos(),
11678 };
11679 let variants = vec![
11680 XmlParseError::InvalidDeclaration(te.clone()),
11681 XmlParseError::InvalidComment(te.clone()),
11682 XmlParseError::InvalidPI(te.clone()),
11683 XmlParseError::InvalidDoctype(te.clone()),
11684 XmlParseError::InvalidEntity(te.clone()),
11685 XmlParseError::InvalidElement(te.clone()),
11686 XmlParseError::InvalidAttribute(te.clone()),
11687 XmlParseError::InvalidCdata(te.clone()),
11688 XmlParseError::InvalidCharData(te),
11689 XmlParseError::UnknownToken(pos()),
11690 ];
11691 for v in &variants {
11692 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11693 }
11694 }
11695
11696 #[test]
11697 fn xml_error_display_covers_the_non_css_variants() {
11698 let variants = vec![
11699 XmlError::NoParserAvailable,
11700 XmlError::InvalidXmlPrefixUri(pos()),
11701 XmlError::UnexpectedXmlUri(pos()),
11702 XmlError::UnexpectedXmlnsUri(pos()),
11703 XmlError::InvalidElementNamePrefix(pos()),
11704 XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
11705 ns: AzString::from(""),
11706 pos: pos(),
11707 }),
11708 XmlError::UnknownNamespace(UnknownNamespaceError {
11709 ns: AzString::from("\u{1F600}"),
11710 pos: pos(),
11711 }),
11712 XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
11713 expected: AzString::from("a"),
11714 actual: AzString::from("b"),
11715 pos: pos(),
11716 }),
11717 XmlError::UnexpectedEntityCloseTag(pos()),
11718 XmlError::UnknownEntityReference(UnknownEntityReferenceError {
11719 entity: AzString::from("x"),
11720 pos: pos(),
11721 }),
11722 XmlError::MalformedEntityReference(pos()),
11723 XmlError::EntityReferenceLoop(pos()),
11724 XmlError::InvalidAttributeValue(pos()),
11725 XmlError::DuplicatedAttribute(DuplicatedAttributeError {
11726 attribute: AzString::from("id"),
11727 pos: pos(),
11728 }),
11729 XmlError::NoRootNode,
11730 XmlError::SizeLimit,
11731 XmlError::DtdDetected,
11732 XmlError::MalformedHierarchy(MalformedHierarchyError {
11733 expected: AzString::from("app"),
11734 got: AzString::from("p"),
11735 }),
11736 XmlError::ParserError(XmlParseError::UnknownToken(pos())),
11737 XmlError::UnclosedRootNode,
11738 XmlError::UnexpectedDeclaration(pos()),
11739 XmlError::NodesLimitReached,
11740 XmlError::AttributesLimitReached,
11741 XmlError::NamespacesLimitReached,
11742 XmlError::InvalidName(pos()),
11743 XmlError::NonXmlChar(pos()),
11744 XmlError::InvalidChar(pos()),
11745 XmlError::InvalidChar2(pos()),
11746 XmlError::InvalidString(pos()),
11747 XmlError::InvalidExternalID(pos()),
11748 XmlError::InvalidComment(pos()),
11749 XmlError::InvalidCharacterData(pos()),
11750 XmlError::UnknownToken(pos()),
11751 XmlError::UnexpectedEndOfStream,
11752 ];
11753 for v in &variants {
11754 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11755 }
11756 }
11757
11758 #[test]
11759 fn component_and_render_and_compile_error_display() {
11760 let unknown = ComponentError::UnknownComponent(AzString::from("\u{1F600}"));
11761 assert!(format!("{unknown}").contains("Unknown component"));
11762
11763 let useless = ComponentError::UselessFunctionArgument(UselessFunctionArgumentError {
11764 component_name: AzString::from("c"),
11765 argument_name: AzString::from("a"),
11766 valid_args: Vec::<AzString>::new().into(),
11767 });
11768 assert!(!format!("{useless}").is_empty());
11769
11770 let render: RenderDomError = unknown.clone().into();
11771 assert!(!format!("{render}").is_empty());
11772
11773 let compile: CompileError = render.clone().into();
11774 assert!(!format!("{compile}").is_empty());
11775
11776 let dom_xml: DomXmlParseError = render.into();
11777 assert!(!format!("{dom_xml}").is_empty());
11778 let compile2: CompileError = dom_xml.into();
11779 assert!(!format!("{compile2}").is_empty());
11780 }
11781
11782 #[test]
11783 fn dom_xml_parse_error_display_covers_the_non_css_variants() {
11784 let variants = vec![
11785 DomXmlParseError::NoHtmlNode,
11786 DomXmlParseError::MultipleHtmlRootNodes,
11787 DomXmlParseError::NoBodyInHtml,
11788 DomXmlParseError::MultipleBodyNodes,
11789 DomXmlParseError::Xml(XmlError::NoRootNode),
11790 DomXmlParseError::MalformedHierarchy(MalformedHierarchyError {
11791 expected: AzString::from("app"),
11792 got: AzString::from("p"),
11793 }),
11794 DomXmlParseError::RenderDom(RenderDomError::Component(
11795 ComponentError::UnknownComponent(AzString::from("x")),
11796 )),
11797 DomXmlParseError::Component(ComponentParseError::NotAComponent),
11798 ];
11799 for v in &variants {
11800 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11801 }
11802 }
11803
11804 #[test]
11805 fn component_parse_error_display_covers_the_non_css_variants() {
11806 let variants = vec![
11807 ComponentParseError::NotAComponent,
11808 ComponentParseError::UnnamedComponent,
11809 ComponentParseError::MissingName(usize::MAX),
11810 ComponentParseError::MissingType(MissingTypeError {
11811 arg_pos: 0,
11812 arg_name: AzString::from(""),
11813 }),
11814 ComponentParseError::WhiteSpaceInComponentName(WhiteSpaceInComponentNameError {
11815 arg_pos: usize::MAX,
11816 arg_name: AzString::from("a b"),
11817 }),
11818 ComponentParseError::WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError {
11819 arg_pos: 0,
11820 arg_name: AzString::from("a"),
11821 arg_type: AzString::from("b c"),
11822 }),
11823 ];
11824 for v in &variants {
11825 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11826 }
11827 }
11828
11829 #[cfg(feature = "serde-json")]
11834 #[test]
11835 fn data_model_to_json_round_trips() {
11836 let m = model_with_text();
11837 let json = m.to_json().expect("serializes");
11838 let back = ComponentDataModel::from_json(&json).expect("deserializes");
11839 assert_eq!(back.name.as_str(), m.name.as_str());
11840 assert_eq!(back.fields.as_ref().len(), m.fields.as_ref().len());
11841 assert_eq!(back.get_default_string("text").map(AzString::as_str), Some("hi"));
11842 }
11843
11844 #[cfg(feature = "serde-json")]
11845 #[test]
11846 fn data_model_from_json_rejects_garbage_without_panicking() {
11847 for s in [
11848 "",
11849 " ",
11850 "\t\n",
11851 "not json",
11852 "{",
11853 "[]",
11854 "null",
11855 "0",
11856 "-0",
11857 "9223372036854775807",
11858 "NaN",
11859 "\u{1F600}",
11860 ] {
11861 assert!(
11862 ComponentDataModel::from_json(s).is_err(),
11863 "{s:?} is not a data model"
11864 );
11865 }
11866 }
11867
11868 #[cfg(feature = "serde-json")]
11869 #[test]
11870 fn data_model_from_json_deeply_nested_input_does_not_stack_overflow() {
11871 let bomb = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
11872 assert!(
11873 ComponentDataModel::from_json(&bomb).is_err(),
11874 "serde_json must reject the nesting bomb, not crash"
11875 );
11876 }
11877
11878 #[cfg(feature = "serde-json")]
11879 #[test]
11880 fn data_model_to_json_on_an_empty_model() {
11881 let m = dm("Empty", Vec::new());
11882 let json = m.to_json().expect("serializes");
11883 assert!(json.contains("\"fields\""), "got {json}");
11884 assert!(ComponentDataModel::from_json(&json).is_ok());
11885 }
11886}