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> {
2254 #[derive(Deserialize)]
2255 struct Helper {
2256 #[serde(default)]
2257 name: alloc::string::String,
2258 #[serde(default)]
2259 description: alloc::string::String,
2260 #[serde(default)]
2261 fields: alloc::vec::Vec<ComponentDataField>,
2262 }
2263
2264 let h = Helper::deserialize(deserializer)?;
2265 Ok(ComponentDataModel {
2266 name: AzString::from(h.name.as_str()),
2267 description: AzString::from(h.description.as_str()),
2268 fields: ComponentDataFieldVec::from_vec(h.fields),
2269 })
2270 }
2271 }
2272}
2273
2274#[cfg(feature = "serde-json")]
2276pub use serde_impl::*;
2277
2278#[cfg(feature = "serde-json")]
2279impl ComponentDataModel {
2280 pub fn to_json(&self) -> Result<alloc::string::String, alloc::string::String> {
2282 serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{}", e))
2283 }
2284
2285 pub fn from_json(json: &str) -> Result<Self, alloc::string::String> {
2287 serde_json::from_str(json).map_err(|e| alloc::format!("{}", e))
2288 }
2289}
2290
2291#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2293#[repr(C)]
2294#[derive(Default)]
2295pub enum ComponentSource {
2296 Builtin,
2298 Compiled,
2300 #[default]
2302 UserDefined,
2303}
2304
2305
2306impl ComponentSource {
2307 #[must_use] pub fn create() -> Self {
2308 Self::default()
2309 }
2310}
2311
2312#[allow(missing_copy_implementations)]
2317#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2318#[repr(C)]
2319pub enum CompileTarget {
2320 Rust,
2321 C,
2322 Cpp,
2323 Python,
2324}
2325
2326impl_result!(
2327 StyledDom,
2328 RenderDomError,
2329 ResultStyledDomRenderDomError,
2330 copy = false,
2331 [Debug, Clone, PartialEq]
2332);
2333
2334impl_result!(
2335 AzString,
2336 CompileError,
2337 ResultStringCompileError,
2338 copy = false,
2339 [Debug, Clone, PartialEq]
2340);
2341
2342pub type ComponentRenderFn =
2349 fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError;
2350
2351pub type ComponentCompileFn = fn(
2353 &ComponentDef,
2354 &CompileTarget,
2355 &ComponentDataModel,
2356 indent: usize,
2357) -> ResultStringCompileError;
2358
2359pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
2362
2363#[repr(C)]
2371pub struct RegisterComponentFn {
2372 pub cb: RegisterComponentFnType,
2373 pub ctx: crate::refany::OptionRefAny,
2376}
2377
2378impl_callback!(RegisterComponentFn, RegisterComponentFnType);
2379
2380pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
2383
2384#[repr(C)]
2392pub struct RegisterComponentLibraryFn {
2393 pub cb: RegisterComponentLibraryFnType,
2394 pub ctx: crate::refany::OptionRefAny,
2397}
2398
2399impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
2400
2401#[derive(Clone)]
2405#[repr(C)]
2406pub struct ComponentDef {
2407 pub id: ComponentId,
2409 pub display_name: AzString,
2411 pub description: AzString,
2413 pub css: AzString,
2415 pub source: ComponentSource,
2417 pub data_model: ComponentDataModel,
2423 pub render_fn: ComponentRenderFn,
2425 pub compile_fn: ComponentCompileFn,
2427 pub render_fn_source: OptionString,
2429 pub compile_fn_source: OptionString,
2431}
2432
2433impl fmt::Debug for ComponentDef {
2434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2435 f.debug_struct("ComponentDef")
2436 .field("id", &self.id)
2437 .field("display_name", &self.display_name)
2438 .field("source", &self.source)
2439 .field("data_model", &self.data_model.name)
2440 .finish_non_exhaustive()
2441 }
2442}
2443
2444impl_vec!(
2445 ComponentDef,
2446 ComponentDefVec,
2447 ComponentDefVecDestructor,
2448 ComponentDefVecDestructorType,
2449 ComponentDefVecSlice,
2450 OptionComponentDef
2451);
2452impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
2453impl_vec_debug!(ComponentDef, ComponentDefVec);
2454impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
2455impl_vec_mut!(ComponentDef, ComponentDefVec);
2456
2457#[derive(Debug, Clone)]
2459#[repr(C)]
2460pub struct ComponentLibrary {
2461 pub name: AzString,
2463 pub version: AzString,
2465 pub description: AzString,
2467 pub components: ComponentDefVec,
2469 pub exportable: bool,
2471 pub modifiable: bool,
2474 pub data_models: ComponentDataModelVec,
2477 pub enum_models: ComponentEnumModelVec,
2480}
2481
2482impl_vec!(
2483 ComponentLibrary,
2484 ComponentLibraryVec,
2485 ComponentLibraryVecDestructor,
2486 ComponentLibraryVecDestructorType,
2487 ComponentLibraryVecSlice,
2488 OptionComponentLibrary
2489);
2490impl_option!(
2491 ComponentLibrary,
2492 OptionComponentLibrary,
2493 copy = false,
2494 [Debug, Clone]
2495);
2496impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
2497impl_vec_clone!(
2498 ComponentLibrary,
2499 ComponentLibraryVec,
2500 ComponentLibraryVecDestructor
2501);
2502impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
2503
2504#[derive(Debug, Clone)]
2506#[repr(C)]
2507pub struct ComponentMap {
2508 pub libraries: ComponentLibraryVec,
2510}
2511
2512impl ComponentMap {
2513 #[must_use] pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
2515 self.libraries
2516 .iter()
2517 .find(|lib| lib.name.as_str() == collection)
2518 .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
2519 }
2520
2521 #[must_use] pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
2523 self.get("builtin", name)
2524 }
2525
2526 #[must_use] pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
2528 if let Some((collection, name)) = qualified.split_once(':') {
2529 self.get(collection, name)
2530 } else {
2531 self.get_unqualified(qualified)
2532 }
2533 }
2534
2535 #[must_use] pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
2537 self.libraries.iter().filter(|lib| lib.exportable).collect()
2538 }
2539
2540 #[must_use] pub fn all_components(&self) -> Vec<&ComponentDef> {
2542 self.libraries
2543 .iter()
2544 .flat_map(|lib| lib.components.iter())
2545 .collect()
2546 }
2547}
2548
2549macro_rules! html_tag_node_types {
2561 ($($tag:literal => $variant:ident),* $(,)?) => {
2562 #[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
2565 match tag {
2566 "img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
2571 crate::resources::ImageRef::null_image(
2572 0,
2573 0,
2574 crate::resources::RawImageFormat::RGBA8,
2575 alloc::vec::Vec::new(),
2576 ),
2577 )),
2578 $($tag => NodeType::$variant,)*
2579 _ => NodeType::Div,
2580 }
2581 }
2582
2583 fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
2586 match tag {
2587 "img" | "image" => NodeTypeTag::Img,
2590 "icon" => NodeTypeTag::Icon,
2591 $($tag => NodeTypeTag::$variant,)*
2592 _ => NodeTypeTag::Div,
2593 }
2594 }
2595 };
2596}
2597
2598html_tag_node_types! {
2599 "html" => Html,
2601 "head" => Head,
2602 "title" => Title,
2603 "body" => Body,
2604 "div" => Div,
2606 "header" => Header,
2607 "footer" => Footer,
2608 "section" => Section,
2609 "article" => Article,
2610 "aside" => Aside,
2611 "nav" => Nav,
2612 "main" => Main,
2613 "figure" => Figure,
2614 "figcaption" => FigCaption,
2615 "address" => Address,
2616 "details" => Details,
2617 "summary" => Summary,
2618 "dialog" => Dialog,
2619 "h1" => H1,
2621 "h2" => H2,
2622 "h3" => H3,
2623 "h4" => H4,
2624 "h5" => H5,
2625 "h6" => H6,
2626 "p" => P,
2628 "span" => Span,
2629 "pre" => Pre,
2630 "code" => Code,
2631 "blockquote" => BlockQuote,
2632 "br" => Br,
2633 "hr" => Hr,
2634 "ul" => Ul,
2636 "ol" => Ol,
2637 "li" => Li,
2638 "dl" => Dl,
2639 "dt" => Dt,
2640 "dd" => Dd,
2641 "menu" => Menu,
2642 "menuitem" => MenuItem,
2643 "dir" => Dir,
2644 "table" => Table,
2646 "caption" => Caption,
2647 "thead" => THead,
2648 "tbody" => TBody,
2649 "tfoot" => TFoot,
2650 "tr" => Tr,
2651 "th" => Th,
2652 "td" => Td,
2653 "colgroup" => ColGroup,
2654 "col" => Col,
2655 "form" => Form,
2657 "fieldset" => FieldSet,
2658 "legend" => Legend,
2659 "label" => Label,
2660 "input" => Input,
2661 "button" => Button,
2662 "select" => Select,
2663 "optgroup" => OptGroup,
2664 "option" => SelectOption,
2665 "textarea" => TextArea,
2666 "output" => Output,
2667 "progress" => Progress,
2668 "meter" => Meter,
2669 "datalist" => DataList,
2670 "a" => A,
2672 "strong" => Strong,
2673 "em" => Em,
2674 "b" => B,
2675 "i" => I,
2676 "u" => U,
2677 "s" => S,
2678 "small" => Small,
2679 "mark" => Mark,
2680 "del" => Del,
2681 "ins" => Ins,
2682 "samp" => Samp,
2683 "kbd" => Kbd,
2684 "var" => Var,
2685 "cite" => Cite,
2686 "dfn" => Dfn,
2687 "abbr" => Abbr,
2688 "acronym" => Acronym,
2689 "q" => Q,
2690 "time" => Time,
2691 "sub" => Sub,
2692 "sup" => Sup,
2693 "big" => Big,
2694 "bdo" => Bdo,
2695 "bdi" => Bdi,
2696 "wbr" => Wbr,
2697 "ruby" => Ruby,
2698 "rt" => Rt,
2699 "rtc" => Rtc,
2700 "rp" => Rp,
2701 "data" => Data,
2702 "canvas" => Canvas,
2704 "object" => Object,
2705 "param" => Param,
2706 "embed" => Embed,
2707 "audio" => Audio,
2708 "video" => Video,
2709 "source" => Source,
2710 "track" => Track,
2711 "map" => Map,
2712 "area" => Area,
2713 "svg" => Svg,
2715 "g" => SvgG,
2716 "defs" => SvgDefs,
2717 "symbol" => SvgSymbol,
2718 "use" => SvgUse,
2719 "switch" => SvgSwitch,
2720 "path" => SvgPath,
2721 "circle" => SvgCircle,
2722 "rect" => SvgRect,
2723 "ellipse" => SvgEllipse,
2724 "line" => SvgLine,
2725 "polygon" => SvgPolygon,
2726 "polyline" => SvgPolyline,
2727 "tspan" => SvgTspan,
2728 "textpath" => SvgTextPath,
2729 "lineargradient" => SvgLinearGradient,
2730 "radialgradient" => SvgRadialGradient,
2731 "stop" => SvgStop,
2732 "pattern" => SvgPattern,
2733 "clippath" => SvgClipPathElement,
2734 "mask" => SvgMask,
2735 "filter" => SvgFilter,
2736 "feblend" => SvgFeBlend,
2737 "fecolormatrix" => SvgFeColorMatrix,
2738 "fecomponenttransfer" => SvgFeComponentTransfer,
2739 "fecomposite" => SvgFeComposite,
2740 "feconvolvematrix" => SvgFeConvolveMatrix,
2741 "fediffuselighting" => SvgFeDiffuseLighting,
2742 "fedisplacementmap" => SvgFeDisplacementMap,
2743 "fedistantlight" => SvgFeDistantLight,
2744 "fedropshadow" => SvgFeDropShadow,
2745 "feflood" => SvgFeFlood,
2746 "fefuncr" => SvgFeFuncR,
2747 "fefuncg" => SvgFeFuncG,
2748 "fefuncb" => SvgFeFuncB,
2749 "fefunca" => SvgFeFuncA,
2750 "fegaussianblur" => SvgFeGaussianBlur,
2751 "feimage" => SvgFeImage,
2752 "femerge" => SvgFeMerge,
2753 "femergenode" => SvgFeMergeNode,
2754 "femorphology" => SvgFeMorphology,
2755 "feoffset" => SvgFeOffset,
2756 "fepointlight" => SvgFePointLight,
2757 "fespecularlighting" => SvgFeSpecularLighting,
2758 "fespotlight" => SvgFeSpotLight,
2759 "fetile" => SvgFeTile,
2760 "feturbulence" => SvgFeTurbulence,
2761 "foreignobject" => SvgForeignObject,
2762 "desc" => SvgDesc,
2763 "view" => SvgView,
2764 "animate" => SvgAnimate,
2765 "animatemotion" => SvgAnimateMotion,
2766 "animatetransform" => SvgAnimateTransform,
2767 "set" => SvgSet,
2768 "mpath" => SvgMpath,
2769 "meta" => Meta,
2771 "link" => Link,
2772 "script" => Script,
2773 "style" => Style,
2774 "base" => Base,
2775}
2776
2777fn builtin_render_fn(
2780 def: &ComponentDef,
2781 data: &ComponentDataModel,
2782 _component_map: &ComponentMap,
2783) -> ResultStyledDomRenderDomError {
2784 let node_type = tag_to_node_type(def.id.name.as_str());
2785 let mut dom = Dom::create_node(node_type);
2786 if let Some(text_str) = data.get_default_string("text") {
2787 let prepared = prepare_string(text_str);
2788 if !prepared.is_empty() {
2789 dom = dom.with_children(alloc::vec![Dom::create_text(prepared)].into());
2790 }
2791 }
2792 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2793 r.into()
2794}
2795
2796fn builtin_compile_fn(
2799 def: &ComponentDef,
2800 target: &CompileTarget,
2801 data: &ComponentDataModel,
2802 indent: usize,
2803) -> ResultStringCompileError {
2804 let node_type = tag_to_node_type(def.id.name.as_str());
2805 let type_name = format!("{node_type:?}"); let text = data.get_default_string("text");
2807
2808 let r: Result<AzString, CompileError> = match target {
2809 CompileTarget::Rust => {
2810 text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
2811 "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text(\"{}\")])",
2812 type_name,
2813 text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
2814 ).into()))
2815 }
2816 CompileTarget::C => {
2817 text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
2818 "AzDom_createText(AZ_STR(\"{}\"))",
2819 text_str
2820 .as_str()
2821 .replace('\\', "\\\\")
2822 .replace('"', "\\\"")
2823 )
2824 .into()))
2825 }
2826 CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
2827 CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
2828 };
2829 r.into()
2830}
2831
2832fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn fmt::Display) {
2834 use crate::dom::{Dom, NodeType};
2835 let text = alloc::format!("{field_name}: {value}");
2836 children.push(
2837 Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(text)].into()),
2838 );
2839}
2840
2841#[allow(clippy::too_many_lines)] #[must_use] pub fn user_defined_render_fn(
2854 def: &ComponentDef,
2855 data: &ComponentDataModel,
2856 component_map: &ComponentMap,
2857) -> ResultStyledDomRenderDomError {
2858 use crate::dom::{Dom, NodeType};
2859 use azul_css::css::Css;
2860
2861 let mut children: Vec<Dom> = Vec::new();
2862
2863 for field in data.fields.as_ref() {
2864 let field_name = field.name.as_str();
2865
2866 match &field.default_value {
2868 OptionComponentDefaultValue::None => {
2869 }
2871 OptionComponentDefaultValue::Some(default_val) => {
2872 match default_val {
2873 ComponentDefaultValue::String(s) => {
2874 let text = s.as_str().trim();
2875 if !text.is_empty() {
2876 let label_dom = Dom::create_node(NodeType::Div).with_children(
2877 alloc::vec![Dom::create_text(text.to_string())].into(),
2878 );
2879 children.push(label_dom);
2880 }
2881 }
2882 ComponentDefaultValue::Bool(v) => {
2883 push_scalar_field(&mut children, field_name, v);
2884 }
2885 ComponentDefaultValue::I32(v) => {
2886 push_scalar_field(&mut children, field_name, v);
2887 }
2888 ComponentDefaultValue::I64(v) => {
2889 push_scalar_field(&mut children, field_name, v);
2890 }
2891 ComponentDefaultValue::U32(v) => {
2892 push_scalar_field(&mut children, field_name, v);
2893 }
2894 ComponentDefaultValue::U64(v) => {
2895 push_scalar_field(&mut children, field_name, v);
2896 }
2897 ComponentDefaultValue::Usize(v) => {
2898 push_scalar_field(&mut children, field_name, v);
2899 }
2900 ComponentDefaultValue::F32(v) => {
2901 push_scalar_field(&mut children, field_name, v);
2902 }
2903 ComponentDefaultValue::F64(v) => {
2904 push_scalar_field(&mut children, field_name, v);
2905 }
2906 ComponentDefaultValue::ColorU(c) => {
2907 let text = alloc::format!(
2908 "{}: #{:02x}{:02x}{:02x}{:02x}",
2909 field_name,
2910 c.r,
2911 c.g,
2912 c.b,
2913 c.a
2914 );
2915 children.push(
2916 Dom::create_node(NodeType::Div)
2917 .with_children(alloc::vec![Dom::create_text(text)].into()),
2918 );
2919 }
2920 ComponentDefaultValue::ComponentInstance(ci) => {
2921 if let Some(sub_comp) =
2923 component_map.get(ci.library.as_str(), ci.component.as_str())
2924 {
2925 let sub_data = sub_comp.data_model.clone();
2926 match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
2927 ResultStyledDomRenderDomError::Ok(_styled_dom) => {
2928 let text = alloc::format!(
2931 "[{}:{}]",
2932 ci.library.as_str(),
2933 ci.component.as_str()
2934 );
2935 children.push(
2936 Dom::create_node(NodeType::Div).with_children(
2937 alloc::vec![Dom::create_text(text)].into(),
2938 ),
2939 );
2940 }
2941 ResultStyledDomRenderDomError::Err(_) => {
2942 let text = alloc::format!(
2944 "[Error rendering {}:{}]",
2945 ci.library.as_str(),
2946 ci.component.as_str()
2947 );
2948 children.push(
2949 Dom::create_node(NodeType::Div).with_children(
2950 alloc::vec![Dom::create_text(text)].into(),
2951 ),
2952 );
2953 }
2954 }
2955 } else {
2956 let text = alloc::format!(
2957 "[Unknown component {}:{}]",
2958 ci.library.as_str(),
2959 ci.component.as_str()
2960 );
2961 children.push(
2962 Dom::create_node(NodeType::Div)
2963 .with_children(alloc::vec![Dom::create_text(text)].into()),
2964 );
2965 }
2966 }
2967 ComponentDefaultValue::CallbackFnPointer(name) => {
2968 let text = alloc::format!("{}: fn({})", field_name, name.as_str());
2970 children.push(
2971 Dom::create_node(NodeType::Div)
2972 .with_children(alloc::vec![Dom::create_text(text)].into()),
2973 );
2974 }
2975 ComponentDefaultValue::Json(json_str) => {
2976 let text = alloc::format!("{}: {}", field_name, json_str.as_str());
2977 children.push(
2978 Dom::create_node(NodeType::Div)
2979 .with_children(alloc::vec![Dom::create_text(text)].into()),
2980 );
2981 }
2982 ComponentDefaultValue::None => {
2983 }
2985 }
2986 }
2987 }
2988 }
2989
2990 let mut wrapper = Dom::create_node(NodeType::Div);
2991 if !children.is_empty() {
2992 wrapper = wrapper.with_children(children.into());
2993 }
2994
2995 let css = if def.css.as_str().is_empty() {
2997 Css::empty()
2998 } else {
2999 Css::from_string(def.css.clone())
3000 };
3001
3002 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
3003 r.into()
3004}
3005
3006#[allow(clippy::too_many_lines)] #[must_use] pub fn user_defined_compile_fn(
3016 def: &ComponentDef,
3017 target: &CompileTarget,
3018 data: &ComponentDataModel,
3019 indent: usize,
3020) -> ResultStringCompileError {
3021 let tag = def.id.name.as_str();
3022 let indent_str = " ".repeat(indent * 4);
3023 let inner_indent = " ".repeat((indent + 1) * 4);
3024
3025 let r: Result<AzString, CompileError> = match target {
3026 CompileTarget::Rust => {
3027 let mut lines = Vec::new();
3028 lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3029 lines.push(alloc::format!(
3030 "{indent_str}let mut children: Vec<Dom> = Vec::new();"
3031 ));
3032
3033 for field in data.fields.as_ref() {
3034 let fname = field.name.as_str();
3035 match &field.default_value {
3036 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3037 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3038 lines.push(alloc::format!(
3039 "{inner_indent}children.push(Dom::create_text(\"{escaped}\"));"
3040 ));
3041 }
3042 OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
3043 lines.push(alloc::format!(
3044 "{inner_indent}children.push(Dom::create_text(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
3045 ));
3046 }
3047 OptionComponentDefaultValue::Some(
3048 ComponentDefaultValue::ComponentInstance(ci),
3049 ) => {
3050 let fn_name =
3051 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3052 lines.push(alloc::format!(
3053 "{}children.push({}()); // sub-component {}:{}",
3054 inner_indent,
3055 fn_name,
3056 ci.library.as_str(),
3057 ci.component.as_str()
3058 ));
3059 }
3060 _ => {
3061 lines.push(alloc::format!(
3063 "{}// field '{}': {:?}",
3064 inner_indent,
3065 fname,
3066 field.field_type
3067 ));
3068 }
3069 }
3070 }
3071
3072 lines.push(alloc::format!(
3073 "{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
3074 ));
3075 Ok(lines.join("\n").into())
3076 }
3077 CompileTarget::C => {
3078 let mut lines = Vec::new();
3079 lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
3080 lines.push(alloc::format!(
3081 "{indent_str}AzDom root = AzDom_createDiv();"
3082 ));
3083
3084 for field in data.fields.as_ref() {
3085 let fname = field.name.as_str();
3086 match &field.default_value {
3087 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3088 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3089 lines.push(alloc::format!(
3090 "{inner_indent}AzDom_addChild(&root, AzDom_createText(AZ_STR(\"{escaped}\")));"
3091 ));
3092 }
3093 OptionComponentDefaultValue::Some(
3094 ComponentDefaultValue::ComponentInstance(ci),
3095 ) => {
3096 let fn_name =
3097 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3098 lines.push(alloc::format!(
3099 "{inner_indent}AzDom_addChild(&root, {fn_name}());"
3100 ));
3101 }
3102 _ => {
3103 lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
3104 }
3105 }
3106 }
3107
3108 lines.push(alloc::format!("{indent_str}return root;"));
3109 Ok(lines.join("\n").into())
3110 }
3111 CompileTarget::Cpp => {
3112 let mut lines = Vec::new();
3113 lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3114 lines.push(alloc::format!(
3115 "{indent_str}auto root = Dom::create_div();"
3116 ));
3117
3118 for field in data.fields.as_ref() {
3119 let fname = field.name.as_str();
3120 match &field.default_value {
3121 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3122 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3123 lines.push(alloc::format!(
3124 "{inner_indent}root.add_child(Dom::create_text(String(\"{escaped}\")));"
3125 ));
3126 }
3127 OptionComponentDefaultValue::Some(
3128 ComponentDefaultValue::ComponentInstance(ci),
3129 ) => {
3130 let fn_name =
3131 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3132 lines.push(alloc::format!(
3133 "{inner_indent}root.add_child({fn_name}());"
3134 ));
3135 }
3136 _ => {
3137 lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
3138 }
3139 }
3140 }
3141
3142 lines.push(alloc::format!("{indent_str}return root;"));
3143 Ok(lines.join("\n").into())
3144 }
3145 CompileTarget::Python => {
3146 let mut lines = Vec::new();
3147 lines.push(alloc::format!("{indent_str}# Component: {tag}"));
3148 lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
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
3155 .as_str()
3156 .replace('\\', "\\\\")
3157 .replace('"', "\\\"")
3158 .replace('\'', "\\'");
3159 lines.push(alloc::format!(
3160 "{inner_indent}root = root.with_child(Dom.create_text(\"{escaped}\"))"
3161 ));
3162 }
3163 OptionComponentDefaultValue::Some(
3164 ComponentDefaultValue::ComponentInstance(ci),
3165 ) => {
3166 let fn_name =
3167 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3168 lines.push(alloc::format!(
3169 "{inner_indent}root = root.with_child({fn_name}())"
3170 ));
3171 }
3172 _ => {
3173 lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
3174 }
3175 }
3176 }
3177
3178 lines.push(alloc::format!("{indent_str}return root"));
3179 Ok(lines.join("\n").into())
3180 }
3181 };
3182 r.into()
3183}
3184
3185fn builtin_component_def(
3197 tag: &str,
3198 display_name: &str,
3199 default_text: Option<&str>,
3200 css: &str,
3201) -> ComponentDef {
3202 let mut fields = builtin_data_model(tag);
3203 if let Some(text) = default_text {
3205 fields.push(data_field(
3206 "text",
3207 ComponentFieldType::String,
3208 Some(ComponentDefaultValue::String(AzString::from(text))),
3209 "Text content of the element",
3210 ));
3211 }
3212 let model_name = format!("{display_name}Data");
3213 ComponentDef {
3214 id: ComponentId::builtin(tag),
3215 display_name: AzString::from(display_name),
3216 description: AzString::from(format!("HTML <{tag}> element").as_str()),
3217 css: AzString::from(css),
3218 source: ComponentSource::Builtin,
3219 data_model: ComponentDataModel {
3220 name: AzString::from(model_name.as_str()),
3221 description: AzString::from(format!("Data model for <{tag}>").as_str()),
3222 fields: fields.into(),
3223 },
3224 render_fn: builtin_render_fn,
3225 compile_fn: builtin_compile_fn,
3226 render_fn_source: None.into(),
3227 compile_fn_source: None.into(),
3228 }
3229}
3230
3231fn data_field(
3233 name: &str,
3234 ft: ComponentFieldType,
3235 default: Option<ComponentDefaultValue>,
3236 description: &str,
3237) -> ComponentDataField {
3238 let required = default.is_none();
3239 ComponentDataField {
3240 name: AzString::from(name),
3241 field_type: ft,
3242 default_value: default.map_or_else(|| OptionComponentDefaultValue::None, OptionComponentDefaultValue::Some),
3243 required,
3244 description: AzString::from(description),
3245 }
3246}
3247
3248#[allow(clippy::too_many_lines)] fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
3255 use ComponentDefaultValue as D;
3256 use ComponentFieldType::{String, Bool, I32};
3257 match tag {
3258 "a" => alloc::vec![
3259 data_field(
3260 "href",
3261 String,
3262 Some(D::String(AzString::from_const_str(""))),
3263 "URL the link points to"
3264 ),
3265 data_field(
3266 "target",
3267 String,
3268 Some(D::String(AzString::from_const_str(""))),
3269 "Where to open the linked document (_blank, _self, _parent, _top)"
3270 ),
3271 data_field(
3272 "rel",
3273 String,
3274 Some(D::String(AzString::from_const_str(""))),
3275 "Relationship between current and linked document"
3276 ),
3277 ],
3278 "img" | "image" => alloc::vec![
3279 data_field("src", String, None, "URL of the image"),
3280 data_field(
3281 "alt",
3282 String,
3283 Some(D::String(AzString::from_const_str(""))),
3284 "Alternative text for the image"
3285 ),
3286 data_field(
3287 "width",
3288 String,
3289 Some(D::String(AzString::from_const_str(""))),
3290 "Width of the image"
3291 ),
3292 data_field(
3293 "height",
3294 String,
3295 Some(D::String(AzString::from_const_str(""))),
3296 "Height of the image"
3297 ),
3298 ],
3299 "form" => alloc::vec![
3300 data_field(
3301 "action",
3302 String,
3303 Some(D::String(AzString::from_const_str(""))),
3304 "URL where form data is submitted"
3305 ),
3306 data_field(
3307 "method",
3308 String,
3309 Some(D::String(AzString::from_const_str("GET"))),
3310 "HTTP method for form submission (GET or POST)"
3311 ),
3312 ],
3313 "label" => alloc::vec![data_field(
3314 "for",
3315 String,
3316 Some(D::String(AzString::from_const_str(""))),
3317 "ID of the form element this label is for"
3318 ),],
3319 "button" => alloc::vec![
3320 data_field(
3321 "type",
3322 String,
3323 Some(D::String(AzString::from_const_str("button"))),
3324 "Button type (button, submit, reset)"
3325 ),
3326 data_field(
3327 "disabled",
3328 Bool,
3329 Some(D::Bool(false)),
3330 "Whether the button is disabled"
3331 ),
3332 ],
3333 "td" | "th" => alloc::vec![
3334 data_field(
3335 "colspan",
3336 I32,
3337 Some(D::I32(1)),
3338 "Number of columns the cell spans"
3339 ),
3340 data_field(
3341 "rowspan",
3342 I32,
3343 Some(D::I32(1)),
3344 "Number of rows the cell spans"
3345 ),
3346 ],
3347 "icon" => alloc::vec![data_field(
3348 "name",
3349 String,
3350 Some(D::String(AzString::from_const_str(""))),
3351 "Icon name"
3352 ),],
3353 "ol" => alloc::vec![
3354 data_field(
3355 "start",
3356 I32,
3357 Some(D::I32(1)),
3358 "Start value for the ordered list"
3359 ),
3360 data_field(
3361 "type",
3362 String,
3363 Some(D::String(AzString::from_const_str("1"))),
3364 "Numbering type (1, A, a, I, i)"
3365 ),
3366 ],
3367 "input" => alloc::vec![
3369 data_field(
3370 "type",
3371 String,
3372 Some(D::String(AzString::from_const_str("text"))),
3373 "Input type (text, password, email, number, checkbox, radio, etc.)"
3374 ),
3375 data_field(
3376 "name",
3377 String,
3378 Some(D::String(AzString::from_const_str(""))),
3379 "Name of the input for form submission"
3380 ),
3381 data_field(
3382 "value",
3383 String,
3384 Some(D::String(AzString::from_const_str(""))),
3385 "Current value of the input"
3386 ),
3387 data_field(
3388 "placeholder",
3389 String,
3390 Some(D::String(AzString::from_const_str(""))),
3391 "Placeholder text"
3392 ),
3393 data_field(
3394 "disabled",
3395 Bool,
3396 Some(D::Bool(false)),
3397 "Whether the input is disabled"
3398 ),
3399 data_field(
3400 "required",
3401 Bool,
3402 Some(D::Bool(false)),
3403 "Whether the input is required"
3404 ),
3405 data_field(
3406 "readonly",
3407 Bool,
3408 Some(D::Bool(false)),
3409 "Whether the input is read-only"
3410 ),
3411 data_field(
3412 "checked",
3413 Bool,
3414 Some(D::Bool(false)),
3415 "Whether the checkbox/radio is checked"
3416 ),
3417 data_field(
3418 "min",
3419 String,
3420 Some(D::String(AzString::from_const_str(""))),
3421 "Minimum value (for number, range, date)"
3422 ),
3423 data_field(
3424 "max",
3425 String,
3426 Some(D::String(AzString::from_const_str(""))),
3427 "Maximum value (for number, range, date)"
3428 ),
3429 data_field(
3430 "step",
3431 String,
3432 Some(D::String(AzString::from_const_str(""))),
3433 "Step increment (for number, range)"
3434 ),
3435 data_field(
3436 "pattern",
3437 String,
3438 Some(D::String(AzString::from_const_str(""))),
3439 "Regex pattern for validation"
3440 ),
3441 data_field(
3442 "maxlength",
3443 String,
3444 Some(D::String(AzString::from_const_str(""))),
3445 "Maximum number of characters"
3446 ),
3447 ],
3448 "select" => alloc::vec![
3449 data_field(
3450 "name",
3451 String,
3452 Some(D::String(AzString::from_const_str(""))),
3453 "Name for form submission"
3454 ),
3455 data_field(
3456 "multiple",
3457 Bool,
3458 Some(D::Bool(false)),
3459 "Whether multiple options can be selected"
3460 ),
3461 data_field(
3462 "disabled",
3463 Bool,
3464 Some(D::Bool(false)),
3465 "Whether the select is disabled"
3466 ),
3467 data_field(
3468 "required",
3469 Bool,
3470 Some(D::Bool(false)),
3471 "Whether selection is required"
3472 ),
3473 data_field(
3474 "size",
3475 String,
3476 Some(D::String(AzString::from_const_str(""))),
3477 "Number of visible options"
3478 ),
3479 ],
3480 "option" => alloc::vec![
3481 data_field(
3482 "value",
3483 String,
3484 Some(D::String(AzString::from_const_str(""))),
3485 "Value submitted with the form"
3486 ),
3487 data_field(
3488 "selected",
3489 Bool,
3490 Some(D::Bool(false)),
3491 "Whether this option is selected"
3492 ),
3493 data_field(
3494 "disabled",
3495 Bool,
3496 Some(D::Bool(false)),
3497 "Whether this option is disabled"
3498 ),
3499 ],
3500 "optgroup" => alloc::vec![
3501 data_field(
3502 "label",
3503 String,
3504 Some(D::String(AzString::from_const_str(""))),
3505 "Label for the option group"
3506 ),
3507 data_field(
3508 "disabled",
3509 Bool,
3510 Some(D::Bool(false)),
3511 "Whether the group is disabled"
3512 ),
3513 ],
3514 "textarea" => alloc::vec![
3515 data_field(
3516 "name",
3517 String,
3518 Some(D::String(AzString::from_const_str(""))),
3519 "Name for form submission"
3520 ),
3521 data_field(
3522 "placeholder",
3523 String,
3524 Some(D::String(AzString::from_const_str(""))),
3525 "Placeholder text"
3526 ),
3527 data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
3528 data_field(
3529 "cols",
3530 I32,
3531 Some(D::I32(20)),
3532 "Visible width in average character widths"
3533 ),
3534 data_field(
3535 "disabled",
3536 Bool,
3537 Some(D::Bool(false)),
3538 "Whether the textarea is disabled"
3539 ),
3540 data_field(
3541 "required",
3542 Bool,
3543 Some(D::Bool(false)),
3544 "Whether content is required"
3545 ),
3546 data_field(
3547 "readonly",
3548 Bool,
3549 Some(D::Bool(false)),
3550 "Whether the textarea is read-only"
3551 ),
3552 data_field(
3553 "maxlength",
3554 String,
3555 Some(D::String(AzString::from_const_str(""))),
3556 "Maximum number of characters"
3557 ),
3558 ],
3559 "fieldset" => alloc::vec![data_field(
3560 "disabled",
3561 Bool,
3562 Some(D::Bool(false)),
3563 "Whether all controls in the fieldset are disabled"
3564 ),],
3565 "output" => alloc::vec![
3566 data_field(
3567 "for",
3568 String,
3569 Some(D::String(AzString::from_const_str(""))),
3570 "IDs of elements that contributed to the output"
3571 ),
3572 data_field(
3573 "name",
3574 String,
3575 Some(D::String(AzString::from_const_str(""))),
3576 "Name for form submission"
3577 ),
3578 ],
3579 "progress" => alloc::vec![
3580 data_field(
3581 "value",
3582 String,
3583 Some(D::String(AzString::from_const_str(""))),
3584 "Current progress value"
3585 ),
3586 data_field(
3587 "max",
3588 String,
3589 Some(D::String(AzString::from_const_str("1"))),
3590 "Maximum value"
3591 ),
3592 ],
3593 "meter" => alloc::vec![
3594 data_field(
3595 "value",
3596 String,
3597 Some(D::String(AzString::from_const_str(""))),
3598 "Current value"
3599 ),
3600 data_field(
3601 "min",
3602 String,
3603 Some(D::String(AzString::from_const_str("0"))),
3604 "Minimum value"
3605 ),
3606 data_field(
3607 "max",
3608 String,
3609 Some(D::String(AzString::from_const_str("1"))),
3610 "Maximum value"
3611 ),
3612 data_field(
3613 "low",
3614 String,
3615 Some(D::String(AzString::from_const_str(""))),
3616 "Low threshold"
3617 ),
3618 data_field(
3619 "high",
3620 String,
3621 Some(D::String(AzString::from_const_str(""))),
3622 "High threshold"
3623 ),
3624 data_field(
3625 "optimum",
3626 String,
3627 Some(D::String(AzString::from_const_str(""))),
3628 "Optimum value"
3629 ),
3630 ],
3631 "details" => alloc::vec![data_field(
3633 "open",
3634 Bool,
3635 Some(D::Bool(false)),
3636 "Whether the details are visible"
3637 ),],
3638 "dialog" => alloc::vec![data_field(
3639 "open",
3640 Bool,
3641 Some(D::Bool(false)),
3642 "Whether the dialog is active and can be interacted with"
3643 ),],
3644 "audio" | "video" => alloc::vec![
3646 data_field(
3647 "src",
3648 String,
3649 Some(D::String(AzString::from_const_str(""))),
3650 "URL of the media resource"
3651 ),
3652 data_field(
3653 "controls",
3654 Bool,
3655 Some(D::Bool(false)),
3656 "Whether to show playback controls"
3657 ),
3658 data_field(
3659 "autoplay",
3660 Bool,
3661 Some(D::Bool(false)),
3662 "Whether to start playing automatically"
3663 ),
3664 data_field(
3665 "loop",
3666 Bool,
3667 Some(D::Bool(false)),
3668 "Whether to loop playback"
3669 ),
3670 data_field(
3671 "muted",
3672 Bool,
3673 Some(D::Bool(false)),
3674 "Whether audio is muted"
3675 ),
3676 data_field(
3677 "preload",
3678 String,
3679 Some(D::String(AzString::from_const_str("auto"))),
3680 "Preload hint (none, metadata, auto)"
3681 ),
3682 ],
3683 "source" => alloc::vec![
3684 data_field("src", String, None, "URL of the media resource"),
3685 data_field(
3686 "type",
3687 String,
3688 Some(D::String(AzString::from_const_str(""))),
3689 "MIME type of the resource"
3690 ),
3691 ],
3692 "track" => alloc::vec![
3693 data_field("src", String, None, "URL of the track file"),
3694 data_field(
3695 "kind",
3696 String,
3697 Some(D::String(AzString::from_const_str("subtitles"))),
3698 "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"
3699 ),
3700 data_field(
3701 "srclang",
3702 String,
3703 Some(D::String(AzString::from_const_str(""))),
3704 "Language of the track text"
3705 ),
3706 data_field(
3707 "label",
3708 String,
3709 Some(D::String(AzString::from_const_str(""))),
3710 "User-readable title for the track"
3711 ),
3712 data_field(
3713 "default",
3714 Bool,
3715 Some(D::Bool(false)),
3716 "Whether this is the default track"
3717 ),
3718 ],
3719 "canvas" => alloc::vec![
3720 data_field(
3721 "width",
3722 String,
3723 Some(D::String(AzString::from_const_str("300"))),
3724 "Width of the canvas in pixels"
3725 ),
3726 data_field(
3727 "height",
3728 String,
3729 Some(D::String(AzString::from_const_str("150"))),
3730 "Height of the canvas in pixels"
3731 ),
3732 ],
3733 "embed" => alloc::vec![
3734 data_field("src", String, None, "URL of the resource to embed"),
3735 data_field(
3736 "type",
3737 String,
3738 Some(D::String(AzString::from_const_str(""))),
3739 "MIME type of the embedded content"
3740 ),
3741 data_field(
3742 "width",
3743 String,
3744 Some(D::String(AzString::from_const_str(""))),
3745 "Width"
3746 ),
3747 data_field(
3748 "height",
3749 String,
3750 Some(D::String(AzString::from_const_str(""))),
3751 "Height"
3752 ),
3753 ],
3754 "object" => alloc::vec![
3755 data_field(
3756 "data",
3757 String,
3758 Some(D::String(AzString::from_const_str(""))),
3759 "URL of the resource"
3760 ),
3761 data_field(
3762 "type",
3763 String,
3764 Some(D::String(AzString::from_const_str(""))),
3765 "MIME type of the resource"
3766 ),
3767 data_field(
3768 "width",
3769 String,
3770 Some(D::String(AzString::from_const_str(""))),
3771 "Width"
3772 ),
3773 data_field(
3774 "height",
3775 String,
3776 Some(D::String(AzString::from_const_str(""))),
3777 "Height"
3778 ),
3779 ],
3780 "param" => alloc::vec![
3781 data_field("name", String, None, "Name of the parameter"),
3782 data_field(
3783 "value",
3784 String,
3785 Some(D::String(AzString::from_const_str(""))),
3786 "Value of the parameter"
3787 ),
3788 ],
3789 "area" => alloc::vec![
3790 data_field(
3791 "shape",
3792 String,
3793 Some(D::String(AzString::from_const_str("default"))),
3794 "Shape of the area (default, rect, circle, poly)"
3795 ),
3796 data_field(
3797 "coords",
3798 String,
3799 Some(D::String(AzString::from_const_str(""))),
3800 "Coordinates of the area"
3801 ),
3802 data_field(
3803 "href",
3804 String,
3805 Some(D::String(AzString::from_const_str(""))),
3806 "URL for the area link"
3807 ),
3808 data_field(
3809 "alt",
3810 String,
3811 Some(D::String(AzString::from_const_str(""))),
3812 "Alternative text"
3813 ),
3814 data_field(
3815 "target",
3816 String,
3817 Some(D::String(AzString::from_const_str(""))),
3818 "Where to open the linked document"
3819 ),
3820 ],
3821 "map" => alloc::vec![data_field(
3822 "name",
3823 String,
3824 None,
3825 "Name of the image map (referenced by usemap)"
3826 ),],
3827 "time" => alloc::vec![data_field(
3829 "datetime",
3830 String,
3831 Some(D::String(AzString::from_const_str(""))),
3832 "Machine-readable date/time value"
3833 ),],
3834 "data" => alloc::vec![data_field(
3835 "value",
3836 String,
3837 Some(D::String(AzString::from_const_str(""))),
3838 "Machine-readable value"
3839 ),],
3840 "abbr" | "acronym" | "dfn" => alloc::vec![data_field(
3841 "title",
3842 String,
3843 Some(D::String(AzString::from_const_str(""))),
3844 "Full expansion or definition"
3845 ),],
3846 "q" | "blockquote" => alloc::vec![data_field(
3847 "cite",
3848 String,
3849 Some(D::String(AzString::from_const_str(""))),
3850 "URL of the source of the quotation"
3851 ),],
3852 "del" | "ins" => alloc::vec![
3853 data_field(
3854 "cite",
3855 String,
3856 Some(D::String(AzString::from_const_str(""))),
3857 "URL explaining the change"
3858 ),
3859 data_field(
3860 "datetime",
3861 String,
3862 Some(D::String(AzString::from_const_str(""))),
3863 "Date/time of the change"
3864 ),
3865 ],
3866 "bdo" => alloc::vec![data_field(
3867 "dir",
3868 String,
3869 Some(D::String(AzString::from_const_str("ltr"))),
3870 "Text direction (ltr, rtl)"
3871 ),],
3872 "col" | "colgroup" => alloc::vec![data_field(
3873 "span",
3874 I32,
3875 Some(D::I32(1)),
3876 "Number of columns the element spans"
3877 ),],
3878 "meta" => alloc::vec![
3880 data_field(
3881 "name",
3882 String,
3883 Some(D::String(AzString::from_const_str(""))),
3884 "Metadata name"
3885 ),
3886 data_field(
3887 "content",
3888 String,
3889 Some(D::String(AzString::from_const_str(""))),
3890 "Metadata value"
3891 ),
3892 data_field(
3893 "charset",
3894 String,
3895 Some(D::String(AzString::from_const_str(""))),
3896 "Character encoding"
3897 ),
3898 data_field(
3899 "http-equiv",
3900 String,
3901 Some(D::String(AzString::from_const_str(""))),
3902 "HTTP header equivalent"
3903 ),
3904 ],
3905 "link" => alloc::vec![
3906 data_field("rel", String, None, "Relationship type"),
3907 data_field(
3908 "href",
3909 String,
3910 Some(D::String(AzString::from_const_str(""))),
3911 "URL of the linked resource"
3912 ),
3913 data_field(
3914 "type",
3915 String,
3916 Some(D::String(AzString::from_const_str(""))),
3917 "MIME type of the linked resource"
3918 ),
3919 ],
3920 "script" => alloc::vec![
3921 data_field(
3922 "src",
3923 String,
3924 Some(D::String(AzString::from_const_str(""))),
3925 "URL of external script"
3926 ),
3927 data_field(
3928 "type",
3929 String,
3930 Some(D::String(AzString::from_const_str(""))),
3931 "MIME type or module"
3932 ),
3933 data_field(
3934 "async",
3935 Bool,
3936 Some(D::Bool(false)),
3937 "Execute asynchronously"
3938 ),
3939 data_field(
3940 "defer",
3941 Bool,
3942 Some(D::Bool(false)),
3943 "Defer execution until page load"
3944 ),
3945 ],
3946 "style" => alloc::vec![data_field(
3947 "type",
3948 String,
3949 Some(D::String(AzString::from_const_str("text/css"))),
3950 "MIME type of the style sheet"
3951 ),],
3952 "base" => alloc::vec![
3953 data_field(
3954 "href",
3955 String,
3956 Some(D::String(AzString::from_const_str(""))),
3957 "Base URL for relative URLs"
3958 ),
3959 data_field(
3960 "target",
3961 String,
3962 Some(D::String(AzString::from_const_str(""))),
3963 "Default target for hyperlinks"
3964 ),
3965 ],
3966 _ => alloc::vec![],
3967 }
3968}
3969
3970impl Default for ComponentMap {
3971 fn default() -> Self {
3977 Self {
3978 libraries: ComponentLibraryVec::from_const_slice(&[]),
3979 }
3980 }
3981}
3982
3983impl ComponentMap {
3984 #[must_use] pub fn create() -> Self {
3985 Self::default()
3986 }
3987
3988 #[must_use] pub fn with_builtin() -> Self {
3990 Self {
3991 libraries: alloc::vec![register_builtin_components()].into(),
3992 }
3993 }
3994
3995 #[must_use] pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
4001 Self {
4002 libraries: libs.clone(),
4003 }
4004 }
4005}
4006
4007fn xml_attrs_to_data_model(
4022 base_model: &ComponentDataModel,
4023 xml_attributes: &XmlAttributeMap,
4024 text_content: Option<&str>,
4025) -> ComponentDataModel {
4026 let mut model = base_model.clone();
4027
4028 let mut fields_vec = core::mem::replace(
4030 &mut model.fields,
4031 ComponentDataFieldVec::from_const_slice(&[]),
4032 )
4033 .into_library_owned_vec();
4034
4035 for field in &mut fields_vec {
4036 if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
4037 field.default_value = OptionComponentDefaultValue::Some(ComponentDefaultValue::String(
4039 attr_value.clone(),
4040 ));
4041 }
4042 }
4043
4044 model.fields = ComponentDataFieldVec::from_vec(fields_vec);
4045
4046 if let Some(text) = text_content {
4048 let prepared = prepare_string(text);
4049 if !prepared.is_empty() {
4050 model = model.with_default(
4051 "text",
4052 ComponentDefaultValue::String(AzString::from(prepared.as_str())),
4053 );
4054 }
4055 }
4056
4057 model
4058}
4059
4060fn builtin_if_component() -> ComponentDef {
4067 ComponentDef {
4068 id: ComponentId::builtin("if"),
4069 display_name: AzString::from_const_str("If"),
4070 description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
4071 css: AzString::from_const_str(""),
4072 source: ComponentSource::Builtin,
4073 data_model: ComponentDataModel {
4074 name: AzString::from_const_str("IfData"),
4075 description: AzString::from_const_str("Data for conditional rendering"),
4076 fields: alloc::vec![
4077 data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
4078 ].into(),
4079 },
4080 render_fn: builtin_if_render_fn,
4081 compile_fn: builtin_if_compile_fn,
4082 render_fn_source: None.into(),
4083 compile_fn_source: None.into(),
4084 }
4085}
4086
4087fn builtin_if_render_fn(
4088 _comp: &ComponentDef,
4089 data_model: &ComponentDataModel,
4090 _component_map: &ComponentMap,
4091) -> ResultStyledDomRenderDomError {
4092 let condition = data_model
4094 .fields
4095 .iter()
4096 .find(|f| f.name.as_str() == "condition")
4097 .and_then(|f| match &f.default_value {
4098 OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b),
4099 _ => None,
4100 })
4101 .unwrap_or(false);
4102
4103 let label = if condition {
4104 "if: true (then branch)"
4105 } else {
4106 "if: false (else branch)"
4107 };
4108 let mut dom =
4109 Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4110 let css = Css::empty();
4111 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4112}
4113
4114fn builtin_if_compile_fn(
4115 _comp: &ComponentDef,
4116 target: &CompileTarget,
4117 _data: &ComponentDataModel,
4118 _indent: usize,
4119) -> ResultStringCompileError {
4120 match target {
4121 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4122 "if data.condition {\n // then branch\n Dom::create_div()\n} else {\n // else branch\n Dom::create_div()\n}"
4123 )),
4124 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4125 "if (data.condition) {\n // then branch\n AzDom_createDiv();\n} else {\n // else branch\n AzDom_createDiv();\n}"
4126 )),
4127 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4128 "if (data.condition) {\n // then branch\n Dom::create_div();\n} else {\n // else branch\n Dom::create_div();\n}"
4129 )),
4130 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4131 "if data.condition:\n # then branch\n Dom.create_div()\nelse:\n # else branch\n Dom.create_div()"
4132 )),
4133 }
4134}
4135
4136fn builtin_for_component() -> ComponentDef {
4139 ComponentDef {
4140 id: ComponentId::builtin("for"),
4141 display_name: AzString::from_const_str("For Loop"),
4142 description: AzString::from_const_str(
4143 "Iterative rendering: repeats children 'count' times.",
4144 ),
4145 css: AzString::from_const_str(""),
4146 source: ComponentSource::Builtin,
4147 data_model: ComponentDataModel {
4148 name: AzString::from_const_str("ForData"),
4149 description: AzString::from_const_str("Data for iterative rendering"),
4150 fields: alloc::vec![data_field(
4151 "count",
4152 ComponentFieldType::U32,
4153 Some(ComponentDefaultValue::U32(3)),
4154 "Number of iterations"
4155 ),]
4156 .into(),
4157 },
4158 render_fn: builtin_for_render_fn,
4159 compile_fn: builtin_for_compile_fn,
4160 render_fn_source: None.into(),
4161 compile_fn_source: None.into(),
4162 }
4163}
4164
4165fn builtin_for_render_fn(
4166 _comp: &ComponentDef,
4167 data_model: &ComponentDataModel,
4168 _component_map: &ComponentMap,
4169) -> ResultStyledDomRenderDomError {
4170 let count = data_model
4171 .fields
4172 .iter()
4173 .find(|f| f.name.as_str() == "count")
4174 .and_then(|f| match &f.default_value {
4175 OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n),
4176 _ => None,
4177 })
4178 .unwrap_or(3);
4179
4180 let mut items: Vec<Dom> = Vec::new();
4181 for i in 0..count {
4182 items.push(
4183 Dom::create_node(NodeType::Div)
4184 .with_children(alloc::vec![Dom::create_text(alloc::format!("Item {i}"))].into()),
4185 );
4186 }
4187 let mut dom = Dom::create_node(NodeType::Div).with_children(items.into());
4188 let css = Css::empty();
4189 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4190}
4191
4192fn builtin_for_compile_fn(
4193 _comp: &ComponentDef,
4194 target: &CompileTarget,
4195 _data: &ComponentDataModel,
4196 _indent: usize,
4197) -> ResultStringCompileError {
4198 match target {
4199 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4200 "let mut children = Vec::new();\nfor i in 0..data.count {\n children.push(Dom::create_div());\n}\nDom::create_div().with_children(children)"
4201 )),
4202 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4203 "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n AzDom_addChild(&container, AzDom_createDiv());\n}"
4204 )),
4205 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4206 "auto container = Dom::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n container.add_child(Dom::create_div());\n}"
4207 )),
4208 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4209 "container = Dom.create_div()\nfor i in range(data.count):\n container = container.with_child(Dom.create_div())"
4210 )),
4211 }
4212}
4213
4214fn builtin_map_component() -> ComponentDef {
4217 ComponentDef {
4218 id: ComponentId::builtin("map"),
4219 display_name: AzString::from_const_str("Map"),
4220 description: AzString::from_const_str(
4221 "Map data to DOM: applies a template to each item in a collection.",
4222 ),
4223 css: AzString::from_const_str(""),
4224 source: ComponentSource::Builtin,
4225 data_model: ComponentDataModel {
4226 name: AzString::from_const_str("MapData"),
4227 description: AzString::from_const_str("Data for map rendering"),
4228 fields: alloc::vec![data_field(
4229 "data_json",
4230 ComponentFieldType::String,
4231 Some(ComponentDefaultValue::String(AzString::from_const_str(
4232 "[]"
4233 ))),
4234 "JSON array of items to map over"
4235 ),]
4236 .into(),
4237 },
4238 render_fn: builtin_map_render_fn,
4239 compile_fn: builtin_map_compile_fn,
4240 render_fn_source: None.into(),
4241 compile_fn_source: None.into(),
4242 }
4243}
4244
4245fn builtin_map_render_fn(
4246 _comp: &ComponentDef,
4247 data_model: &ComponentDataModel,
4248 _component_map: &ComponentMap,
4249) -> ResultStyledDomRenderDomError {
4250 let data_str = data_model
4252 .fields
4253 .iter()
4254 .find(|f| f.name.as_str() == "data_json")
4255 .and_then(|f| match &f.default_value {
4256 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
4257 Some(s.as_str().to_string())
4258 }
4259 _ => None,
4260 })
4261 .unwrap_or_else(|| "[]".to_string());
4262
4263 let label = alloc::format!("map: data_json={data_str}");
4264 let mut dom =
4265 Dom::create_node(NodeType::Div).with_children(alloc::vec![Dom::create_text(label)].into());
4266 let css = Css::empty();
4267 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4268}
4269
4270fn builtin_map_compile_fn(
4271 _comp: &ComponentDef,
4272 target: &CompileTarget,
4273 _data: &ComponentDataModel,
4274 _indent: usize,
4275) -> ResultStringCompileError {
4276 match target {
4277 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4278 "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)"
4279 )),
4280 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4281 "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
4282 )),
4283 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4284 "// Parse data.data_json and map each item\nauto container = Dom::create_div();\n// TODO: iterate parsed JSON array"
4285 )),
4286 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4287 "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n container = container.with_child(Dom.create_div())"
4288 )),
4289 }
4290}
4291
4292#[allow(clippy::too_many_lines)] #[must_use] pub extern "C" fn register_builtin_components() -> ComponentLibrary {
4302 ComponentLibrary {
4303 name: AzString::from_const_str("builtin"),
4304 version: AzString::from_const_str("1.0.0"),
4305 description: AzString::from_const_str("Built-in HTML elements"),
4306 exportable: false,
4307 modifiable: false,
4308 data_models: Vec::new().into(),
4309 enum_models: Vec::new().into(),
4310 components: alloc::vec![
4311 builtin_component_def("html", "HTML", None, ""),
4313 builtin_component_def("head", "Head", None, ""),
4314 builtin_component_def("title", "Title", Some(""), ""),
4315 builtin_component_def("body", "Body", None, ""),
4316 builtin_component_def("div", "Div", None, ""),
4318 builtin_component_def("header", "Header", None, ""),
4319 builtin_component_def("footer", "Footer", None, ""),
4320 builtin_component_def("section", "Section", None, ""),
4321 builtin_component_def("article", "Article", None, ""),
4322 builtin_component_def("aside", "Aside", None, ""),
4323 builtin_component_def("nav", "Nav", None, ""),
4324 builtin_component_def("main", "Main", None, ""),
4325 builtin_component_def("figure", "Figure", None, ""),
4326 builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
4327 builtin_component_def("address", "Address", Some(""), ""),
4328 builtin_component_def("details", "Details", None, ""),
4329 builtin_component_def("summary", "Summary", Some("Details"), ""),
4330 builtin_component_def("dialog", "Dialog", None, ""),
4331 builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
4333 builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
4334 builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
4335 builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
4336 builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
4337 builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
4338 builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
4340 builtin_component_def("span", "Span", Some(""), ""),
4341 builtin_component_def("pre", "Preformatted", Some(""), ""),
4342 builtin_component_def("code", "Code", Some(""), ""),
4343 builtin_component_def("blockquote", "Blockquote", Some(""), ""),
4344 builtin_component_def("br", "Line Break", None, ""),
4345 builtin_component_def("hr", "Horizontal Rule", None, ""),
4346 builtin_component_def("icon", "Icon", Some(""), ""),
4347 builtin_component_def("ul", "Unordered List", None, ""),
4349 builtin_component_def("ol", "Ordered List", None, ""),
4350 builtin_component_def("li", "List Item", Some("List item"), ""),
4351 builtin_component_def("dl", "Description List", None, ""),
4352 builtin_component_def("dt", "Description Term", Some(""), ""),
4353 builtin_component_def("dd", "Description Details", Some(""), ""),
4354 builtin_component_def("menu", "Menu", None, ""),
4355 builtin_component_def("menuitem", "Menu Item", Some(""), ""),
4356 builtin_component_def("dir", "Directory List", None, ""),
4357 builtin_component_def("table", "Table", None, ""),
4359 builtin_component_def("caption", "Table Caption", Some(""), ""),
4360 builtin_component_def("thead", "Table Head", None, ""),
4361 builtin_component_def("tbody", "Table Body", None, ""),
4362 builtin_component_def("tfoot", "Table Foot", None, ""),
4363 builtin_component_def("tr", "Table Row", None, ""),
4364 builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
4365 builtin_component_def("td", "Table Data Cell", Some(""), ""),
4366 builtin_component_def("colgroup", "Column Group", None, ""),
4367 builtin_component_def("col", "Column", None, ""),
4368 builtin_component_def("a", "Link", Some("Link text"), ""),
4370 builtin_component_def("strong", "Strong", Some(""), ""),
4371 builtin_component_def("em", "Emphasis", Some(""), ""),
4372 builtin_component_def("b", "Bold", Some(""), ""),
4373 builtin_component_def("i", "Italic", Some(""), ""),
4374 builtin_component_def("u", "Underline", Some(""), ""),
4375 builtin_component_def("s", "Strikethrough", Some(""), ""),
4376 builtin_component_def("small", "Small", Some(""), ""),
4377 builtin_component_def("mark", "Mark", Some(""), ""),
4378 builtin_component_def("del", "Deleted Text", Some(""), ""),
4379 builtin_component_def("ins", "Inserted Text", Some(""), ""),
4380 builtin_component_def("sub", "Subscript", Some(""), ""),
4381 builtin_component_def("sup", "Superscript", Some(""), ""),
4382 builtin_component_def("samp", "Sample Output", Some(""), ""),
4383 builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
4384 builtin_component_def("var", "Variable", Some(""), ""),
4385 builtin_component_def("cite", "Citation", Some(""), ""),
4386 builtin_component_def("dfn", "Definition", Some(""), ""),
4387 builtin_component_def("abbr", "Abbreviation", Some(""), ""),
4388 builtin_component_def("acronym", "Acronym", Some(""), ""),
4389 builtin_component_def("q", "Inline Quote", Some(""), ""),
4390 builtin_component_def("time", "Time", Some(""), ""),
4391 builtin_component_def("big", "Big", Some(""), ""),
4392 builtin_component_def("bdo", "BiDi Override", Some(""), ""),
4393 builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
4394 builtin_component_def("wbr", "Word Break Opportunity", None, ""),
4395 builtin_component_def("ruby", "Ruby Annotation", None, ""),
4396 builtin_component_def("rt", "Ruby Text", Some(""), ""),
4397 builtin_component_def("rtc", "Ruby Text Container", None, ""),
4398 builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
4399 builtin_component_def("data", "Data", Some(""), ""),
4400 builtin_component_def("form", "Form", None, ""),
4402 builtin_component_def("fieldset", "Field Set", None, ""),
4403 builtin_component_def("legend", "Legend", Some("Legend"), ""),
4404 builtin_component_def("label", "Label", Some("Label"), ""),
4405 builtin_component_def("input", "Input", None, ""),
4406 builtin_component_def("button", "Button", Some("Button text"), ""),
4407 builtin_component_def("select", "Select", None, ""),
4408 builtin_component_def("optgroup", "Option Group", None, ""),
4409 builtin_component_def("option", "Option", Some(""), ""),
4410 builtin_component_def("textarea", "Text Area", Some(""), ""),
4411 builtin_component_def("output", "Output", Some(""), ""),
4412 builtin_component_def("progress", "Progress", None, ""),
4413 builtin_component_def("meter", "Meter", None, ""),
4414 builtin_component_def("datalist", "Data List", None, ""),
4415 builtin_component_def("canvas", "Canvas", None, ""),
4417 builtin_component_def("object", "Object", None, ""),
4418 builtin_component_def("param", "Parameter", None, ""),
4419 builtin_component_def("embed", "Embed", None, ""),
4420 builtin_component_def("audio", "Audio", None, ""),
4421 builtin_component_def("video", "Video", None, ""),
4422 builtin_component_def("source", "Source", None, ""),
4423 builtin_component_def("track", "Track", None, ""),
4424 builtin_component_def("map", "Image Map", None, ""),
4425 builtin_component_def("area", "Map Area", None, ""),
4426 builtin_component_def("svg", "SVG", None, ""),
4427 builtin_component_def("meta", "Meta", None, ""),
4429 builtin_component_def("link", "Link (Resource)", None, ""),
4430 builtin_component_def("script", "Script", Some(""), ""),
4431 builtin_component_def("style", "Style", Some(""), ""),
4432 builtin_component_def("base", "Base URL", None, ""),
4433 builtin_if_component(),
4435 builtin_for_component(),
4436 builtin_map_component(),
4437 ]
4438 .into(),
4439 }
4440}
4441
4442#[derive(Debug, Default)]
4449pub struct DomXml {
4450 pub parsed_dom: StyledDom,
4451}
4452
4453impl DomXml {
4454 #[cfg(test)]
4472 pub fn assert_eq(self, other: StyledDom) {
4473 let mut body = Dom::create_body();
4474 let mut fixed = StyledDom::create(&mut body, Css::empty());
4475 fixed.append_child(other);
4476 assert!(!(self.parsed_dom != fixed),
4477 "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
4478 ----------\r\n{}\r\n",
4479 self.parsed_dom.get_html_string("", "", true),
4480 fixed.get_html_string("", "", true)
4481 );
4482 }
4483
4484 #[must_use] pub fn into_styled_dom(self) -> StyledDom {
4485 self.into()
4486 }
4487}
4488
4489impl From<DomXml> for StyledDom {
4490 fn from(val: DomXml) -> Self {
4491 val.parsed_dom
4492 }
4493}
4494
4495#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4497#[repr(C, u8)]
4498pub enum XmlNodeChild {
4499 Text(AzString),
4501 Element(XmlNode),
4503}
4504
4505impl_option!(
4506 XmlNodeChild,
4507 OptionXmlNodeChild,
4508 copy = false,
4509 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4510);
4511
4512impl XmlNodeChild {
4513 #[must_use] pub fn as_text(&self) -> Option<&str> {
4515 match self {
4516 Self::Text(s) => Some(s.as_str()),
4517 Self::Element(_) => None,
4518 }
4519 }
4520
4521 #[must_use] pub const fn as_element(&self) -> Option<&XmlNode> {
4523 match self {
4524 Self::Text(_) => None,
4525 Self::Element(node) => Some(node),
4526 }
4527 }
4528
4529 pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
4531 match self {
4532 Self::Text(_) => None,
4533 Self::Element(node) => Some(node),
4534 }
4535 }
4536}
4537
4538impl_vec!(
4539 XmlNodeChild,
4540 XmlNodeChildVec,
4541 XmlNodeChildVecDestructor,
4542 XmlNodeChildVecDestructorType,
4543 XmlNodeChildVecSlice,
4544 OptionXmlNodeChild
4545);
4546impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
4547impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
4548impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
4549impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
4550impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
4551impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
4552impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
4553impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
4554
4555#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4557#[repr(C)]
4558pub struct XmlNode {
4559 pub node_type: XmlTagName,
4561 pub attributes: XmlAttributeMap,
4563 pub children: XmlNodeChildVec,
4565}
4566
4567impl_option!(
4568 XmlNode,
4569 OptionXmlNode,
4570 copy = false,
4571 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4572);
4573
4574impl XmlNode {
4575 pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
4576 Self {
4577 node_type: node_type.into(),
4578 ..Default::default()
4579 }
4580 }
4581 #[must_use] pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
4582 Self {
4583 children: v.into(),
4584 ..self
4585 }
4586 }
4587
4588 #[must_use] pub fn get_text_content(&self) -> String {
4590 self.children
4591 .as_ref()
4592 .iter()
4593 .filter_map(|child| child.as_text())
4594 .collect::<Vec<_>>()
4595 .join("")
4596 }
4597
4598 #[must_use] pub fn has_only_text_children(&self) -> bool {
4600 self.children
4601 .as_ref()
4602 .iter()
4603 .all(|child| matches!(child, XmlNodeChild::Text(_)))
4604 }
4605}
4606
4607impl_vec!(
4608 XmlNode,
4609 XmlNodeVec,
4610 XmlNodeVecDestructor,
4611 XmlNodeVecDestructorType,
4612 XmlNodeVecSlice,
4613 OptionXmlNode
4614);
4615impl_vec_mut!(XmlNode, XmlNodeVec);
4616impl_vec_debug!(XmlNode, XmlNodeVec);
4617impl_vec_partialeq!(XmlNode, XmlNodeVec);
4618impl_vec_eq!(XmlNode, XmlNodeVec);
4619impl_vec_partialord!(XmlNode, XmlNodeVec);
4620impl_vec_ord!(XmlNode, XmlNodeVec);
4621impl_vec_hash!(XmlNode, XmlNodeVec);
4622impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
4623
4624#[derive(Debug, Clone, PartialEq)]
4625#[repr(C, u8)]
4626pub enum DomXmlParseError {
4627 NoHtmlNode,
4629 MultipleHtmlRootNodes,
4631 NoBodyInHtml,
4633 MultipleBodyNodes,
4635 Xml(XmlError),
4640 MalformedHierarchy(MalformedHierarchyError),
4642 RenderDom(RenderDomError),
4645 Component(ComponentParseError),
4647 Css(CssParseErrorOwned),
4649}
4650
4651impl From<XmlError> for DomXmlParseError {
4652 fn from(e: XmlError) -> Self {
4653 Self::Xml(e)
4654 }
4655}
4656
4657impl From<ComponentParseError> for DomXmlParseError {
4658 fn from(e: ComponentParseError) -> Self {
4659 Self::Component(e)
4660 }
4661}
4662
4663impl From<RenderDomError> for DomXmlParseError {
4664 fn from(e: RenderDomError) -> Self {
4665 Self::RenderDom(e)
4666 }
4667}
4668
4669impl From<CssParseErrorOwned> for DomXmlParseError {
4670 fn from(e: CssParseErrorOwned) -> Self {
4671 Self::Css(e)
4672 }
4673}
4674
4675#[derive(Debug, Clone, PartialEq)]
4678#[repr(C, u8)]
4679pub enum CompileError {
4680 Dom(RenderDomError),
4681 Xml(DomXmlParseError),
4682 Css(CssParseErrorOwned),
4683}
4684
4685impl From<ComponentError> for CompileError {
4686 fn from(e: ComponentError) -> Self {
4687 Self::Dom(RenderDomError::Component(e))
4688 }
4689}
4690
4691impl From<CssParseErrorOwned> for CompileError {
4692 fn from(e: CssParseErrorOwned) -> Self {
4693 Self::Css(e)
4694 }
4695}
4696
4697impl fmt::Display for CompileError {
4698 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4699 use self::CompileError::{Dom, Xml, Css};
4700 match self {
4701 Dom(d) => write!(f, "{d}"),
4702 Xml(s) => write!(f, "{s}"),
4703 Css(s) => write!(f, "{}", s.to_shared()),
4704 }
4705 }
4706}
4707
4708impl From<RenderDomError> for CompileError {
4709 fn from(e: RenderDomError) -> Self {
4710 Self::Dom(e)
4711 }
4712}
4713
4714impl From<DomXmlParseError> for CompileError {
4715 fn from(e: DomXmlParseError) -> Self {
4716 Self::Xml(e)
4717 }
4718}
4719
4720#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4722#[repr(C)]
4723pub struct UselessFunctionArgumentError {
4724 pub component_name: AzString,
4725 pub argument_name: AzString,
4726 pub valid_args: StringVec,
4727}
4728
4729#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4730#[repr(C, u8)]
4731pub enum ComponentError {
4732 UselessFunctionArgument(UselessFunctionArgumentError),
4735 UnknownComponent(AzString),
4740}
4741
4742#[derive(Debug, Clone, PartialEq)]
4743#[repr(C, u8)]
4744pub enum RenderDomError {
4745 Component(ComponentError),
4746 CssError(CssParseErrorOwned),
4748}
4749
4750impl From<ComponentError> for RenderDomError {
4751 fn from(e: ComponentError) -> Self {
4752 Self::Component(e)
4753 }
4754}
4755
4756impl From<CssParseErrorOwned> for RenderDomError {
4757 fn from(e: CssParseErrorOwned) -> Self {
4758 Self::CssError(e)
4759 }
4760}
4761
4762#[derive(Debug, Clone, PartialEq, Eq)]
4764#[repr(C)]
4765pub struct MissingTypeError {
4766 pub arg_pos: usize,
4767 pub arg_name: AzString,
4768}
4769
4770#[derive(Debug, Clone, PartialEq, Eq)]
4772#[repr(C)]
4773pub struct WhiteSpaceInComponentNameError {
4774 pub arg_pos: usize,
4775 pub arg_name: AzString,
4776}
4777
4778#[derive(Debug, Clone, PartialEq, Eq)]
4780#[repr(C)]
4781pub struct WhiteSpaceInComponentTypeError {
4782 pub arg_pos: usize,
4783 pub arg_name: AzString,
4784 pub arg_type: AzString,
4785}
4786
4787#[derive(Debug, Clone, PartialEq)]
4788#[repr(C, u8)]
4789pub enum ComponentParseError {
4790 NotAComponent,
4792 UnnamedComponent,
4794 MissingName(usize),
4796 MissingType(MissingTypeError),
4799 WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
4802 WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
4805 CssError(CssParseErrorOwned),
4807}
4808
4809impl fmt::Display for DomXmlParseError {
4810 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4811 use self::DomXmlParseError::{NoHtmlNode, MultipleHtmlRootNodes, NoBodyInHtml, MultipleBodyNodes, Xml, MalformedHierarchy, RenderDom, Component, Css};
4812 match self {
4813 NoHtmlNode => write!(
4814 f,
4815 "No <html> node found as the root of the file - empty file?"
4816 ),
4817 MultipleHtmlRootNodes => write!(
4818 f,
4819 "Multiple <html> nodes found as the root of the file - only one root node allowed"
4820 ),
4821 NoBodyInHtml => write!(
4822 f,
4823 "No <body> node found as a direct child of an <html> node - malformed DOM \
4824 hierarchy?"
4825 ),
4826 MultipleBodyNodes => write!(
4827 f,
4828 "Multiple <body> nodes present, only one <body> node is allowed"
4829 ),
4830 Xml(e) => write!(f, "Error parsing XML: {e}"),
4831 MalformedHierarchy(e) => write!(
4832 f,
4833 "Invalid </{}> tag: expected </{}>",
4834 e.got.as_str(),
4835 e.expected.as_str()
4836 ),
4837 RenderDom(e) => write!(f, "Error rendering DOM: {e}"),
4838 Component(c) => write!(f, "Error parsing component in <head> node:\r\n{c}"),
4839 Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
4840 }
4841 }
4842}
4843
4844impl fmt::Display for ComponentParseError {
4845 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4846 use self::ComponentParseError::{NotAComponent, UnnamedComponent, MissingName, MissingType, WhiteSpaceInComponentName, WhiteSpaceInComponentType, CssError};
4847 match self {
4848 NotAComponent => write!(f, "Expected <component/> node, found no such node"),
4849 UnnamedComponent => write!(
4850 f,
4851 "Found <component/> tag with out a \"name\" attribute, component must have a name"
4852 ),
4853 MissingName(arg_pos) => write!(
4854 f,
4855 "Argument at position {arg_pos} is either empty or has no name"
4856 ),
4857 MissingType(e) => write!(
4858 f,
4859 "Argument \"{}\" at position {} doesn't have a `: type`",
4860 e.arg_name, e.arg_pos
4861 ),
4862 WhiteSpaceInComponentName(e) => {
4863 write!(
4864 f,
4865 "Missing `:` between the name and the type in argument {} (around \"{}\")",
4866 e.arg_pos, e.arg_name
4867 )
4868 }
4869 WhiteSpaceInComponentType(e) => {
4870 write!(
4871 f,
4872 "Missing `,` between two arguments (in argument {}, position {}, around \
4873 \"{}\")",
4874 e.arg_name, e.arg_pos, e.arg_type
4875 )
4876 }
4877 CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
4878 }
4879 }
4880}
4881
4882impl fmt::Display for ComponentError {
4883 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4884 use self::ComponentError::{UselessFunctionArgument, UnknownComponent};
4885 match self {
4886 UselessFunctionArgument(e) => {
4887 write!(
4888 f,
4889 "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
4890 e.component_name, e.argument_name, e.valid_args
4891 )
4892 }
4893 UnknownComponent(name) => write!(f, "Unknown component: \"{name}\""),
4894 }
4895 }
4896}
4897
4898impl fmt::Display for RenderDomError {
4899 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4900 use self::RenderDomError::{Component, CssError};
4901 match self {
4902 Component(c) => write!(f, "{c}"),
4903 CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
4904 }
4905 }
4906}
4907
4908#[allow(clippy::result_large_err)] pub fn get_html_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4915 let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
4916 if let XmlNodeChild::Element(node) = child {
4917 if node.node_type.as_str().eq_ignore_ascii_case("html") {
4921 Some(node)
4922 } else {
4923 None
4924 }
4925 } else {
4926 None
4927 }
4928 });
4929
4930 let html_node = html_node_iterator
4931 .next()
4932 .ok_or(DomXmlParseError::NoHtmlNode)?;
4933 if html_node_iterator.next().is_some() {
4934 Err(DomXmlParseError::MultipleHtmlRootNodes)
4935 } else {
4936 Ok(html_node)
4937 }
4938}
4939
4940#[allow(clippy::result_large_err)] pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
4947 fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
4948 if depth > MAX_XML_NESTING_DEPTH {
4951 return None;
4952 }
4953 for child in nodes {
4954 if let XmlNodeChild::Element(node) = child {
4955 if node.node_type.as_str().eq_ignore_ascii_case("body") {
4957 return Some(node);
4958 }
4959 if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
4961 return Some(found);
4962 }
4963 }
4964 }
4965 None
4966 }
4967
4968 let direct_body = root_nodes.iter().find_map(|child| {
4970 if let XmlNodeChild::Element(node) = child {
4971 if node.node_type.as_str().eq_ignore_ascii_case("body") {
4973 Some(node)
4974 } else {
4975 None
4976 }
4977 } else {
4978 None
4979 }
4980 });
4981
4982 if let Some(body) = direct_body {
4983 return Ok(body);
4984 }
4985
4986 find_body_recursive(root_nodes, 0).ok_or(DomXmlParseError::NoBodyInHtml)
4989}
4990
4991fn find_node_by_type<'a>(root_nodes: &'a [XmlNodeChild], node_type: &str) -> Option<&'a XmlNode> {
4995 for child in root_nodes {
4997 if let XmlNodeChild::Element(node) = child {
4998 if node.node_type.as_str().eq_ignore_ascii_case(node_type) {
5000 return Some(node);
5001 }
5002 }
5003 }
5004
5005 for child in root_nodes {
5007 if let XmlNodeChild::Element(node) = child {
5008 if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
5009 return Some(found);
5010 }
5011 }
5012 }
5013
5014 None
5015}
5016
5017#[must_use] pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
5018 node.attributes
5019 .iter()
5020 .find(|n| normalize_casing(n.key.as_str()).as_str() == attribute)
5021 .map(|s| &s.value)
5022}
5023
5024#[must_use] pub fn normalize_casing(input: &str) -> String {
5026 let mut words: Vec<String> = Vec::new();
5027 let mut cur_str = Vec::new();
5028
5029 for ch in input.chars() {
5030 if ch.is_uppercase() || ch == '_' || ch == '-' {
5031 if !cur_str.is_empty() {
5032 words.push(cur_str.iter().collect());
5033 cur_str.clear();
5034 }
5035 if ch.is_uppercase() {
5036 cur_str.extend(ch.to_lowercase());
5037 }
5038 } else {
5039 cur_str.extend(ch.to_lowercase());
5040 }
5041 }
5042
5043 if !cur_str.is_empty() {
5044 words.push(cur_str.iter().collect());
5045 cur_str.clear();
5046 }
5047
5048 words.join("_")
5049}
5050
5051#[allow(trivial_casts)]
5054pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
5055 let mut hierarchy = hierarchy.to_vec();
5056 hierarchy.reverse();
5057 let Some(item) = hierarchy.pop() else {
5058 return Some(root_node);
5059 };
5060 let child = root_node.children.as_mut().get_mut(item)?;
5061 match child {
5062 XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
5063 XmlNodeChild::Text(_) => None, }
5065}
5066
5067fn get_item_internal<'a>(
5068 hierarchy: &mut Vec<usize>,
5069 root_node: &'a mut XmlNode,
5070) -> Option<&'a mut XmlNode> {
5071 if hierarchy.is_empty() {
5072 return Some(root_node);
5073 }
5074 let Some(cur_item) = hierarchy.pop() else {
5075 return Some(root_node);
5076 };
5077 let child = root_node.children.as_mut().get_mut(cur_item)?;
5078 match child {
5079 XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
5080 XmlNodeChild::Text(_) => None, }
5082}
5083
5084#[allow(clippy::result_large_err)] pub fn str_to_dom<'a>(
5091 root_nodes: &'a [XmlNodeChild],
5092 component_map: &'a ComponentMap,
5093 max_width: Option<f32>,
5094) -> Result<StyledDom, DomXmlParseError> {
5095 str_to_dom_fast(root_nodes, component_map, max_width)
5097}
5098
5099#[allow(clippy::result_large_err)] fn str_to_dom_fast<'a>(
5105 root_nodes: &'a [XmlNodeChild],
5106 component_map: &'a ComponentMap,
5107 max_width: Option<f32>,
5108) -> Result<StyledDom, DomXmlParseError> {
5109 let html_node = get_html_node(root_nodes)?;
5110 let body_node = get_body_node(html_node.children.as_ref())?;
5111
5112 let mut global_style = None;
5113
5114 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5115 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5116 let text = style_node.get_text_content();
5117 if !text.is_empty() {
5118 let parsed_css = Css::from_string(text.into());
5119 global_style = Some(parsed_css);
5120 }
5121 }
5122 }
5123
5124 render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
5125 .map_err(Into::into)
5126}
5127
5128#[allow(clippy::result_large_err)] pub fn str_to_dom_unstyled<'a>(
5141 root_nodes: &'a [XmlNodeChild],
5142 component_map: &'a ComponentMap,
5143) -> Result<Dom, DomXmlParseError> {
5144 let html_node = get_html_node(root_nodes)?;
5145 let body_node = get_body_node(html_node.children.as_ref())?;
5146
5147 let mut global_style = None;
5148
5149 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5150 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5151 let text = style_node.get_text_content();
5152 if !text.is_empty() {
5153 let parsed_css = Css::from_string(text.into());
5154 global_style = Some(parsed_css);
5155 }
5156 }
5157 }
5158
5159 let body_dom = xml_node_to_dom_fast(body_node, component_map, false, 0)
5161 .map_err(DomXmlParseError::from)?;
5162
5163 let root_node_type = body_dom.root.node_type.clone();
5165
5166 let mut full_dom = match root_node_type {
5167 NodeType::Html => body_dom,
5168 NodeType::Body => Dom::create_html().with_child(body_dom),
5169 _ => {
5170 let body_wrapper = Dom::create_body().with_child(body_dom);
5171 Dom::create_html().with_child(body_wrapper)
5172 }
5173 };
5174
5175 if let Some(css) = global_style {
5177 full_dom.css = alloc::vec![css].into();
5178 }
5179
5180 Ok(full_dom)
5181}
5182
5183#[allow(clippy::result_large_err)] pub fn str_to_rust_code<'a>(
5190 root_nodes: &'a [XmlNodeChild],
5191 imports: &str,
5192 component_map: &'a ComponentMap,
5193) -> Result<String, CompileError> {
5194 let html_node = get_html_node(root_nodes)?;
5195 let body_node = get_body_node(html_node.children.as_ref())?;
5196 let mut global_style = Css::empty();
5197
5198 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
5199 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
5200 let text = style_node.get_text_content();
5201 if !text.is_empty() {
5202 let parsed_css = azul_css::parser2::new_from_str(&text).0;
5203 global_style = parsed_css;
5204 }
5205 }
5206 }
5207
5208 global_style.sort_by_specificity();
5209
5210 let mut css_blocks = BTreeMap::new();
5211 let mut extra_blocks = VecContents::default();
5212 let app_source = compile_body_node_to_rust_code(
5213 body_node,
5214 component_map,
5215 &mut extra_blocks,
5216 &mut css_blocks,
5217 &global_style,
5218 CssMatcher {
5219 path: Vec::new(),
5220 indices_in_parent: vec![0],
5221 children_length: vec![body_node.children.as_ref().len()],
5222 },
5223 )?;
5224
5225 let app_source = app_source
5226 .lines()
5227 .map(|l| format!(" {l}"))
5228 .collect::<Vec<String>>()
5229 .join("\r\n");
5230
5231 let _ = (&css_blocks, &extra_blocks);
5236
5237 let main_func = "
5238
5239use azul::{
5240 app::{App, AppConfig},
5241 dom::Dom,
5242 callbacks::{RefAny, LayoutCallbackInfo},
5243 window::WindowCreateOptions,
5244};
5245
5246struct Data { }
5247
5248extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
5249 crate::ui::render()
5250}
5251
5252fn main() {
5253 let config = AppConfig::create();
5254 let app = App::create(RefAny::new(Data { }), config);
5255 let window = WindowCreateOptions::create(render);
5256 app.run(window);
5257}";
5258
5259 let ui_module = format!(
5260 "#[allow(unused_imports)]\r\npub mod ui {{
5261
5262 use azul::prelude::*;
5263 use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
5264 use azul::str::String as AzString;
5265
5266 pub fn render() -> Dom {{\r\n{app_source}\r\n }}\r\n}}"
5267 );
5268 let source_code = format!(
5269 "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
5270 code\r\n{}\r\n{}\r\n\r\n{}{}",
5271 imports,
5272 compile_components(Vec::new()), ui_module,
5274 main_func,
5275 );
5276
5277 Ok(source_code)
5278}
5279
5280#[allow(clippy::needless_pass_by_value)] fn compile_components(
5283 components: Vec<(
5284 ComponentName,
5285 CompiledComponent,
5286 ComponentArguments,
5287 BTreeMap<String, String>,
5288 )>,
5289) -> String {
5290 let cs = components
5291 .iter()
5292 .map(|(name, function_body, function_args, css_blocks)| {
5293 let name = &normalize_casing(name);
5294 let f = compile_component(name, function_args, function_body)
5295 .lines()
5296 .map(|l| format!(" {l}"))
5297 .collect::<Vec<String>>()
5298 .join("\r\n");
5299
5300 format!(
5303 "#[allow(unused_imports)]\r\npub mod {name} {{\r\n use azul::dom::Dom;\r\n use \
5304 azul::str::String as AzString;\r\n{f}\r\n}}"
5305 )
5306 })
5307 .collect::<Vec<String>>()
5308 .join("\r\n\r\n");
5309
5310 let cs = cs
5311 .lines()
5312 .map(|l| format!(" {l}"))
5313 .collect::<Vec<String>>()
5314 .join("\r\n");
5315
5316 if cs.is_empty() {
5317 cs
5318 } else {
5319 format!("pub mod components {{\r\n{cs}\r\n}}")
5320 }
5321}
5322
5323fn format_component_args(component_args: &ComponentArgumentVec) -> String {
5324 let mut args = component_args
5325 .iter()
5326 .map(|a| format!("{}: {}", a.name, a.arg_type))
5327 .collect::<Vec<String>>();
5328
5329 args.sort_by(|a, b| b.cmp(a));
5330
5331 args.join(", ")
5332}
5333
5334#[must_use] pub fn compile_component(
5335 component_name: &str,
5336 component_args: &ComponentArguments,
5337 component_function_body: &str,
5338) -> String {
5339 let component_name = &normalize_casing(component_name);
5340 let function_args = format_component_args(&component_args.args);
5341 let component_function_body = component_function_body
5342 .lines()
5343 .map(|l| format!(" {l}"))
5344 .collect::<Vec<String>>()
5345 .join("\r\n");
5346 let should_inline = component_function_body.lines().count() == 1;
5347 format!(
5348 "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
5349 if should_inline { "#[inline]\r\n" } else { "" },
5350 if component_args.accepts_text {
5352 "text: AzString"
5353 } else {
5354 ""
5355 },
5356 if function_args.is_empty() || !component_args.accepts_text {
5357 ""
5358 } else {
5359 ", "
5360 },
5361 function_args,
5362 component_function_body,
5363 )
5364}
5365
5366fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
5368 attr?.as_str().trim().parse::<f32>().ok()
5369}
5370
5371fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
5373 let nums: Vec<f32> = pts
5374 .split(|c: char| c == ',' || c.is_ascii_whitespace())
5375 .filter(|s| !s.is_empty())
5376 .filter_map(|s| s.parse::<f32>().ok())
5377 .collect();
5378 if nums.len() < 4 || !nums.len().is_multiple_of(2) {
5379 return None;
5380 }
5381 let mut elements = Vec::new();
5382 let points: Vec<azul_css::props::basic::SvgPoint> = nums
5383 .chunks_exact(2)
5384 .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
5385 .collect();
5386 for w in points.windows(2) {
5387 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5388 w[0], w[1],
5389 )));
5390 }
5391 if close && points.len() >= 2 {
5392 let first = points[0];
5393 let last = *points.last().unwrap();
5394 if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
5395 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5396 last, first,
5397 )));
5398 }
5399 }
5400 Some(crate::svg::SvgMultiPolygon {
5401 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5402 items: crate::svg::SvgPathElementVec::from_vec(elements),
5403 }]),
5404 })
5405}
5406
5407#[allow(clippy::too_many_lines)] fn apply_xml_node_attributes(
5418 node: &mut crate::dom::NodeData,
5419 xml_node: &XmlNode,
5420 component_name: &str,
5421 inside_svg: bool,
5422) {
5423 use crate::dom::{IdOrClass, NodeType, TabIndex};
5424
5425 if component_name == "img" {
5431 if let Some(src) = xml_node.attributes.get_key("src") {
5432 let width = xml_node
5433 .attributes
5434 .get_key("width")
5435 .and_then(|w| {
5436 w.as_str()
5437 .trim()
5438 .trim_end_matches("px")
5439 .trim()
5440 .parse::<usize>()
5441 .ok()
5442 })
5443 .unwrap_or(0);
5444 let height = xml_node
5445 .attributes
5446 .get_key("height")
5447 .and_then(|h| {
5448 h.as_str()
5449 .trim()
5450 .trim_end_matches("px")
5451 .trim()
5452 .parse::<usize>()
5453 .ok()
5454 })
5455 .unwrap_or(0);
5456 let image_ref = crate::resources::ImageRef::null_image(
5457 width,
5458 height,
5459 crate::resources::RawImageFormat::RGBA8,
5460 src.as_str().as_bytes().to_vec(),
5461 );
5462 node
5463 .set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
5464 }
5465 }
5466
5467 let mut ids_and_classes = Vec::new();
5469 if let Some(id_str) = xml_node.attributes.get_key("id") {
5470 for id in id_str.split_whitespace() {
5471 ids_and_classes.push(IdOrClass::Id(id.into()));
5472 }
5473 }
5474 if let Some(class_str) = xml_node.attributes.get_key("class") {
5475 for class in class_str.split_whitespace() {
5476 ids_and_classes.push(IdOrClass::Class(class.into()));
5477 }
5478 }
5479 if !ids_and_classes.is_empty() {
5480 node.set_ids_and_classes(ids_and_classes.into());
5481 }
5482
5483 if let Some(focusable) = xml_node
5485 .attributes
5486 .get_key("focusable")
5487 .and_then(|f| parse_bool(f.as_str()))
5488 {
5489 if focusable { node.set_tab_index(TabIndex::Auto) } else { node.set_tab_index(TabIndex::NoKeyboardFocus) }
5490 }
5491
5492 if let Some(tab_index) = xml_node
5494 .attributes
5495 .get_key("tabindex")
5496 .and_then(|val| val.parse::<isize>().ok())
5497 {
5498 match tab_index {
5499 0 => node.set_tab_index(TabIndex::Auto),
5500 i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(u32::try_from(i).unwrap_or(u32::MAX))),
5501 _ => node.set_tab_index(TabIndex::NoKeyboardFocus),
5502 }
5503 }
5504
5505 apply_cell_span_attributes(node, xml_node);
5507
5508 let dir_prop = xml_node.attributes.get_key("dir").and_then(|d| {
5512 let v = d.as_str().trim();
5513 if v.eq_ignore_ascii_case("rtl") {
5514 Some(azul_css::props::style::StyleDirection::Rtl)
5515 } else if v.eq_ignore_ascii_case("ltr") {
5516 Some(azul_css::props::style::StyleDirection::Ltr)
5517 } else {
5518 None
5519 }
5520 });
5521
5522 let style_attr = xml_node.attributes.get_key("style");
5524 if style_attr.is_some() || dir_prop.is_some() {
5525 use azul_css::dynamic_selector::CssPropertyWithConditions;
5526 let css_key_map = azul_css::props::property::get_css_key_map();
5527 let mut props: Vec<CssPropertyWithConditions> = Vec::new();
5528 if let Some(dir) = dir_prop {
5529 props.push(CssPropertyWithConditions::simple(
5530 azul_css::props::property::CssProperty::Direction(
5531 azul_css::css::CssPropertyValue::Exact(dir),
5532 ),
5533 ));
5534 }
5535 if let Some(style) = style_attr {
5536 let mut attributes = Vec::new();
5537 for s in style.as_str().split(';') {
5538 let mut s = s.split(':');
5539 let Some(key) = s.next() else {
5540 continue;
5541 };
5542 let Some(value) = s.next() else {
5543 continue;
5544 };
5545 drop(azul_css::parser2::parse_css_declaration(
5548 key.trim(),
5549 value.trim(),
5550 azul_css::parser2::ErrorLocationRange::default(),
5551 &css_key_map,
5552 &mut Vec::new(),
5553 &mut attributes,
5554 ));
5555 }
5556 props.extend(attributes.into_iter().filter_map(|s| match s {
5557 CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
5558 CssDeclaration::Dynamic(_) => None,
5559 }));
5560 }
5561 if !props.is_empty() {
5562 node.set_css_props(props.into());
5563 }
5564 }
5565
5566 let tag = component_name;
5568 let is_svg_shape = inside_svg
5569 && matches!(
5570 tag,
5571 "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
5572 );
5573
5574 if is_svg_shape {
5575 let clip = match tag {
5576 "path" => xml_node
5577 .attributes
5578 .get_key("d")
5579 .and_then(|d| crate::path_parser::parse_svg_path_d(d.as_str()).ok()),
5580 "circle" => {
5581 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5582 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5583 let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
5584 if r > 0.0 {
5585 Some(crate::svg::SvgMultiPolygon {
5586 rings: crate::svg::SvgPathVec::from_vec(vec![
5587 crate::path_parser::svg_circle_to_paths(cx, cy, r),
5588 ]),
5589 })
5590 } else {
5591 None
5592 }
5593 }
5594 "rect" => {
5595 let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
5596 let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
5597 let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
5598 let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
5599 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5600 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
5601 if w > 0.0 && h > 0.0 {
5602 Some(crate::svg::SvgMultiPolygon {
5603 rings: crate::svg::SvgPathVec::from_vec(vec![
5604 crate::path_parser::svg_rect_to_path(x, y, w, h, rx, ry),
5605 ]),
5606 })
5607 } else {
5608 None
5609 }
5610 }
5611 "ellipse" => {
5612 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5613 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5614 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5615 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
5616 if rx > 0.0 && ry > 0.0 {
5617 use azul_css::props::basic::{SvgCubicCurve, SvgPoint};
5619 const KAPPA: f32 = 0.552_284_8;
5620 let kx = rx * KAPPA;
5621 let ky = ry * KAPPA;
5622 let elements = vec![
5623 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5624 start: SvgPoint { x: cx, y: cy - ry },
5625 ctrl_1: SvgPoint {
5626 x: cx + kx,
5627 y: cy - ry,
5628 },
5629 ctrl_2: SvgPoint {
5630 x: cx + rx,
5631 y: cy - ky,
5632 },
5633 end: SvgPoint { x: cx + rx, y: cy },
5634 }),
5635 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5636 start: SvgPoint { x: cx + rx, y: cy },
5637 ctrl_1: SvgPoint {
5638 x: cx + rx,
5639 y: cy + ky,
5640 },
5641 ctrl_2: SvgPoint {
5642 x: cx + kx,
5643 y: cy + ry,
5644 },
5645 end: SvgPoint { x: cx, y: cy + ry },
5646 }),
5647 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5648 start: SvgPoint { x: cx, y: cy + ry },
5649 ctrl_1: SvgPoint {
5650 x: cx - kx,
5651 y: cy + ry,
5652 },
5653 ctrl_2: SvgPoint {
5654 x: cx - rx,
5655 y: cy + ky,
5656 },
5657 end: SvgPoint { x: cx - rx, y: cy },
5658 }),
5659 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5660 start: SvgPoint { x: cx - rx, y: cy },
5661 ctrl_1: SvgPoint {
5662 x: cx - rx,
5663 y: cy - ky,
5664 },
5665 ctrl_2: SvgPoint {
5666 x: cx - kx,
5667 y: cy - ry,
5668 },
5669 end: SvgPoint { x: cx, y: cy - ry },
5670 }),
5671 ];
5672 Some(crate::svg::SvgMultiPolygon {
5673 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5674 items: crate::svg::SvgPathElementVec::from_vec(elements),
5675 }]),
5676 })
5677 } else {
5678 None
5679 }
5680 }
5681 "line" => {
5682 let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
5683 let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
5684 let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
5685 let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
5686 Some(crate::svg::SvgMultiPolygon {
5687 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5688 items: crate::svg::SvgPathElementVec::from_vec(vec![
5689 crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5690 azul_css::props::basic::SvgPoint { x: x1, y: y1 },
5691 azul_css::props::basic::SvgPoint { x: x2, y: y2 },
5692 )),
5693 ]),
5694 }]),
5695 })
5696 }
5697 "polygon" | "polyline" => xml_node
5698 .attributes
5699 .get_key("points")
5700 .and_then(|pts| parse_svg_points(pts.as_str(), tag == "polygon")),
5701 _ => None,
5702 };
5703
5704 if let Some(mp) = clip {
5705 node.set_svg_data(crate::dom::SvgNodeData::Path(mp));
5706 }
5707 }
5708}
5709
5710fn apply_cell_span_attributes(node: &mut crate::dom::NodeData, xml_node: &XmlNode) {
5716 let mut spans = Vec::new();
5717 if let Some(n) = xml_node
5718 .attributes
5719 .get_key("colspan")
5720 .and_then(|v| v.as_str().trim().parse::<i32>().ok())
5721 {
5722 spans.push(crate::dom::AttributeType::ColSpan(n));
5723 }
5724 if let Some(n) = xml_node
5725 .attributes
5726 .get_key("rowspan")
5727 .and_then(|v| v.as_str().trim().parse::<i32>().ok())
5728 {
5729 spans.push(crate::dom::AttributeType::RowSpan(n));
5730 }
5731 if !spans.is_empty() {
5732 let mut v = node.attributes().clone().into_library_owned_vec();
5733 v.extend(spans);
5734 node.set_attributes(v.into());
5735 }
5736}
5737
5738#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
5744fn xml_node_to_dom_fast<'a>(
5745 xml_node: &'a XmlNode,
5746 component_map: &'a ComponentMap,
5747 inside_svg: bool,
5748 depth: usize,
5749) -> Result<Dom, RenderDomError> {
5750 use crate::dom::Dom;
5751
5752 let component_name = normalize_casing(&xml_node.node_type);
5753
5754 let node_type = tag_to_node_type(&component_name);
5756 let mut dom = Dom::create_node(node_type);
5757
5758 apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
5759
5760 let child_inside_svg = inside_svg || component_name == "svg";
5761
5762 if depth >= MAX_XML_NESTING_DEPTH {
5767 return Ok(dom);
5768 }
5769
5770 let mut children = Vec::new();
5772 for child in xml_node.children.as_ref() {
5773 match child {
5774 XmlNodeChild::Element(child_node) => {
5775 let child_dom =
5776 xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
5777 children.push(child_dom);
5778 }
5779 XmlNodeChild::Text(text) => {
5780 let text_dom = Dom::create_text(AzString::from(text.as_str()));
5781 children.push(text_dom);
5782 }
5783 }
5784 }
5785
5786 if !children.is_empty() {
5787 dom = dom.with_children(children.into());
5788 }
5789
5790 Ok(dom)
5791}
5792
5793#[derive(Debug)]
5796pub struct CompactDomBuilder {
5797 hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
5798 node_data: Vec<crate::dom::NodeData>,
5799 css: Vec<crate::dom::CssWithNodeId>,
5800 stack: Vec<(usize, Option<usize>)>,
5802}
5803
5804impl Default for CompactDomBuilder {
5805 fn default() -> Self {
5806 Self::new()
5807 }
5808}
5809
5810impl CompactDomBuilder {
5811 #[must_use] pub const fn new() -> Self {
5812 Self {
5813 hierarchy: Vec::new(),
5814 node_data: Vec::new(),
5815 css: Vec::new(),
5816 stack: Vec::new(),
5817 }
5818 }
5819
5820 #[must_use] pub fn with_capacity(cap: usize) -> Self {
5821 Self {
5822 hierarchy: Vec::with_capacity(cap),
5823 node_data: Vec::with_capacity(cap),
5824 css: Vec::new(),
5825 stack: Vec::new(),
5826 }
5827 }
5828
5829 pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
5831 use crate::id::NodeId;
5832 use crate::styled_dom::NodeHierarchyItem;
5833
5834 let idx = self.hierarchy.len();
5835
5836 let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
5838 NodeId::into_raw(&Some(NodeId::new(parent_idx)))
5839 } else {
5840 0 };
5842
5843 let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
5845 prev_child
5846 .map_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
5847 } else {
5848 0
5849 };
5850
5851 if let Some(&(_, Some(prev_idx))) = self.stack.last() {
5853 self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
5854 }
5855
5856 if let Some(parent) = self.stack.last_mut() {
5858 parent.1 = Some(idx);
5859 }
5860
5861 self.hierarchy.push(NodeHierarchyItem {
5863 parent: parent_raw,
5864 previous_sibling: prev_sibling_raw,
5865 next_sibling: 0, last_child: 0, });
5868 self.node_data.push(node_data);
5869
5870 self.stack.push((idx, None));
5872 }
5873
5874 pub fn close_node(&mut self) {
5876 use crate::id::NodeId;
5877
5878 if let Some((idx, last_child_idx)) = self.stack.pop() {
5879 self.hierarchy[idx].last_child = last_child_idx
5881 .map_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
5882 }
5883 }
5884
5885 pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
5887 self.open_node(node_data);
5888 self.close_node();
5889 }
5890
5891 pub fn add_css(&mut self, node_id: usize, css: Css) {
5893 self.css.push(crate::dom::CssWithNodeId { node_id, css });
5894 }
5895
5896 #[must_use] pub fn finish(self) -> crate::dom::FastDom {
5898 crate::dom::FastDom {
5899 node_hierarchy: self.hierarchy.into(),
5900 node_data: self.node_data.into(),
5901 css: self.css.into(),
5902 }
5903 }
5904}
5905
5906#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
5911fn xml_node_to_fast_dom<'a>(
5912 xml_node: &'a XmlNode,
5913 component_map: &'a ComponentMap,
5914 inside_svg: bool,
5915 builder: &mut CompactDomBuilder,
5916 depth: usize,
5917) -> Result<(), RenderDomError> {
5918 use crate::dom::NodeData;
5919
5920 let component_name = normalize_casing(&xml_node.node_type);
5921 let node_type = tag_to_node_type(&component_name);
5922 let mut node_data = NodeData::create_node(node_type);
5923
5924 apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
5925
5926 let child_inside_svg = inside_svg || component_name == "svg";
5927
5928 builder.open_node(node_data);
5930
5931 if depth < MAX_XML_NESTING_DEPTH {
5936 for child in xml_node.children.as_ref() {
5938 match child {
5939 XmlNodeChild::Element(child_node) => {
5940 xml_node_to_fast_dom(
5941 child_node,
5942 component_map,
5943 child_inside_svg,
5944 builder,
5945 depth + 1,
5946 )?;
5947 }
5948 XmlNodeChild::Text(text) => {
5949 builder.add_leaf(NodeData::create_text(AzString::from(text.as_str())));
5950 }
5951 }
5952 }
5953 }
5954
5955 builder.close_node();
5957
5958 Ok(())
5959}
5960
5961#[allow(clippy::result_large_err)] fn render_dom_from_body_node_fast<'a>(
5965 body_node: &'a XmlNode,
5966 mut global_css: Option<Css>,
5967 component_map: &'a ComponentMap,
5968 max_width: Option<f32>,
5969) -> Result<StyledDom, RenderDomError> {
5970 use crate::dom::{NodeData, NodeType};
5971
5972 let mut builder = CompactDomBuilder::new();
5973
5974 builder.open_node(NodeData::create_node(NodeType::Html));
5977 xml_node_to_fast_dom(body_node, component_map, false, &mut builder, 0)?;
5979 builder.close_node();
5981
5982 let mut combined_rules: Vec<CssRuleBlock> = Vec::new();
5984 if let Some(max_width) = max_width {
5985 let max_width_css =
5986 Css::from_string(format!("html {{ max-width: {max_width}px; }}").into());
5987 combined_rules.extend(max_width_css.rules.into_library_owned_vec());
5988 }
5989 if let Some(css) = global_css.take() {
5990 combined_rules.extend(css.rules.into_library_owned_vec());
5991 }
5992 let combined_css = Css::new(combined_rules);
5993
5994 let mut fast_dom = builder.finish();
5996 fast_dom.css = vec![crate::dom::CssWithNodeId {
5997 node_id: 0, css: combined_css,
5999 }]
6000 .into();
6001
6002 let styled = StyledDom::create_from_fast_dom(fast_dom);
6004 Ok(styled)
6005}
6006
6007fn set_stringified_attributes(
6010 dom_string: &mut String,
6011 xml_attributes: &XmlAttributeMap,
6012 filtered_xml_attributes: &ComponentArgumentVec,
6013 tabs: usize,
6014) {
6015 let t0 = String::from(" ").repeat(tabs);
6016 let t = String::from(" ").repeat(tabs + 1);
6017
6018 let _ = &t;
6021 for id in xml_attributes
6022 .get_key("id")
6023 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6024 .unwrap_or_default()
6025 {
6026 let _ = write!(
6027 dom_string,
6028 "\r\n{}.with_id(\"{}\")",
6029 t0,
6030 format_args_dynamic(id, filtered_xml_attributes)
6031 );
6032 }
6033
6034 for class in xml_attributes
6035 .get_key("class")
6036 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6037 .unwrap_or_default()
6038 {
6039 let _ = write!(
6040 dom_string,
6041 "\r\n{}.with_class(\"{}\")",
6042 t0,
6043 format_args_dynamic(class, filtered_xml_attributes)
6044 );
6045 }
6046
6047 if let Some(focusable) = xml_attributes
6048 .get_key("focusable")
6049 .map(|f| format_args_dynamic(f, filtered_xml_attributes))
6050 .and_then(|f| parse_bool(&f))
6051 {
6052 if focusable { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); } else { let _ = write!(dom_string,
6053 "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6054 ); }
6055 }
6056
6057 if let Some(tab_index) = xml_attributes
6058 .get_key("tabindex")
6059 .map(|val| format_args_dynamic(val, filtered_xml_attributes))
6060 .and_then(|val| val.parse::<isize>().ok())
6061 {
6062 match tab_index {
6063 0 => { let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)"); },
6064 i if i > 0 => { let _ = write!(dom_string,
6065 "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
6066 t, usize::try_from(i).unwrap_or(0)
6067 ); },
6068 _ => { let _ = write!(dom_string,
6069 "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6070 ); },
6071 }
6072 }
6073}
6074
6075#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6077pub enum DynamicItem {
6078 Var {
6080 name: String,
6081 format_spec: Option<String>,
6083 },
6084 Str(String),
6085}
6086
6087#[must_use] pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
6105 use self::DynamicItem::{Str, Var};
6106
6107 let input: Vec<char> = input.chars().collect();
6108 let input_chars_len = input.len();
6109
6110 let mut items = Vec::new();
6111 let mut current_idx = 0;
6112 let mut last_idx = 0;
6113
6114 while current_idx < input_chars_len {
6115 let c = input[current_idx];
6116 match c {
6117 '{' if input.get(current_idx + 1).copied() != Some('{') => {
6118 let mut start_offset = 1;
6120 let mut has_found_variable = false;
6121 while let Some(c) = input.get(current_idx + start_offset) {
6122 if c.is_whitespace() {
6123 break;
6124 }
6125 if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
6126 {
6127 start_offset += 1;
6128 has_found_variable = true;
6129 break;
6130 }
6131 start_offset += 1;
6132 }
6133
6134 if has_found_variable {
6138 if last_idx != current_idx {
6139 items.push(Str(input[last_idx..current_idx].iter().collect()));
6140 }
6141
6142 let var_content: String = input
6144 [(current_idx + 1)..(current_idx + start_offset - 1)]
6145 .iter()
6146 .collect();
6147 let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
6149 let name = var_content[..colon_pos].to_string();
6150 let spec = var_content[(colon_pos + 1)..].to_string();
6151 (name, Some(spec))
6152 } else {
6153 (var_content, None)
6154 };
6155 items.push(Var {
6156 name: var_name,
6157 format_spec,
6158 });
6159 current_idx += start_offset;
6160 last_idx = current_idx;
6161 } else {
6162 current_idx += start_offset;
6163 }
6164 }
6165 _ => {
6166 current_idx += 1;
6167 }
6168 }
6169 }
6170
6171 if current_idx != last_idx {
6172 items.push(Str(input[last_idx..].iter().collect()));
6173 }
6174
6175 for item in &mut items {
6176 if let Str(s) = item {
6178 *s = s.replace("{{", "{").replace("}}", "}");
6179 }
6180 }
6181
6182 items
6183}
6184
6185fn combine_and_replace_dynamic_items(
6192 input: &[DynamicItem],
6193 variables: &ComponentArgumentVec,
6194) -> String {
6195 let mut s = String::new();
6196
6197 for item in input {
6198 match item {
6199 DynamicItem::Var { name, format_spec } => {
6200 let variable_name = normalize_casing(name.trim());
6201 if let Some(resolved_var) = variables
6202 .iter()
6203 .find(|s| s.name.as_str() == variable_name)
6204 .map(|q| &q.arg_type) {
6205 s.push_str(resolved_var);
6207 } else {
6208 s.push('{');
6209 s.push_str(name);
6210 if let Some(spec) = format_spec {
6211 s.push(':');
6212 s.push_str(spec);
6213 }
6214 s.push('}');
6215 }
6216 }
6217 DynamicItem::Str(dynamic_str) => {
6218 s.push_str(dynamic_str);
6219 }
6220 }
6221 }
6222
6223 s
6224}
6225
6226#[must_use] pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
6245 let dynamic_str_items = split_dynamic_string(input);
6246 combine_and_replace_dynamic_items(&dynamic_str_items, variables)
6247}
6248
6249fn decode_numeric_entity(entity: &str) -> Option<char> {
6253 let num = entity.strip_prefix('#')?;
6254 let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
6255 u32::from_str_radix(hex, 16).ok()?
6256 } else {
6257 num.parse::<u32>().ok()?
6258 };
6259 char::from_u32(code)
6260}
6261
6262fn decode_entities(input: &str) -> String {
6269 const MAX_ENTITY_BODY: usize = 12;
6272
6273 let mut out = String::with_capacity(input.len());
6274 let bytes = input.as_bytes();
6275 let mut i = 0;
6276 while i < input.len() {
6277 if bytes[i] == b'&' {
6278 if let Some(semi_rel) = input[i + 1..].find(';') {
6279 if semi_rel <= MAX_ENTITY_BODY {
6280 let body = &input[i + 1..i + 1 + semi_rel];
6281 let end = i + 1 + semi_rel; if body.eq_ignore_ascii_case("nbsp") {
6284 out.push_str(&input[i..=end]);
6285 i = end + 1;
6286 continue;
6287 }
6288 let decoded = match body {
6289 "lt" => Some('<'),
6290 "gt" => Some('>'),
6291 "amp" => Some('&'),
6292 "quot" => Some('"'),
6293 "apos" => Some('\''),
6294 _ => decode_numeric_entity(body),
6295 };
6296 if let Some(c) = decoded {
6297 out.push(c);
6298 i = end + 1;
6299 continue;
6300 }
6301 }
6302 }
6303 out.push('&');
6305 i += 1;
6306 } else {
6307 let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
6309 out.push(ch);
6310 i += ch.len_utf8();
6311 }
6312 }
6313 out
6314}
6315
6316#[must_use] pub fn prepare_string(input: &str) -> String {
6318 const SPACE: &str = " ";
6319 const RETURN: &str = "\n";
6320
6321 let input = input.trim();
6322
6323 if input.is_empty() {
6324 return String::new();
6325 }
6326
6327 let input = decode_entities(input);
6335
6336 let input_len = input.len();
6337 let mut final_lines: Vec<String> = Vec::new();
6338 let mut last_line_was_empty = false;
6339
6340 for line in input.lines() {
6341 let line = line.trim();
6342 let line = line.replace(" ", " ");
6343 let current_line_is_empty = line.is_empty();
6344
6345 if !current_line_is_empty {
6346 if last_line_was_empty {
6347 final_lines.push(format!("{RETURN}{line}"));
6348 } else {
6349 final_lines.push(line.to_string());
6350 }
6351 }
6352
6353 last_line_was_empty = current_line_is_empty;
6354 }
6355
6356 let mut target = String::with_capacity(input_len);
6357 for (line_idx, line) in final_lines.iter().enumerate() {
6358 if !(line.starts_with(RETURN) || line_idx == 0) {
6363 target.push_str(SPACE);
6364 }
6365 target.push_str(line);
6366 }
6367 target
6368}
6369
6370#[must_use] pub fn parse_bool(input: &str) -> Option<bool> {
6372 match input {
6373 "true" => Some(true),
6374 "false" => Some(false),
6375 _ => None,
6376 }
6377}
6378
6379#[derive(Debug, Clone)]
6380pub struct CssMatcher {
6381 path: Vec<CssPathSelector>,
6382 indices_in_parent: Vec<usize>,
6383 children_length: Vec<usize>,
6384}
6385
6386impl CssMatcher {
6387 fn get_hash(&self) -> u64 {
6388 use core::hash::Hash;
6389
6390 use core::hash::Hasher;
6391
6392 let mut hasher = crate::hash::DefaultHasher::new();
6393 for p in &self.path {
6394 p.hash(&mut hasher);
6395 }
6396 hasher.finish()
6397 }
6398}
6399
6400impl CssMatcher {
6401 fn matches(&self, path: &CssPath) -> bool {
6402 use azul_css::css::CssPathSelector::*;
6403
6404 use crate::style::{CssGroupIterator, CssGroupSplitReason};
6405
6406 if self.path.is_empty() {
6407 return false;
6408 }
6409 if path.selectors.as_ref().is_empty() {
6410 return false;
6411 }
6412
6413 let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
6415 path_groups.reverse();
6416
6417 if path_groups.is_empty() {
6418 return false;
6419 }
6420 let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
6421 self_groups.reverse();
6422 if self_groups.is_empty() {
6423 return false;
6424 }
6425
6426 if self.indices_in_parent.len() != self_groups.len() {
6427 return false;
6428 }
6429 if self.children_length.len() != self_groups.len() {
6430 return false;
6431 }
6432
6433 let mut cur_selfgroup_scan = 0;
6447 let mut cur_pathgroup_scan = 0;
6448 let mut valid = false;
6449 let mut path_group = path_groups[cur_pathgroup_scan].clone();
6450
6451 while cur_selfgroup_scan < self_groups.len() {
6452 let mut advance = None;
6453
6454 for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
6456 let gm = group_matches(
6457 &path_group.0,
6458 &self_groups[cur_selfgroup_scan + id].0,
6459 self.indices_in_parent[cur_selfgroup_scan + id],
6460 self.children_length[cur_selfgroup_scan + id],
6461 );
6462
6463 if gm {
6464 advance = Some(id);
6467 break;
6468 }
6469 }
6470
6471 match advance {
6472 Some(n) => {
6473 if cur_pathgroup_scan == path_groups.len() - 1 {
6476 return cur_selfgroup_scan + n == self_groups.len() - 1;
6478 }
6479 cur_pathgroup_scan += 1;
6480 cur_selfgroup_scan += n;
6481 path_group = path_groups[cur_pathgroup_scan].clone();
6482 }
6483 None => return false, }
6485 }
6486
6487 cur_pathgroup_scan == path_groups.len() - 1
6489 }
6490}
6491
6492fn group_matches(
6495 a: &[&CssPathSelector],
6496 b: &[&CssPathSelector],
6497 idx_in_parent: usize,
6498 parent_children: usize,
6499) -> bool {
6500 use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::{Global, PseudoSelector, Type, Class, Id}};
6501
6502 for selector in a {
6503 match selector {
6504 Global |
6506PseudoSelector(CssPathPseudoSelector::Hover | CssPathPseudoSelector::Active |
6507CssPathPseudoSelector::Focus) => {}
6508
6509 Type(tag) => {
6510 if !b.iter().any(|t| **t == Type(*tag)) {
6511 return false;
6512 }
6513 }
6514 Class(class) => {
6515 if !b.iter().any(|t| **t == Class(class.clone())) {
6516 return false;
6517 }
6518 }
6519 Id(id) => {
6520 if !b.iter().any(|t| **t == Id(id.clone())) {
6521 return false;
6522 }
6523 }
6524 PseudoSelector(CssPathPseudoSelector::First) => {
6525 if idx_in_parent != 0 {
6526 return false;
6527 }
6528 }
6529 PseudoSelector(CssPathPseudoSelector::Last) => {
6530 if idx_in_parent != parent_children.saturating_sub(1) {
6531 return false;
6532 }
6533 }
6534 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
6535 if idx_in_parent != *i as usize {
6536 return false;
6537 }
6538 }
6539 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
6540 if !idx_in_parent.is_multiple_of(2) {
6541 return false;
6542 }
6543 }
6544 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
6545 if idx_in_parent.is_multiple_of(2) {
6546 return false;
6547 }
6548 }
6549 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
6550 if !idx_in_parent.saturating_sub(p.offset as usize).is_multiple_of(p.pattern_repeat as usize)
6551 {
6552 return false;
6553 }
6554 }
6555
6556 _ => return false, }
6558 }
6559
6560 true
6561}
6562
6563struct CssBlock {
6564 ending: Option<CssPathPseudoSelector>,
6565 block: CssRuleBlock,
6566}
6567
6568#[allow(clippy::result_large_err)] pub fn compile_body_node_to_rust_code<'a>(
6573 body_node: &'a XmlNode,
6574 component_map: &'a ComponentMap,
6575 extra_blocks: &mut VecContents,
6576 css_blocks: &mut BTreeMap<String, String>,
6577 css: &Css,
6578 mut matcher: CssMatcher,
6579) -> Result<String, CompileError> {
6580 use azul_css::css::CssDeclaration;
6581
6582 let t = "";
6583 let t2 = " ";
6584 let mut dom_string = String::from("Dom::create_body()");
6585 let node_type = CssPathSelector::Type(NodeTypeTag::Body);
6586 matcher.path.push(node_type);
6587
6588 let ids = body_node
6589 .attributes
6590 .get_key("id")
6591 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6592 .unwrap_or_default();
6593 matcher.path.extend(
6594 ids.into_iter()
6595 .map(|id| CssPathSelector::Id(id.to_string().into())),
6596 );
6597 let classes = body_node
6598 .attributes
6599 .get_key("class")
6600 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6601 .unwrap_or_default();
6602 matcher.path.extend(
6603 classes
6604 .into_iter()
6605 .map(|class| CssPathSelector::Class(class.to_string().into())),
6606 );
6607
6608 let matcher_hash = matcher.get_hash();
6609 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6610 if !css_blocks_for_this_node.is_empty() {
6611 for css_block in &css_blocks_for_this_node {
6617 for declaration in css_block.block.declarations.as_ref() {
6618 let prop = match declaration {
6619 CssDeclaration::Static(s) => s,
6620 CssDeclaration::Dynamic(d) => &d.default_value,
6621 };
6622 extra_blocks.insert_from_css_property(prop);
6623 }
6624 }
6625
6626 let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6627 if !inline_css.is_empty() {
6628 let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6629 let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6630 }
6631 let _ = (&mut *css_blocks, matcher_hash); }
6633
6634 if !body_node.children.as_ref().is_empty() {
6635 use azul_css::codegen::format::GetHash;
6636 let children_hash = body_node.children.as_ref().get_hash();
6637 dom_string.push_str("\r\n.with_children(vec![\r\n");
6638
6639 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
6640 match child {
6641 XmlNodeChild::Element(child_node) => {
6642 let mut matcher = matcher.clone();
6643 matcher.path.push(CssPathSelector::Children);
6644 matcher.indices_in_parent.push(child_idx);
6645 matcher.children_length.push(body_node.children.len());
6646
6647 let _ = write!(dom_string,
6648 "{}{},\r\n",
6649 t,
6650 compile_node_to_rust_code_inner(
6651 child_node,
6652 component_map,
6653 1,
6654 extra_blocks,
6655 css_blocks,
6656 css,
6657 matcher,
6658 )?
6659 );
6660 }
6661 XmlNodeChild::Text(text) => {
6662 let text = text.trim();
6663 if !text.is_empty() {
6664 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6665 let _ = write!(dom_string,
6666 "{t}Dom::create_text(\"{escaped}\"),\r\n"
6667 );
6668 }
6669 }
6670 }
6671 }
6672 let _ = write!(dom_string, "\r\n{t}])");
6673 }
6674
6675 let dom_string = dom_string.trim();
6676 Ok(dom_string.to_string())
6677}
6678
6679fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
6685 fn decls_of(block: &CssBlock) -> Vec<String> {
6686 block
6687 .block
6688 .declarations
6689 .as_ref()
6690 .iter()
6691 .map(|d| {
6692 let prop = match d {
6693 CssDeclaration::Static(s) => s,
6694 CssDeclaration::Dynamic(dy) => &dy.default_value,
6695 };
6696 format!("{}: {};", prop.key(), prop.value())
6697 })
6698 .collect()
6699 }
6700
6701 let mut normal: Vec<String> = Vec::new();
6702 let mut pseudo: Vec<String> = Vec::new();
6703 for block in blocks {
6704 let pseudo_sel = match block.ending {
6705 Some(CssPathPseudoSelector::Hover) => Some(":hover"),
6706 Some(CssPathPseudoSelector::Active) => Some(":active"),
6707 Some(CssPathPseudoSelector::Focus) => Some(":focus"),
6708 _ => None,
6709 };
6710 match pseudo_sel {
6711 None => normal.extend(decls_of(block)),
6712 Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
6713 }
6714 }
6715
6716 let mut parts = normal;
6717 parts.extend(pseudo);
6718 parts.join(" ")
6719}
6720
6721fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
6722 let mut blocks = Vec::new();
6723
6724 for css_block in css.rules.as_ref() {
6725 if matcher.matches(&css_block.path) {
6726 let mut ending = None;
6727
6728 if let Some(CssPathSelector::PseudoSelector(p)) =
6729 css_block.path.selectors.as_ref().last()
6730 {
6731 ending = Some(p.clone());
6732 }
6733
6734 blocks.push(CssBlock {
6735 ending,
6736 block: css_block.clone(),
6737 });
6738 }
6739 }
6740
6741 blocks
6742}
6743
6744fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
6745 use self::DynamicItem::{Var, Str};
6746 if input.is_empty() {
6747 String::from("AzString::from_const_str(\"\")")
6748 } else if input.len() == 1 {
6749 match &input[0] {
6751 Var { name, format_spec } => {
6752 let var_name = normalize_casing(name.trim());
6753 if let Some(spec) = format_spec {
6754 format!("format!(\"{{:{spec}}}\", {var_name}).into()")
6755 } else {
6756 var_name
6757 }
6758 }
6759 Str(s) => format!("AzString::from_const_str(\"{s}\")"),
6760 }
6761 } else {
6762 let mut formatted_str = String::from("format!(\"");
6764 let mut variables = Vec::new();
6765 for item in input {
6766 match item {
6767 Var { name, format_spec } => {
6768 let variable_name = normalize_casing(name.trim());
6769 if let Some(spec) = format_spec {
6770 let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
6771 } else {
6772 let _ = write!(formatted_str, "{{{variable_name}}}");
6773 }
6774 variables.push(variable_name.clone());
6775 }
6776 Str(s) => {
6777 let s = s.replace('"', "\\\"");
6778 formatted_str.push_str(&s);
6779 }
6780 }
6781 }
6782
6783 formatted_str.push('\"');
6784 if !variables.is_empty() {
6785 formatted_str.push_str(", ");
6786 }
6787
6788 formatted_str.push_str(&variables.join(", "));
6789 formatted_str.push_str(").into()");
6790 formatted_str
6791 }
6792}
6793
6794fn format_args_for_rust_code(input: &str) -> String {
6795 let dynamic_str_items = split_dynamic_string(input);
6796 compile_and_format_dynamic_items(&dynamic_str_items)
6797}
6798
6799#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
6803#[allow(clippy::too_many_lines)] fn compile_node_to_rust_code_inner(
6805 node: &XmlNode,
6806 component_map: &ComponentMap,
6807 tabs: usize,
6808 extra_blocks: &mut VecContents,
6809 css_blocks: &mut BTreeMap<String, String>,
6810 css: &Css,
6811 mut matcher: CssMatcher,
6812) -> Result<String, CompileError> {
6813 use azul_css::css::CssDeclaration;
6814
6815 let t = String::from(" ").repeat(tabs - 1);
6816 let t2 = String::from(" ").repeat(tabs);
6817
6818 let component_name = normalize_casing(&node.node_type);
6819
6820 let node_type_tag = tag_to_node_type_tag(&component_name);
6822 let node_type = CssPathSelector::Type(node_type_tag);
6823
6824 let ctor = analyze_node_ctor(&component_name, node);
6833 let mut dom_string = ctor.render_rust().map_or_else(|| {
6834 let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
6835 format!("{t2}Dom::create_node(NodeType::{tag})")
6836 }, |expr| format!("{t2}{expr}"));
6837
6838 matcher.path.push(node_type);
6839 let ids = node
6840 .attributes
6841 .get_key("id")
6842 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6843 .unwrap_or_default();
6844
6845 matcher.path.extend(
6846 ids.into_iter()
6847 .map(|id| CssPathSelector::Id(id.to_string().into())),
6848 );
6849
6850 let classes = node
6851 .attributes
6852 .get_key("class")
6853 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6854 .unwrap_or_default();
6855
6856 matcher.path.extend(
6857 classes
6858 .into_iter()
6859 .map(|class| CssPathSelector::Class(class.to_string().into())),
6860 );
6861
6862 let matcher_hash = matcher.get_hash();
6863 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
6864 if !css_blocks_for_this_node.is_empty() {
6865 for css_block in &css_blocks_for_this_node {
6871 for declaration in css_block.block.declarations.as_ref() {
6872 let prop = match declaration {
6873 CssDeclaration::Static(s) => s,
6874 CssDeclaration::Dynamic(d) => &d.default_value,
6875 };
6876 extra_blocks.insert_from_css_property(prop);
6877 }
6878 }
6879
6880 let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
6881 if !inline_css.is_empty() {
6882 let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
6883 let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
6884 }
6885 let _ = (&mut *css_blocks, matcher_hash); }
6887
6888 set_stringified_attributes(
6889 &mut dom_string,
6890 &node.attributes,
6891 &ComponentArgumentVec::new(),
6892 tabs,
6893 );
6894
6895 let mut caption_skipped = false;
6898 let mut children_string = node
6899 .children
6900 .as_ref()
6901 .iter()
6902 .enumerate()
6903 .filter_map(|(child_idx, c)| match c {
6904 XmlNodeChild::Element(child_node) => {
6905 if ctor.skip_caption()
6906 && !caption_skipped
6907 && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
6908 {
6909 caption_skipped = true;
6910 return None;
6911 }
6912 let mut matcher = matcher.clone();
6913 matcher.path.push(CssPathSelector::Children);
6914 matcher.indices_in_parent.push(child_idx);
6915 matcher.children_length.push(node.children.len());
6916
6917 Some(compile_node_to_rust_code_inner(
6918 child_node,
6919 component_map,
6920 tabs + 1,
6921 extra_blocks,
6922 css_blocks,
6923 css,
6924 matcher,
6925 ))
6926 }
6927 XmlNodeChild::Text(text) => {
6928 if ctor.consumes_text() {
6929 return None;
6930 }
6931 let text = text.trim();
6932 if text.is_empty() {
6933 None
6934 } else {
6935 let t2 = String::from(" ").repeat(tabs);
6936 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
6937 Some(Ok(format!(
6938 "{t2}Dom::create_text(\"{escaped}\")"
6939 )))
6940 }
6941 }
6942 })
6943 .collect::<Result<Vec<_>, _>>()?
6944 .join(",\r\n");
6945
6946 if !children_string.is_empty() {
6947 let _ = write!(dom_string,
6948 "\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
6949 );
6950 }
6951
6952 Ok(dom_string)
6953}
6954
6955const SAFE_CONTAINER_TAGS: &[&str] = &[
6971 "Abbr", "Acronym", "Address", "Article", "Aside", "B", "Bdi", "Bdo", "Big",
6976 "BlockQuote", "Body", "Br", "Caption", "Cite", "Code", "ColGroup", "Dd",
6977 "Del", "Dfn", "Dir", "Div", "Dl", "Dt", "Em", "Embed", "FigCaption",
6978 "Figure", "Footer", "H1", "H2", "H3", "H4", "H5", "H6", "Head", "Header",
6979 "Hr", "Html", "I", "Ins", "Kbd", "Li", "Link", "Main", "Map", "Mark",
6980 "Meta", "Nav", "Object", "Ol", "P", "Pre", "Q", "Rp", "Rt", "Rtc", "Ruby",
6981 "S", "Samp", "Script", "Section", "Small", "Span", "Strong", "Style", "Sub",
6982 "Sup", "Svg", "TBody", "Td", "TFoot", "Th", "THead", "Title", "Tr", "U",
6983 "Ul", "Var", "Wbr",
6984];
6985
6986fn safe_container_tag(tag_dbg: &str) -> &'static str {
6989 SAFE_CONTAINER_TAGS.iter().copied().find(|t| *t == tag_dbg).unwrap_or("Div")
6990}
6991
6992#[derive(Debug, Clone)]
7019enum CtorArg {
7020 Str(String),
7022 Aria(String),
7024 Float(f32),
7026 OptSome(String),
7028 OptNone,
7030}
7031
7032enum NodeCtor {
7034 Plain,
7036 Semantic {
7038 suffix: String,
7041 args: Vec<CtorArg>,
7042 consumes_text: bool,
7045 skip_caption: bool,
7048 },
7049}
7050
7051fn cap_first(tag: &str) -> String {
7055 let mut c = tag.chars();
7056 c.next().map_or_else(String::new, |f| f.to_uppercase().collect::<String>() + c.as_str())
7057}
7058
7059fn camel_to_snake(s: &str) -> String {
7063 let chars: Vec<char> = s.chars().collect();
7064 let mut out = String::new();
7065 for (i, &ch) in chars.iter().enumerate() {
7066 if ch.is_ascii_uppercase() && i > 0 {
7067 let prev = chars[i - 1];
7068 let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
7069 if prev.is_ascii_lowercase()
7070 || prev.is_ascii_digit()
7071 || (prev.is_ascii_uppercase() && next_lower)
7072 {
7073 out.push('_');
7074 }
7075 }
7076 out.extend(ch.to_lowercase());
7077 }
7078 out
7079}
7080
7081fn esc_lit(s: &str) -> String {
7083 s.replace('\\', "\\\\").replace('"', "\\\"")
7084}
7085
7086fn fmt_f32_lit(f: f32) -> String {
7088 let s = format!("{f}");
7089 if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
7090 s
7091 } else {
7092 format!("{s}.0")
7093 }
7094}
7095
7096fn node_direct_text(node: &XmlNode) -> String {
7098 node.children
7099 .as_ref()
7100 .iter()
7101 .filter_map(|c| match c {
7102 XmlNodeChild::Text(t) => {
7103 let t = t.trim();
7104 if t.is_empty() { None } else { Some(t.to_string()) }
7105 }
7106 XmlNodeChild::Element(_) => None,
7107 })
7108 .collect::<Vec<_>>()
7109 .join(" ")
7110}
7111
7112fn node_aria_label(node: &XmlNode) -> Option<String> {
7114 node.attributes.get_key("aria-label").and_then(|v| {
7115 let v = v.as_str().trim();
7116 if v.is_empty() { None } else { Some(v.to_string()) }
7117 })
7118}
7119
7120fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
7122 node.attributes
7123 .get_key(key).map_or_else(|| default.to_string(), |v| v.as_str().to_string())
7124}
7125
7126fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
7128 node.attributes
7129 .get_key(key)
7130 .and_then(|v| v.as_str().trim().parse::<f32>().ok())
7131 .unwrap_or(default)
7132}
7133
7134fn first_caption_text(node: &XmlNode) -> Option<String> {
7136 node.children.as_ref().iter().find_map(|c| match c {
7137 XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
7138 let t = e.get_text_content();
7139 let t = t.trim();
7140 if t.is_empty() { None } else { Some(t.to_string()) }
7141 }
7142 _ => None,
7143 })
7144}
7145
7146const WITH_TEXT_TAGS: &[&str] = &[
7148 "acronym", "b", "bdi", "bdo", "big", "blockquote", "cite", "code", "del",
7149 "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "ins", "kbd", "li",
7150 "mark", "p", "pre", "rp", "rt", "s", "samp", "small", "span", "strong",
7151 "style", "sub", "sup", "td", "th", "title", "u", "var",
7152];
7153
7154#[allow(clippy::too_many_lines)] fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
7157 fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
7159 NodeCtor::Semantic {
7160 suffix: suffix.into(),
7161 args,
7162 consumes_text,
7163 skip_caption: false,
7164 }
7165 }
7166
7167 let aria = node_aria_label(node);
7168 let has_aria = aria.is_some();
7169 let label = aria.unwrap_or_default();
7170 let pure_text = node.has_only_text_children();
7173 let text = node_direct_text(node);
7174 let has_text = !text.is_empty();
7175 let cap = cap_first(tag);
7176
7177 if WITH_TEXT_TAGS.contains(&tag) {
7179 if pure_text && has_text {
7180 return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
7181 }
7182 return NodeCtor::Plain;
7183 }
7184
7185 match tag {
7186 "details" | "form" | "fieldset" | "legend" | "menu" | "output"
7188 | "datalist" | "canvas" | "audio" | "video" | "area" => {
7189 if has_aria {
7190 sem(cap, vec![CtorArg::Aria(label)], false)
7191 } else {
7192 sem(format!("{cap}NoA11y"), vec![], false)
7193 }
7194 }
7195 "summary" => {
7197 if pure_text && has_text {
7198 if has_aria {
7199 sem("SummaryWithText", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7200 } else {
7201 sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
7202 }
7203 } else if has_aria {
7204 sem("Summary", vec![CtorArg::Aria(label)], false)
7205 } else {
7206 sem("SummaryNoA11y", vec![], false)
7207 }
7208 }
7209
7210 "button" => {
7212 if has_aria {
7213 sem("Button", vec![CtorArg::Str(text), CtorArg::Aria(label)], true)
7214 } else {
7215 sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
7216 }
7217 }
7218 "a" => {
7219 let href = node_attr_or(node, "href", "");
7220 if has_aria {
7221 sem("A", vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)], true)
7222 } else {
7223 let lbl = if has_text { CtorArg::OptSome(text) } else { CtorArg::OptNone };
7224 sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
7225 }
7226 }
7227 "label" => {
7228 let for_id = node_attr_or(node, "for", "");
7229 if has_aria {
7230 sem("Label", vec![CtorArg::Str(for_id), CtorArg::Str(text), CtorArg::Aria(label)], true)
7231 } else {
7232 sem("LabelNoA11y", vec![CtorArg::Str(for_id), CtorArg::Str(text)], true)
7233 }
7234 }
7235 "input" => {
7236 let ty = node_attr_or(node, "type", "text");
7237 let name = node_attr_or(node, "name", "");
7238 if has_aria {
7239 sem("Input", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7240 } else {
7241 sem("InputNoA11y", vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)], false)
7242 }
7243 }
7244 "textarea" => {
7245 let name = node_attr_or(node, "name", "");
7246 if has_aria {
7247 sem("Textarea", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7248 } else {
7249 sem("TextareaNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7250 }
7251 }
7252 "select" => {
7253 let name = node_attr_or(node, "name", "");
7254 if has_aria {
7255 sem("Select", vec![CtorArg::Str(name), CtorArg::Str(label.clone()), CtorArg::Aria(label)], false)
7256 } else {
7257 sem("SelectNoA11y", vec![CtorArg::Str(name), CtorArg::Str(label)], false)
7258 }
7259 }
7260 "option" => {
7261 let value = node_attr_or(node, "value", "");
7262 if has_aria {
7263 sem("Option", vec![CtorArg::Str(value), CtorArg::Str(text), CtorArg::Aria(label)], true)
7264 } else {
7265 sem("OptionNoA11y", vec![CtorArg::Str(value), CtorArg::Str(text)], true)
7266 }
7267 }
7268 "optgroup" => {
7269 let lbl = node_attr_or(node, "label", "");
7270 if has_aria {
7271 sem("Optgroup", vec![CtorArg::Str(lbl), CtorArg::Aria(label)], false)
7272 } else {
7273 sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
7274 }
7275 }
7276 "table" => {
7277 if has_aria {
7278 let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
7281 NodeCtor::Semantic {
7282 suffix: "Table".to_string(),
7283 args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
7284 consumes_text: false,
7285 skip_caption: true,
7286 }
7287 } else {
7288 sem("TableNoA11y", vec![], false)
7289 }
7290 }
7291
7292 "progress" => sem(
7294 "ProgressNoA11y",
7295 vec![
7296 CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7297 CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7298 ],
7299 false,
7300 ),
7301 "meter" => sem(
7302 "MeterNoA11y",
7303 vec![
7304 CtorArg::Float(node_attr_f32(node, "value", 0.0)),
7305 CtorArg::Float(node_attr_f32(node, "min", 0.0)),
7306 CtorArg::Float(node_attr_f32(node, "max", 1.0)),
7307 ],
7308 false,
7309 ),
7310 "dialog" => sem("DialogNoA11y", vec![], false),
7311
7312 _ => NodeCtor::Plain,
7313 }
7314}
7315
7316impl CtorArg {
7317 fn render_rust(&self) -> String {
7320 match self {
7321 Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
7322 Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
7323 Self::Float(f) => fmt_f32_lit(*f),
7324 Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
7325 Self::OptNone => "OptionString::None".to_string(),
7326 }
7327 }
7328 fn render_c(&self) -> String {
7329 match self {
7330 Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
7331 Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
7332 Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7333 Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
7334 Self::OptNone => "AzOptionString_none()".to_string(),
7335 }
7336 }
7337 fn render_cpp(&self) -> String {
7338 match self {
7339 Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
7340 Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
7341 Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
7342 Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
7343 Self::OptNone => "OptionString::none()".to_string(),
7344 }
7345 }
7346 fn render_python(&self) -> String {
7347 match self {
7348 Self::Str(s) => format!("\"{}\"", esc_lit(s)),
7349 Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
7350 Self::Float(f) => fmt_f32_lit(*f),
7351 Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
7352 Self::OptNone => "azul.OptionString.none()".to_string(),
7353 }
7354 }
7355}
7356
7357impl NodeCtor {
7358 const fn consumes_text(&self) -> bool {
7359 matches!(self, Self::Semantic { consumes_text: true, .. })
7360 }
7361 const fn skip_caption(&self) -> bool {
7362 matches!(self, Self::Semantic { skip_caption: true, .. })
7363 }
7364 fn render_rust(&self) -> Option<String> {
7366 match self {
7367 Self::Plain => None,
7368 Self::Semantic { suffix, args, .. } => Some(format!(
7369 "Dom::create_{}({})",
7370 camel_to_snake(suffix),
7371 args.iter().map(CtorArg::render_rust).collect::<Vec<_>>().join(", ")
7372 )),
7373 }
7374 }
7375 fn render_c(&self) -> Option<String> {
7377 match self {
7378 Self::Plain => None,
7379 Self::Semantic { suffix, args, .. } => Some(format!(
7380 "AzDom_create{}({})",
7381 suffix,
7382 args.iter().map(CtorArg::render_c).collect::<Vec<_>>().join(", ")
7383 )),
7384 }
7385 }
7386 fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
7388 match self {
7389 Self::Plain => None,
7390 Self::Semantic { suffix, args, .. } => {
7391 let snake = camel_to_snake(suffix);
7392 let (prefix, rendered) = match target {
7393 CompileTarget::Cpp => (
7394 format!("Dom::create_{snake}"),
7395 args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
7396 ),
7397 CompileTarget::Python => (
7398 format!("azul.Dom.create_{snake}"),
7399 args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
7400 ),
7401 _ => return None,
7402 };
7403 Some(format!("{}({})", prefix, rendered.join(", ")))
7404 }
7405 }
7406 }
7407}
7408
7409struct FluentSyntax {
7412 target: CompileTarget,
7413 create_node: fn(&str) -> String,
7415 create_text: fn(&str) -> String,
7417 with_css: fn(&str) -> String,
7419 with_class: fn(&str) -> String,
7421 with_id: fn(&str) -> String,
7423 with_child: fn(&str) -> String,
7425}
7426
7427const CPP_SYNTAX: FluentSyntax = FluentSyntax {
7428 target: CompileTarget::Cpp,
7429 create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
7433 create_text: |s| alloc::format!("Dom::create_text(String(\"{s}\"))"),
7434 with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
7435 with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
7436 with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
7437 with_child: |c| alloc::format!(".with_child({c})"),
7438};
7439
7440const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
7441 target: CompileTarget::Python,
7442 create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
7444 create_text: |s| alloc::format!("azul.Dom.create_text(\"{s}\")"),
7445 with_css: |s| alloc::format!(".with_css(\"{s}\")"),
7446 with_class: |s| alloc::format!(".with_class(\"{s}\")"),
7447 with_id: |s| alloc::format!(".with_id(\"{s}\")"),
7448 with_child: |c| alloc::format!(".with_child({c})"),
7449};
7450
7451#[allow(clippy::result_large_err)] #[allow(clippy::only_used_in_recursion)]
7456fn compile_node_fluent(
7457 node: &XmlNode,
7458 syntax: &FluentSyntax,
7459 component_map: &ComponentMap,
7460 css: &Css,
7461 mut matcher: CssMatcher,
7462) -> Result<String, CompileError> {
7463 use azul_css::css::CssDeclaration;
7464
7465 let component_name = normalize_casing(&node.node_type);
7466 let node_type_tag = tag_to_node_type_tag(&component_name);
7467 let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7468
7469 let ctor = analyze_node_ctor(&component_name, node);
7476 let mut s = ctor.render_fluent(&syntax.target).map_or_else(|| (syntax.create_node)(safe_container_tag(&tag_dbg)), |expr| expr);
7477
7478 matcher.path.push(CssPathSelector::Type(node_type_tag));
7479 let ids: Vec<String> = node.attributes.get_key("id")
7480 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7481 .unwrap_or_default();
7482 matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7483 let classes: Vec<String> = node.attributes.get_key("class")
7484 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7485 .unwrap_or_default();
7486 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7487
7488 let blocks = get_css_blocks(css, &matcher);
7490 if !blocks.is_empty() {
7491 let inline_css = css_blocks_to_inline_string(&blocks);
7492 if !inline_css.is_empty() {
7493 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7494 s.push_str(&(syntax.with_css)(&esc));
7495 }
7496 }
7497 for id in &ids {
7498 s.push_str(&(syntax.with_id)(&id.replace('\\', "\\\\").replace('"', "\\\"")));
7499 }
7500 for class in &classes {
7501 s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7502 }
7503
7504 let mut caption_skipped = false;
7507 for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7508 match child {
7509 XmlNodeChild::Element(child_node) => {
7510 if ctor.skip_caption()
7511 && !caption_skipped
7512 && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7513 {
7514 caption_skipped = true;
7515 continue;
7516 }
7517 let mut m = matcher.clone();
7518 m.path.push(CssPathSelector::Children);
7519 m.indices_in_parent.push(child_idx);
7520 m.children_length.push(node.children.len());
7521 let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7522 s.push_str(&(syntax.with_child)(&child_src));
7523 }
7524 XmlNodeChild::Text(text) => {
7525 if ctor.consumes_text() {
7526 continue;
7527 }
7528 let text = text.trim();
7529 if !text.is_empty() {
7530 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7531 s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7532 }
7533 }
7534 }
7535 }
7536
7537 Ok(s)
7538}
7539
7540#[allow(clippy::result_large_err)] fn compile_body_fluent<'a>(
7543 body_node: &'a XmlNode,
7544 syntax: &FluentSyntax,
7545 component_map: &'a ComponentMap,
7546 css: &Css,
7547 mut matcher: CssMatcher,
7548) -> Result<String, CompileError> {
7549 let mut s = (syntax.create_node)("Body");
7550 matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7551 let classes: Vec<String> = body_node.attributes.get_key("class")
7552 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7553 .unwrap_or_default();
7554 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7555
7556 let blocks = get_css_blocks(css, &matcher);
7557 if !blocks.is_empty() {
7558 let inline_css = css_blocks_to_inline_string(&blocks);
7559 if !inline_css.is_empty() {
7560 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7561 s.push_str(&(syntax.with_css)(&esc));
7562 }
7563 }
7564 for class in &classes {
7565 s.push_str(&(syntax.with_class)(&class.replace('\\', "\\\\").replace('"', "\\\"")));
7566 }
7567
7568 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7569 match child {
7570 XmlNodeChild::Element(child_node) => {
7571 let mut m = matcher.clone();
7572 m.path.push(CssPathSelector::Children);
7573 m.indices_in_parent.push(child_idx);
7574 m.children_length.push(body_node.children.len());
7575 let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
7576 s.push_str(&(syntax.with_child)(&child_src));
7577 }
7578 XmlNodeChild::Text(text) => {
7579 let text = text.trim();
7580 if !text.is_empty() {
7581 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7582 s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
7583 }
7584 }
7585 }
7586 }
7587 Ok(s)
7588}
7589
7590#[allow(clippy::result_large_err)] fn parse_page_style_and_body(
7594 root_nodes: &[XmlNodeChild],
7595) -> Result<(Css, &XmlNode), CompileError> {
7596 let html_node = get_html_node(root_nodes)?;
7597 let body_node = get_body_node(html_node.children.as_ref())?;
7598 let mut global_style = Css::empty();
7599 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
7600 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
7601 let text = style_node.get_text_content();
7602 if !text.is_empty() {
7603 global_style = azul_css::parser2::new_from_str(&text).0;
7604 }
7605 }
7606 }
7607 global_style.sort_by_specificity();
7608 Ok((global_style, body_node))
7609}
7610
7611fn body_matcher(body_node: &XmlNode) -> CssMatcher {
7612 CssMatcher {
7613 path: Vec::new(),
7614 indices_in_parent: vec![0],
7615 children_length: vec![body_node.children.as_ref().len()],
7616 }
7617}
7618
7619#[allow(clippy::result_large_err)] pub fn str_to_cpp_code<'a>(
7625 root_nodes: &'a [XmlNodeChild],
7626 component_map: &'a ComponentMap,
7627) -> Result<String, CompileError> {
7628 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7629 let render = compile_body_fluent(body_node, &CPP_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7630 Ok(alloc::format!(
7631 "// Auto-generated UI source code (C++). Build:\n\
7632 // clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
7633 #include \"azul20.hpp\"\n\
7634 using namespace azul;\n\n\
7635 struct Data {{}};\n\n\
7636 AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n \
7637 return {render};\n}}\n\n\
7638 int main() {{\n \
7639 RefAny data = RefAny::create(Data{{}});\n \
7640 WindowCreateOptions window = WindowCreateOptions::create(render);\n \
7641 App app = App::create(std::move(data), AppConfig::default_());\n \
7642 app.run(std::move(window));\n \
7643 return 0;\n}}\n"
7644 ))
7645}
7646
7647#[allow(clippy::result_large_err)] pub fn str_to_python_code<'a>(
7653 root_nodes: &'a [XmlNodeChild],
7654 component_map: &'a ComponentMap,
7655) -> Result<String, CompileError> {
7656 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7657 let render = compile_body_fluent(body_node, &PYTHON_SYNTAX, component_map, &global_style, body_matcher(body_node))?;
7658 Ok(alloc::format!(
7659 "# Auto-generated UI source code (Python). Run: python3 main.py\n\
7660 import azul\n\n\
7661 class Data:\n pass\n\n\
7662 def render(data, info):\n return (\n {}\n )\n\n\
7663 def main():\n \
7664 app = azul.App.create(Data(), azul.AppConfig.create())\n \
7665 window = azul.WindowCreateOptions.create(render)\n \
7666 app.run(window)\n\n\
7667 if __name__ == \"__main__\":\n main()\n",
7668 render.replace("\r\n", "\n ")
7669 ))
7670}
7671
7672fn c_creator_suffix(tag_dbg: &str) -> String {
7683 let mut chars = tag_dbg.chars();
7684 chars.next().map_or_else(|| "Div".to_string(), |first| {
7685 let rest: String = chars.as_str().to_lowercase();
7686 alloc::format!("{first}{rest}")
7687 })
7688}
7689
7690#[allow(clippy::result_large_err)] fn compile_node_c(
7692 node: &XmlNode,
7693 component_map: &ComponentMap,
7694 css: &Css,
7695 mut matcher: CssMatcher,
7696 counter: &mut usize,
7697 out: &mut String,
7698) -> Result<String, CompileError> {
7699 let _ = component_map;
7700 let component_name = normalize_casing(&node.node_type);
7701 let node_type_tag = tag_to_node_type_tag(&component_name);
7702 let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
7703
7704 let var = alloc::format!("n{}", *counter);
7705 *counter += 1;
7706 let ctor = analyze_node_ctor(&component_name, node);
7707 match ctor.render_c() {
7708 Some(expr) => { let _ = writeln!(out, " AzDom {var} = {expr};"); },
7709 None => { let _ = writeln!(out,
7710 " AzDom {} = AzDom_create{}();",
7711 var,
7712 c_creator_suffix(safe_container_tag(&tag_dbg))
7713 ); },
7714 }
7715
7716 matcher.path.push(CssPathSelector::Type(node_type_tag));
7717 let ids: Vec<String> = node.attributes.get_key("id")
7718 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7719 .unwrap_or_default();
7720 matcher.path.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
7721 let classes: Vec<String> = node.attributes.get_key("class")
7722 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7723 .unwrap_or_default();
7724 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7725
7726 let blocks = get_css_blocks(css, &matcher);
7727 if !blocks.is_empty() {
7728 let inline_css = css_blocks_to_inline_string(&blocks);
7729 if !inline_css.is_empty() {
7730 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7731 let _ = writeln!(out, " {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
7732 }
7733 }
7734 for id in &ids {
7735 let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
7736 let _ = writeln!(out, " {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
7737 }
7738 for class in &classes {
7739 let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
7740 let _ = writeln!(out, " {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));");
7741 }
7742
7743 let mut caption_skipped = false;
7744 for (child_idx, child) in node.children.as_ref().iter().enumerate() {
7745 match child {
7746 XmlNodeChild::Element(child_node) => {
7747 if ctor.skip_caption()
7748 && !caption_skipped
7749 && child_node.node_type.as_str().eq_ignore_ascii_case("caption")
7750 {
7751 caption_skipped = true;
7752 continue;
7753 }
7754 let mut m = matcher.clone();
7755 m.path.push(CssPathSelector::Children);
7756 m.indices_in_parent.push(child_idx);
7757 m.children_length.push(node.children.len());
7758 let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
7759 let _ = writeln!(out, " AzDom_addChild(&{var}, {child_var});");
7760 }
7761 XmlNodeChild::Text(text) => {
7762 if ctor.consumes_text() {
7763 continue;
7764 }
7765 let text = text.trim();
7766 if !text.is_empty() {
7767 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7768 let _ = writeln!(out,
7769 " AzDom_addChild(&{var}, AzDom_createText(AZ_STR(\"{esc}\")));"
7770 );
7771 }
7772 }
7773 }
7774 }
7775 Ok(var)
7776}
7777
7778#[allow(clippy::result_large_err)] pub fn str_to_c_code<'a>(
7784 root_nodes: &'a [XmlNodeChild],
7785 component_map: &'a ComponentMap,
7786) -> Result<String, CompileError> {
7787 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
7788 let mut body = String::new();
7789 let mut counter = 0usize;
7790
7791 let root = alloc::format!("n{counter}");
7793 counter += 1;
7794 let _ = writeln!(body, " AzDom {root} = AzDom_createBody();");
7795
7796 let mut matcher = body_matcher(body_node);
7797 matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
7798 let classes: Vec<String> = body_node.attributes.get_key("class")
7799 .map(|v| v.split_whitespace().map(alloc::string::ToString::to_string).collect())
7800 .unwrap_or_default();
7801 matcher.path.extend(classes.iter().map(|c| CssPathSelector::Class(c.clone().into())));
7802 let blocks = get_css_blocks(&global_style, &matcher);
7803 if !blocks.is_empty() {
7804 let inline_css = css_blocks_to_inline_string(&blocks);
7805 if !inline_css.is_empty() {
7806 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7807 let _ = writeln!(body, " {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));");
7808 }
7809 }
7810 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7811 match child {
7812 XmlNodeChild::Element(child_node) => {
7813 let mut m = matcher.clone();
7814 m.path.push(CssPathSelector::Children);
7815 m.indices_in_parent.push(child_idx);
7816 m.children_length.push(body_node.children.len());
7817 let child_var = compile_node_c(child_node, component_map, &global_style, m, &mut counter, &mut body)?;
7818 let _ = writeln!(body, " AzDom_addChild(&{root}, {child_var});");
7819 }
7820 XmlNodeChild::Text(text) => {
7821 let text = text.trim();
7822 if !text.is_empty() {
7823 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
7824 let _ = writeln!(body,
7825 " AzDom_addChild(&{root}, AzDom_createText(AZ_STR(\"{esc}\")));"
7826 );
7827 }
7828 }
7829 }
7830 }
7831
7832 Ok(alloc::format!(
7833 "/* Auto-generated UI source code (C). Build:\n\
7834 * clang -I <azul>/target/codegen main.c -lazul\n */\n\
7835 #include \"azul.h\"\n\
7836 #include <string.h>\n\
7837 #define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
7838 AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
7839 {body} return {root};\n}}\n\n\
7840 int main(void) {{\n \
7841 AzString data_type = AZ_STR(\"Data\");\n \
7842 AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n \
7843 AzApp app = AzApp_create(data, AzAppConfig_create());\n \
7844 AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n \
7845 AzApp_run(&app, window);\n \
7846 AzApp_delete(&app);\n \
7847 return 0;\n}}\n"
7848 ))
7849}
7850
7851#[cfg(test)]
7852mod tests {
7853 use super::*;
7854 use crate::dom::{Dom, NodeType};
7855
7856 #[test]
7857 fn test_inline_span_parsing() {
7858 let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
7862
7863 let expected_dom = Dom::create_p().with_children(
7873 vec![
7874 Dom::create_text("Text before "),
7875 Dom::create_node(NodeType::Span)
7876 .with_children(vec![Dom::create_text("inline text")].into()),
7877 Dom::create_text(" text after."),
7878 ]
7879 .into(),
7880 );
7881
7882 assert_eq!(expected_dom.children.as_ref().len(), 3);
7884
7885 match &expected_dom.children.as_ref()[1].root.node_type {
7887 NodeType::Span => {}
7888 other => panic!("Expected Span, got {:?}", other),
7889 }
7890
7891 assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
7893
7894 println!("Test passed: Inline span parsing structure is correct");
7895 }
7896
7897 #[test]
7898 fn test_xml_node_structure() {
7899 let node = XmlNode {
7903 node_type: "p".into(),
7904 attributes: XmlAttributeMap {
7905 inner: StringPairVec::from_const_slice(&[]),
7906 },
7907 children: vec![
7908 XmlNodeChild::Text("Before ".into()),
7909 XmlNodeChild::Element(XmlNode {
7910 node_type: "span".into(),
7911 children: vec![XmlNodeChild::Text("inline".into())].into(),
7912 ..Default::default()
7913 }),
7914 XmlNodeChild::Text(" after".into()),
7915 ]
7916 .into(),
7917 };
7918
7919 assert_eq!(node.children.as_ref().len(), 3);
7921 assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
7922 assert_eq!(
7923 node.children.as_ref()[1]
7924 .as_element()
7925 .unwrap()
7926 .node_type
7927 .as_str(),
7928 "span"
7929 );
7930 assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
7931
7932 let span = node.children.as_ref()[1].as_element().unwrap();
7934 assert_eq!(span.children.as_ref().len(), 1);
7935 assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
7936
7937 println!("Test passed: XmlNode structure preserves text nodes correctly");
7938 }
7939
7940 #[test]
7941 fn test_img_tag_becomes_image_node_with_src_tag() {
7942 use crate::resources::DecodedImage;
7947 use crate::window::{AzStringPair, StringPairVec};
7948
7949 let img_node = XmlNode {
7950 node_type: "img".into(),
7951 attributes: XmlAttributeMap::from(StringPairVec::from_vec(alloc::vec![
7952 AzStringPair {
7953 key: "src".into(),
7954 value: "cat.jpg".into()
7955 },
7956 AzStringPair {
7957 key: "width".into(),
7958 value: "300".into()
7959 },
7960 AzStringPair {
7961 key: "height".into(),
7962 value: "169".into()
7963 },
7964 ])),
7965 children: Vec::new().into(),
7966 };
7967
7968 let component_map = ComponentMap::default();
7969 let dom = xml_node_to_dom_fast(&img_node, &component_map, false, 0)
7970 .expect("xml_node_to_dom_fast for <img> should succeed");
7971
7972 match dom.root.get_node_type() {
7973 NodeType::Image(image_ref) => match image_ref.as_ref().get_data() {
7974 DecodedImage::NullImage {
7975 tag, width, height, ..
7976 } => {
7977 assert_eq!(
7978 core::str::from_utf8(tag).unwrap(),
7979 "cat.jpg",
7980 "image tag must carry the src string"
7981 );
7982 assert_eq!(*width, 300, "width attribute should set intrinsic width");
7983 assert_eq!(*height, 169, "height attribute should set intrinsic height");
7984 }
7985 other => panic!("expected NullImage carrying the src tag, got {:?}", other),
7986 },
7987 other => panic!("expected NodeType::Image for <img>, got {:?}", other),
7988 }
7989
7990 println!("Test passed: <img src=\"cat.jpg\"> -> NodeType::Image tagged \"cat.jpg\"");
7991 }
7992
7993 #[test]
7994 fn test_tag_to_node_type_img_is_image() {
7995 match tag_to_node_type("img") {
7997 NodeType::Image(_) => {}
7998 other => panic!("tag_to_node_type(\"img\") should be Image, got {:?}", other),
7999 }
8000 }
8001
8002 fn nested_divs(depth: usize) -> XmlNode {
8004 let mut node = XmlNode {
8005 node_type: "div".into(),
8006 ..Default::default()
8007 };
8008 for _ in 0..depth {
8009 node = XmlNode {
8010 node_type: "div".into(),
8011 children: vec![XmlNodeChild::Element(node)].into(),
8012 ..Default::default()
8013 };
8014 }
8015 node
8016 }
8017
8018 #[test]
8023 fn extract_css_urls_unicode_import_no_panic() {
8024 let mut res = Vec::new();
8025 Xml::extract_css_urls("İ@import 'x'", &mut res);
8026 assert_eq!(res.len(), 1, "should find the one @import target");
8027 assert_eq!(res[0].url.as_str(), "x");
8028 }
8029
8030 #[test]
8032 fn extract_css_urls_is_case_insensitive() {
8033 let mut res = Vec::new();
8034 Xml::extract_css_urls("body { background: URL(http://e.com/a.png); }", &mut res);
8035 assert_eq!(res.len(), 1);
8036 assert_eq!(res[0].url.as_str(), "http://e.com/a.png");
8037
8038 let mut res2 = Vec::new();
8039 Xml::extract_css_urls("@IMPORT \"theme.css\";", &mut res2);
8040 assert_eq!(res2.len(), 1);
8041 assert_eq!(res2[0].url.as_str(), "theme.css");
8042 }
8043
8044 #[test]
8047 fn scan_external_resources_deep_nesting_ok() {
8048 let xml = Xml {
8049 root: vec![XmlNodeChild::Element(nested_divs(2000))].into(),
8050 };
8051 drop(xml.scan_external_resources());
8053 }
8054
8055 #[test]
8059 fn xml_node_to_dom_fast_deep_nesting_ok() {
8060 let deep = nested_divs(2000);
8061 let component_map = ComponentMap::default();
8062 let dom = xml_node_to_dom_fast(&deep, &component_map, false, 0);
8063 assert!(dom.is_ok(), "deep DOM build must not overflow the stack");
8064
8065 let mut builder = CompactDomBuilder::new();
8066 let fast = xml_node_to_fast_dom(&deep, &component_map, false, &mut builder, 0);
8067 assert!(fast.is_ok(), "deep FastDom build must not overflow the stack");
8068 }
8069
8070 #[test]
8074 fn component_field_type_parse_depth_capped() {
8075 let deep = format!("{}Bool{}", "Option<".repeat(4000), ">".repeat(4000));
8076 assert!(
8077 ComponentFieldType::parse(&deep).is_none(),
8078 "over-deep type string must be rejected, not overflow"
8079 );
8080
8081 let shallow = format!("{}Bool{}", "Option<".repeat(8), ">".repeat(8));
8082 assert!(
8083 ComponentFieldType::parse(&shallow).is_some(),
8084 "ordinary nesting must still parse"
8085 );
8086 }
8087
8088 #[test]
8091 fn prepare_string_entity_decoding() {
8092 assert_eq!(prepare_string("a & b"), "a & b");
8093 assert_eq!(prepare_string("&lt;"), "<");
8095 assert_eq!(prepare_string(""hi""), "\"hi\"");
8096 assert_eq!(prepare_string("AB"), "AB");
8097 assert_eq!(prepare_string("A"), "A");
8098 assert_eq!(prepare_string("<tag>"), "<tag>");
8100 }
8101}
8102
8103#[cfg(test)]
8104#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
8105mod autotest_generated {
8106 use super::*;
8107 use crate::dom::{NodeData, NodeType};
8108 use azul_css::css::{CssNthChildPattern, CssNthChildSelector};
8109
8110 fn attrs(kv: &[(&str, &str)]) -> XmlAttributeMap {
8113 XmlAttributeMap::from(StringPairVec::from_vec(
8114 kv.iter()
8115 .map(|(k, v)| AzStringPair {
8116 key: AzString::from(*k),
8117 value: AzString::from(*v),
8118 })
8119 .collect::<Vec<_>>(),
8120 ))
8121 }
8122
8123 fn node(tag: &str, kv: &[(&str, &str)], children: Vec<XmlNodeChild>) -> XmlNode {
8124 XmlNode {
8125 node_type: tag.into(),
8126 attributes: attrs(kv),
8127 children: children.into(),
8128 }
8129 }
8130
8131 fn txt(s: &str) -> XmlNodeChild {
8132 XmlNodeChild::Text(AzString::from(s))
8133 }
8134
8135 fn elem(n: XmlNode) -> XmlNodeChild {
8136 XmlNodeChild::Element(n)
8137 }
8138
8139 fn doc(css: &str, body_children: Vec<XmlNodeChild>) -> Vec<XmlNodeChild> {
8141 let style = node("style", &[], vec![txt(css)]);
8142 let head = node("head", &[], vec![elem(style)]);
8143 let body = node("body", &[], body_children);
8144 vec![elem(node("html", &[], vec![elem(head), elem(body)]))]
8145 }
8146
8147 fn no_args() -> ComponentArgumentVec {
8148 ComponentArgumentVec::from_const_slice(&[])
8149 }
8150
8151 fn dm(name: &str, fields: Vec<ComponentDataField>) -> ComponentDataModel {
8152 ComponentDataModel {
8153 name: AzString::from(name),
8154 description: AzString::from_const_str(""),
8155 fields: fields.into(),
8156 }
8157 }
8158
8159 fn user_def(css: &str, fields: Vec<ComponentDataField>) -> ComponentDef {
8160 ComponentDef {
8161 id: ComponentId::new("mylib", "widget"),
8162 display_name: AzString::from_const_str("Widget"),
8163 description: AzString::from_const_str(""),
8164 css: AzString::from(css),
8165 source: ComponentSource::UserDefined,
8166 data_model: dm("WidgetData", fields),
8167 render_fn: user_defined_render_fn,
8168 compile_fn: user_defined_compile_fn,
8169 render_fn_source: None.into(),
8170 compile_fn_source: None.into(),
8171 }
8172 }
8173
8174 const LONG: usize = 200_000;
8177
8178 #[test]
8183 fn extract_url_value_empty_and_whitespace() {
8184 assert_eq!(Xml::extract_url_value(""), None);
8185 assert_eq!(Xml::extract_url_value(" "), None);
8186 assert_eq!(Xml::extract_url_value("\t\n\r "), None);
8187 }
8188
8189 #[test]
8190 fn extract_url_value_valid_minimal() {
8191 assert_eq!(
8192 Xml::extract_url_value("a.png)"),
8193 Some("a.png".to_string()),
8194 "unquoted url terminated by ')'"
8195 );
8196 assert_eq!(
8197 Xml::extract_url_value("\"a.png\")"),
8198 Some("a.png".to_string()),
8199 "double-quoted url"
8200 );
8201 assert_eq!(
8202 Xml::extract_url_value("'a.png')"),
8203 Some("a.png".to_string()),
8204 "single-quoted url"
8205 );
8206 }
8207
8208 #[test]
8209 fn extract_url_value_leading_trailing_junk_is_trimmed() {
8210 assert_eq!(
8212 Xml::extract_url_value(" a.png )tail"),
8213 Some("a.png".to_string())
8214 );
8215 }
8216
8217 #[test]
8218 fn extract_url_value_garbage_returns_none() {
8219 assert_eq!(Xml::extract_url_value("\"unterminated"), None);
8221 assert_eq!(Xml::extract_url_value("'unterminated"), None);
8222 assert_eq!(Xml::extract_url_value("no-closing-paren"), None);
8223 assert_eq!(Xml::extract_url_value("\u{0}\u{1}\u{7f}"), None);
8224 }
8225
8226 #[test]
8227 fn extract_url_value_boundary_numbers() {
8228 for s in [
8229 "0)",
8230 "-0)",
8231 "9223372036854775807)",
8232 "-9223372036854775808)",
8233 "NaN)",
8234 "inf)",
8235 "1e400)",
8236 ] {
8237 let got = Xml::extract_url_value(s);
8238 assert!(got.is_some(), "numeric-looking url {s:?} is still a url");
8239 }
8240 assert_eq!(Xml::extract_url_value("0)"), Some("0".to_string()));
8241 }
8242
8243 #[test]
8244 fn extract_url_value_unicode_no_panic() {
8245 assert_eq!(
8247 Xml::extract_url_value("\u{1F600}\u{0301})"),
8248 Some("\u{1F600}\u{0301}".to_string())
8249 );
8250 assert_eq!(Xml::extract_url_value("\"\u{130}\")"), Some("\u{130}".to_string()));
8251 assert_eq!(Xml::extract_url_value("\u{1F600}"), None);
8252 }
8253
8254 #[test]
8255 fn extract_url_value_extremely_long_terminates() {
8256 let s = "a".repeat(LONG);
8257 assert_eq!(Xml::extract_url_value(&s), None, "no ')' anywhere => None");
8258 let s2 = format!("{})", "b".repeat(LONG));
8259 assert_eq!(Xml::extract_url_value(&s2).map(|v| v.len()), Some(LONG));
8260 }
8261
8262 #[test]
8263 fn extract_url_value_nested_brackets_no_stack_overflow() {
8264 let s = "(".repeat(10_000);
8266 assert_eq!(Xml::extract_url_value(&s), None);
8267 let s2 = format!("{}{}", "(".repeat(10_000), ")");
8268 assert_eq!(Xml::extract_url_value(&s2), Some("(".repeat(10_000)));
8269 }
8270
8271 #[test]
8276 fn extract_quoted_string_empty_whitespace_garbage() {
8277 assert_eq!(Xml::extract_quoted_string(""), None);
8278 assert_eq!(Xml::extract_quoted_string(" "), None);
8279 assert_eq!(Xml::extract_quoted_string("\t\n"), None);
8280 assert_eq!(Xml::extract_quoted_string("bare"), None);
8281 assert_eq!(Xml::extract_quoted_string(" \"x\""), None);
8283 }
8284
8285 #[test]
8286 fn extract_quoted_string_valid_minimal_and_empty_quotes() {
8287 assert_eq!(Xml::extract_quoted_string("\"x\""), Some("x".to_string()));
8288 assert_eq!(Xml::extract_quoted_string("'x'"), Some("x".to_string()));
8289 assert_eq!(Xml::extract_quoted_string("\"\""), Some(String::new()));
8291 assert_eq!(Xml::extract_quoted_string("''"), Some(String::new()));
8292 }
8293
8294 #[test]
8295 fn extract_quoted_string_unterminated_is_none() {
8296 assert_eq!(Xml::extract_quoted_string("\"abc"), None);
8297 assert_eq!(Xml::extract_quoted_string("'abc"), None);
8298 assert_eq!(Xml::extract_quoted_string("\"abc'"), None);
8300 }
8301
8302 #[test]
8303 fn extract_quoted_string_boundary_numbers_and_unicode() {
8304 assert_eq!(Xml::extract_quoted_string("\"0\""), Some("0".to_string()));
8305 assert_eq!(Xml::extract_quoted_string("\"-0\""), Some("-0".to_string()));
8306 assert_eq!(Xml::extract_quoted_string("\"NaN\""), Some("NaN".to_string()));
8307 assert_eq!(
8308 Xml::extract_quoted_string("\"\u{1F600}\u{0301}\""),
8309 Some("\u{1F600}\u{0301}".to_string())
8310 );
8311 }
8312
8313 #[test]
8314 fn extract_quoted_string_extremely_long_terminates() {
8315 let unterminated = format!("\"{}", "x".repeat(LONG));
8316 assert_eq!(Xml::extract_quoted_string(&unterminated), None);
8317 let terminated = format!("\"{}\"", "x".repeat(LONG));
8318 assert_eq!(
8319 Xml::extract_quoted_string(&terminated).map(|s| s.len()),
8320 Some(LONG)
8321 );
8322 }
8323
8324 #[test]
8329 fn parse_srcset_empty_and_whitespace_yield_no_urls() {
8330 assert!(Xml::parse_srcset("").is_empty());
8331 assert!(Xml::parse_srcset(" ").is_empty());
8332 assert!(Xml::parse_srcset("\t\n").is_empty());
8333 assert!(Xml::parse_srcset(",,,").is_empty(), "all-empty entries dropped");
8334 }
8335
8336 #[test]
8337 fn parse_srcset_valid_minimal() {
8338 assert_eq!(
8339 Xml::parse_srcset("a.png 1x, b.png 2x"),
8340 vec!["a.png".to_string(), "b.png".to_string()]
8341 );
8342 assert_eq!(Xml::parse_srcset("a.png"), vec!["a.png".to_string()]);
8344 }
8345
8346 #[test]
8347 fn parse_srcset_garbage_and_boundary_numbers() {
8348 assert_eq!(Xml::parse_srcset("0, -0, NaN"), vec!["0", "-0", "NaN"]);
8349 assert_eq!(Xml::parse_srcset("\u{0}\u{7f} 1x"), vec!["\u{0}\u{7f}".to_string()]);
8351 }
8352
8353 #[test]
8354 fn parse_srcset_unicode_no_panic() {
8355 assert_eq!(
8356 Xml::parse_srcset("\u{1F600}.png 1x, \u{130}.png 2x"),
8357 vec!["\u{1F600}.png".to_string(), "\u{130}.png".to_string()]
8358 );
8359 }
8360
8361 #[test]
8362 fn parse_srcset_extremely_long_terminates() {
8363 let s = "a.png 1x,".repeat(20_000);
8364 assert_eq!(Xml::parse_srcset(&s).len(), 20_000);
8365 let one_huge = "a".repeat(LONG);
8366 assert_eq!(Xml::parse_srcset(&one_huge).len(), 1);
8367 }
8368
8369 #[test]
8374 fn looks_like_resource_edges() {
8375 assert!(!Xml::looks_like_resource(""));
8376 assert!(!Xml::looks_like_resource(" "));
8377 assert!(!Xml::looks_like_resource("/about"));
8378 assert!(Xml::looks_like_resource("/a.PNG"), "case-insensitive");
8379 assert!(Xml::looks_like_resource("x.pdf"));
8380 assert!(!Xml::looks_like_resource("x.png?v=1"));
8383 assert!(!Xml::looks_like_resource(&"a".repeat(LONG)));
8384 }
8385
8386 #[test]
8387 fn guess_kind_from_url_covers_every_bucket() {
8388 use ExternalResourceKind::*;
8389 assert_eq!(Xml::guess_kind_from_url(""), Unknown);
8390 assert_eq!(Xml::guess_kind_from_url("a.PNG"), Image);
8391 assert_eq!(Xml::guess_kind_from_url("a.woff2"), Font);
8392 assert_eq!(Xml::guess_kind_from_url("a.css"), Stylesheet);
8393 assert_eq!(Xml::guess_kind_from_url("a.mjs"), Script);
8394 assert_eq!(Xml::guess_kind_from_url("a.webm"), Video);
8395 assert_eq!(Xml::guess_kind_from_url("a.flac"), Audio);
8396 assert_eq!(Xml::guess_kind_from_url("a.ico"), Icon);
8397 assert_eq!(Xml::guess_kind_from_url("a.png?v=1"), Image);
8399 assert_eq!(Xml::guess_kind_from_url("\u{1F600}"), Unknown);
8400 }
8401
8402 #[test]
8403 fn guess_mime_from_url_empty_and_garbage() {
8404 assert_eq!(Xml::guess_mime_from_url("", ""), None);
8405 assert_eq!(Xml::guess_mime_from_url(" ", ""), None);
8406 assert_eq!(Xml::guess_mime_from_url("\u{0}\u{7f}", ""), None);
8407 assert_eq!(Xml::guess_mime_from_url("\u{1F600}", ""), None);
8408 }
8409
8410 #[test]
8411 fn guess_mime_from_url_valid_minimal_and_category_fallback() {
8412 let m = Xml::guess_mime_from_url("a.PNG", "").expect("png is a known extension");
8413 assert_eq!(m.inner.as_str(), "image/png");
8414 let m = Xml::guess_mime_from_url("a.png?v=1", "").expect("query string stripped");
8415 assert_eq!(m.inner.as_str(), "image/png");
8416 let m = Xml::guess_mime_from_url("/no-ext", "image").expect("category fallback");
8418 assert_eq!(m.inner.as_str(), "image/*");
8419 assert_eq!(Xml::guess_mime_from_url("/no-ext", "bogus"), None);
8421 }
8422
8423 #[test]
8424 fn guess_mime_from_url_boundary_numbers_and_long() {
8425 assert_eq!(Xml::guess_mime_from_url("0", ""), None);
8426 assert_eq!(Xml::guess_mime_from_url("-0", ""), None);
8427 assert_eq!(Xml::guess_mime_from_url("NaN", ""), None);
8428 assert_eq!(Xml::guess_mime_from_url("inf", ""), None);
8429 let long = format!("{}.png", "a".repeat(LONG));
8430 assert_eq!(
8431 Xml::guess_mime_from_url(&long, "").map(|m| m.inner.as_str().to_string()),
8432 Some("image/png".to_string())
8433 );
8434 }
8435
8436 #[test]
8441 fn extract_css_urls_empty_and_garbage_no_panic() {
8442 let mut v = Vec::new();
8443 Xml::extract_css_urls("", &mut v);
8444 Xml::extract_css_urls(" ", &mut v);
8445 Xml::extract_css_urls("\u{0}\u{7f}\u{1F600}", &mut v);
8446 Xml::extract_css_urls("url(", &mut v);
8447 Xml::extract_css_urls("@import", &mut v);
8448 Xml::extract_css_urls("@import url(", &mut v);
8449 assert!(v.is_empty(), "no well-formed url in any of those inputs");
8450 }
8451
8452 #[test]
8453 fn extract_css_urls_valid_minimal() {
8454 let mut v = Vec::new();
8455 Xml::extract_css_urls("a { background: url('x.png'); }", &mut v);
8456 assert_eq!(v.len(), 1);
8457 assert_eq!(v[0].url.as_str(), "x.png");
8458 assert_eq!(v[0].kind, ExternalResourceKind::Image);
8459 assert_eq!(v[0].source_attribute.as_str(), "url()");
8460 }
8461
8462 #[test]
8463 fn extract_css_urls_import_is_tagged_as_stylesheet() {
8464 let mut v = Vec::new();
8465 Xml::extract_css_urls("@import url(theme.css);", &mut v);
8466 assert_eq!(v.len(), 1);
8467 assert_eq!(v[0].url.as_str(), "theme.css");
8468 assert_eq!(v[0].kind, ExternalResourceKind::Stylesheet);
8469 assert_eq!(v[0].source_attribute.as_str(), "@import");
8470 }
8471
8472 #[test]
8473 fn extract_css_urls_multibyte_before_url_no_panic() {
8474 let mut v = Vec::new();
8476 Xml::extract_css_urls("\u{130}\u{1F600} URL(\"a.css\") \u{0301}", &mut v);
8477 assert_eq!(v.len(), 1);
8478 assert_eq!(v[0].url.as_str(), "a.css");
8479 }
8480
8481 #[test]
8482 fn extract_css_urls_extremely_long_terminates() {
8483 let mut v = Vec::new();
8486 Xml::extract_css_urls(&"url(".repeat(2_000), &mut v);
8487 assert!(v.is_empty());
8489
8490 let mut v2 = Vec::new();
8491 Xml::extract_css_urls(&"url(a.png)".repeat(2_000), &mut v2);
8492 assert_eq!(v2.len(), 2_000);
8493 }
8494
8495 #[test]
8496 fn scan_node_on_empty_and_extreme_nodes_no_panic() {
8497 let mut v = Vec::new();
8498 Xml::scan_node(&XmlNode::default(), &mut v);
8499 Xml::scan_node(&node("", &[], vec![]), &mut v);
8500 Xml::scan_node(&node(&"a".repeat(10_000), &[("style", "url(x.png)")], vec![]), &mut v);
8501 assert_eq!(v.len(), 1, "only the inline style url()");
8502 assert_eq!(v[0].url.as_str(), "x.png");
8503 }
8504
8505 #[test]
8506 fn scan_node_img_srcset_and_background() {
8507 let mut v = Vec::new();
8508 Xml::scan_node(
8509 &node(
8510 "IMG",
8511 &[("src", "a.png"), ("srcset", "b.png 1x, c.png 2x"), ("background", "d.gif")],
8512 vec![],
8513 ),
8514 &mut v,
8515 );
8516 let urls: Vec<&str> = v.iter().map(|r| r.url.as_str()).collect();
8517 assert_eq!(urls, vec!["a.png", "b.png", "c.png", "d.gif"]);
8518 assert!(v.iter().all(|r| r.kind == ExternalResourceKind::Image));
8519 }
8520
8521 #[test]
8522 fn scan_external_resources_on_empty_document() {
8523 let xml = Xml {
8524 root: Vec::new().into(),
8525 };
8526 assert_eq!(xml.scan_external_resources().as_ref().len(), 0);
8527 }
8528
8529 #[test]
8530 fn scan_external_resources_finds_every_element_kind() {
8531 let xml = Xml {
8532 root: vec![
8533 elem(node("img", &[("src", "i.png")], vec![])),
8534 elem(node("link", &[("href", "s.css"), ("rel", "stylesheet")], vec![])),
8535 elem(node("script", &[("src", "s.js")], vec![])),
8536 elem(node("video", &[("src", "v.mp4"), ("poster", "p.jpg")], vec![])),
8537 elem(node("audio", &[("src", "a.mp3")], vec![])),
8538 elem(node("a", &[("href", "f.pdf")], vec![])),
8539 elem(node("a", &[("href", "/page")], vec![])),
8540 ]
8541 .into(),
8542 };
8543 let res = xml.scan_external_resources();
8544 let mut urls: Vec<&str> = res.as_ref().iter().map(|r| r.url.as_str()).collect();
8545 urls.sort_unstable();
8546 assert_eq!(
8547 urls,
8548 vec!["a.mp3", "f.pdf", "i.png", "p.jpg", "s.css", "s.js", "v.mp4"],
8549 "`/page` is not a resource and must be skipped"
8550 );
8551 }
8552
8553 #[test]
8558 fn mime_type_hint_new_no_panic_and_fields_match_args() {
8559 for s in ["", " ", "text/css", "\u{1F600}", "\u{0}"] {
8560 assert_eq!(MimeTypeHint::new(s).inner.as_str(), s, "new() stores verbatim");
8561 }
8562 let long = "x".repeat(LONG);
8563 assert_eq!(MimeTypeHint::new(&long).inner.as_str().len(), LONG);
8564 }
8565
8566 #[test]
8567 fn mime_type_hint_from_extension_edges() {
8568 assert_eq!(
8569 MimeTypeHint::from_extension("").inner.as_str(),
8570 "application/octet-stream"
8571 );
8572 assert_eq!(
8573 MimeTypeHint::from_extension("PnG").inner.as_str(),
8574 "image/png",
8575 "extension match is case-insensitive"
8576 );
8577 assert_eq!(MimeTypeHint::from_extension("jpeg").inner.as_str(), "image/jpeg");
8578 assert_eq!(MimeTypeHint::from_extension("woff2").inner.as_str(), "font/woff2");
8579 assert_eq!(
8580 MimeTypeHint::from_extension("\u{1F600}").inner.as_str(),
8581 "application/octet-stream"
8582 );
8583 assert_eq!(
8584 MimeTypeHint::from_extension(&"z".repeat(LONG)).inner.as_str(),
8585 "application/octet-stream"
8586 );
8587 }
8588
8589 #[test]
8594 fn component_id_builtin_and_new_invariants() {
8595 let b = ComponentId::builtin("div");
8596 assert_eq!(b.collection.as_str(), "builtin");
8597 assert_eq!(b.name.as_str(), "div");
8598
8599 let c = ComponentId::new("", "");
8600 assert_eq!(c.collection.as_str(), "");
8601 assert_eq!(c.name.as_str(), "");
8602
8603 let u = ComponentId::new("\u{1F600}", "\u{130}");
8604 assert_eq!(u.collection.as_str(), "\u{1F600}");
8605 assert_eq!(u.name.as_str(), "\u{130}");
8606 }
8607
8608 #[test]
8609 fn component_id_qualified_name_roundtrips_through_the_map_lookup() {
8610 assert_eq!(ComponentId::builtin("div").qualified_name(), "builtin:div");
8611 assert_eq!(ComponentId::new("", "").qualified_name(), ":");
8612 assert_eq!(ComponentId::new("a", "b:c").qualified_name(), "a:b:c");
8615 }
8616
8617 #[test]
8622 fn component_field_type_box_new_as_ref_and_clone() {
8623 let b = ComponentFieldTypeBox::new(ComponentFieldType::Bool);
8624 assert!(!b.ptr.is_null());
8625 assert_eq!(*b.as_ref(), ComponentFieldType::Bool);
8626
8627 let c = b.clone();
8628 assert_eq!(*c.as_ref(), ComponentFieldType::Bool);
8629 assert_ne!(b.ptr, c.ptr, "clone must deep-copy, not alias");
8630 assert_eq!(b, c, "PartialEq compares pointees");
8631 drop(c);
8632 assert_eq!(*b.as_ref(), ComponentFieldType::Bool, "original survives");
8633 }
8634
8635 #[test]
8636 fn component_field_type_box_nested_deeply_drops_cleanly() {
8637 let mut t = ComponentFieldType::Bool;
8638 for _ in 0..64 {
8639 t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(t));
8640 }
8641 assert_eq!(t.format(), format!("{}Bool{}", "Option<".repeat(64), ">".repeat(64)));
8642 drop(t);
8643 }
8644
8645 #[test]
8646 fn component_field_value_box_new_as_ref_and_clone() {
8647 let v = ComponentFieldValueBox::new(ComponentFieldValue::I32(i32::MIN));
8648 assert!(!v.ptr.is_null());
8649 assert_eq!(*v.as_ref(), ComponentFieldValue::I32(i32::MIN));
8650 let c = v.clone();
8651 assert_ne!(v.ptr, c.ptr);
8652 assert_eq!(v, c);
8653 }
8654
8655 #[test]
8660 fn component_field_type_parse_empty_whitespace_garbage() {
8661 assert_eq!(ComponentFieldType::parse(""), None);
8662 assert_eq!(ComponentFieldType::parse(" "), None);
8663 assert_eq!(ComponentFieldType::parse("\t\n"), None);
8664 assert_eq!(ComponentFieldType::parse("lowercase"), None);
8665 assert_eq!(ComponentFieldType::parse("\u{0}\u{7f}"), None);
8666 assert_eq!(ComponentFieldType::parse("Option<>"), None, "empty inner rejected");
8667 assert_eq!(ComponentFieldType::parse("Vec<>"), None);
8668 assert_eq!(ComponentFieldType::parse("Option<lowercase>"), None);
8669 }
8670
8671 #[test]
8672 fn component_field_type_parse_valid_minimal_and_trimming() {
8673 assert_eq!(ComponentFieldType::parse("String"), Some(ComponentFieldType::String));
8674 assert_eq!(
8675 ComponentFieldType::parse(" String "),
8676 Some(ComponentFieldType::String),
8677 "leading/trailing whitespace is trimmed"
8678 );
8679 assert_eq!(ComponentFieldType::parse("bool"), Some(ComponentFieldType::Bool));
8680 assert_eq!(ComponentFieldType::parse("usize"), Some(ComponentFieldType::Usize));
8681 assert_eq!(
8682 ComponentFieldType::parse("StructRef(Foo)"),
8683 Some(ComponentFieldType::StructRef(AzString::from("Foo")))
8684 );
8685 assert_eq!(
8686 ComponentFieldType::parse("EnumRef(Foo)"),
8687 Some(ComponentFieldType::EnumRef(AzString::from("Foo")))
8688 );
8689 assert_eq!(
8690 ComponentFieldType::parse("RefAny"),
8691 Some(ComponentFieldType::RefAny(AzString::from("")))
8692 );
8693 }
8694
8695 #[test]
8696 fn component_field_type_parse_leading_trailing_junk_is_rejected_or_absorbed() {
8697 assert_eq!(
8700 ComponentFieldType::parse("String;garbage"),
8701 Some(ComponentFieldType::StructRef(AzString::from("String;garbage")))
8702 );
8703 assert_eq!(ComponentFieldType::parse("string;garbage"), None);
8705 }
8706
8707 #[test]
8708 fn component_field_type_parse_boundary_numbers() {
8709 for s in ["0", "-0", "9223372036854775807", "-9223372036854775808", "1e400", "inf"] {
8710 assert_eq!(
8711 ComponentFieldType::parse(s),
8712 None,
8713 "numeric literal {s:?} is not a type name"
8714 );
8715 }
8716 assert_eq!(
8719 ComponentFieldType::parse("NaN"),
8720 Some(ComponentFieldType::StructRef(AzString::from("NaN")))
8721 );
8722 }
8723
8724 #[test]
8725 fn component_field_type_parse_unicode_no_panic() {
8726 assert_eq!(ComponentFieldType::parse("\u{1F600}"), None, "emoji is not uppercase");
8727 assert_eq!(
8729 ComponentFieldType::parse("\u{0391}bc"),
8730 Some(ComponentFieldType::StructRef(AzString::from("\u{0391}bc")))
8731 );
8732 }
8733
8734 #[test]
8735 fn component_field_type_parse_depth_boundary_is_exact() {
8736 let ok = format!(
8738 "{}Bool{}",
8739 "Option<".repeat(MAX_TYPE_PARSE_DEPTH),
8740 ">".repeat(MAX_TYPE_PARSE_DEPTH)
8741 );
8742 assert!(
8743 ComponentFieldType::parse(&ok).is_some(),
8744 "exactly MAX_TYPE_PARSE_DEPTH wrappers must still parse"
8745 );
8746
8747 let too_deep = format!(
8748 "{}Bool{}",
8749 "Option<".repeat(MAX_TYPE_PARSE_DEPTH + 1),
8750 ">".repeat(MAX_TYPE_PARSE_DEPTH + 1)
8751 );
8752 assert_eq!(
8753 ComponentFieldType::parse(&too_deep),
8754 None,
8755 "one wrapper past the cap must be rejected, not overflow"
8756 );
8757 }
8758
8759 #[test]
8760 fn component_field_type_parse_depth_direct_call_honors_start_depth() {
8761 assert_eq!(
8762 ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH),
8763 Some(ComponentFieldType::Bool),
8764 "depth == cap is still allowed"
8765 );
8766 assert_eq!(
8767 ComponentFieldType::parse_depth("Bool", MAX_TYPE_PARSE_DEPTH + 1),
8768 None
8769 );
8770 assert_eq!(
8771 ComponentFieldType::parse_depth("Bool", usize::MAX),
8772 None,
8773 "usize::MAX start depth must not overflow, just refuse"
8774 );
8775 }
8776
8777 #[test]
8778 fn component_field_type_parse_nested_recursion_does_not_stack_overflow() {
8779 let bomb = format!("{}Bool{}", "Vec<".repeat(50_000), ">".repeat(50_000));
8780 assert_eq!(ComponentFieldType::parse(&bomb), None);
8781 }
8782
8783 #[test]
8784 fn component_field_type_parse_extremely_long_terminates() {
8785 let long = format!("A{}", "b".repeat(LONG));
8787 assert_eq!(
8788 ComponentFieldType::parse(&long),
8789 Some(ComponentFieldType::StructRef(AzString::from(long.as_str())))
8790 );
8791 }
8792
8793 #[test]
8794 fn component_field_type_round_trip_representative() {
8795 let representative = vec![
8796 ComponentFieldType::String,
8797 ComponentFieldType::Bool,
8798 ComponentFieldType::I32,
8799 ComponentFieldType::I64,
8800 ComponentFieldType::U32,
8801 ComponentFieldType::U64,
8802 ComponentFieldType::Usize,
8803 ComponentFieldType::F32,
8804 ComponentFieldType::F64,
8805 ComponentFieldType::ColorU,
8806 ComponentFieldType::CssProperty,
8807 ComponentFieldType::ImageRef,
8808 ComponentFieldType::FontRef,
8809 ComponentFieldType::StyledDom,
8810 ComponentFieldType::StructRef(AzString::from("Foo")),
8811 ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::Bool)),
8812 ComponentFieldType::VecType(ComponentFieldTypeBox::new(ComponentFieldType::I32)),
8813 ComponentFieldType::RefAny(AzString::from("")),
8814 ComponentFieldType::RefAny(AzString::from("Hint")),
8815 ComponentFieldType::Callback(ComponentCallbackSignature {
8816 return_type: AzString::from("Update"),
8817 args: Vec::new().into(),
8818 }),
8819 ];
8820 for x in representative {
8821 let s = x.format();
8822 assert_eq!(
8823 ComponentFieldType::parse(&s),
8824 Some(x.clone()),
8825 "parse(format({x:?})) must round-trip"
8826 );
8827 }
8828 }
8829
8830 #[test]
8831 fn component_field_type_round_trip_edge_values() {
8832 for x in [
8834 ComponentFieldType::StructRef(AzString::from("\u{0391}\u{1F600}")),
8835 ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
8836 ComponentFieldType::VecType(ComponentFieldTypeBox::new(
8837 ComponentFieldType::StructRef(AzString::from("Foo")),
8838 )),
8839 )),
8840 ] {
8841 assert_eq!(ComponentFieldType::parse(&x.format()), Some(x.clone()));
8842 }
8843 }
8844
8845 #[test]
8846 fn component_field_type_format_is_an_idempotent_normalization() {
8847 let e = ComponentFieldType::EnumRef(AzString::from("Role"));
8851 let once = e.format();
8852 assert_eq!(once, "Role");
8853 let reparsed = ComponentFieldType::parse(&once).expect("parses");
8854 assert_eq!(
8855 reparsed,
8856 ComponentFieldType::StructRef(AzString::from("Role")),
8857 "EnumRef is lossy through format() — it comes back as StructRef"
8858 );
8859 assert_eq!(reparsed.format(), once, "but the normalization is idempotent");
8860 }
8861
8862 #[test]
8863 fn component_field_type_display_matches_format() {
8864 let t = ComponentFieldType::OptionType(ComponentFieldTypeBox::new(ComponentFieldType::F64));
8865 assert_eq!(format!("{t}"), t.format());
8866 assert_eq!(format!("{t}"), "Option<F64>");
8867 }
8868
8869 #[test]
8870 fn component_field_type_format_no_panic_on_empty_payloads() {
8871 let t = ComponentFieldType::Callback(ComponentCallbackSignature {
8872 return_type: AzString::from(""),
8873 args: Vec::new().into(),
8874 });
8875 assert_eq!(t.format(), "Callback()");
8876 assert_eq!(ComponentFieldType::parse("Callback()"), Some(t));
8878 }
8879
8880 fn named(name: &str, v: ComponentFieldValue) -> ComponentFieldNamedValue {
8885 ComponentFieldNamedValue {
8886 name: AzString::from(name),
8887 value: v,
8888 }
8889 }
8890
8891 fn named_vec() -> ComponentFieldNamedValueVec {
8892 vec![
8893 named("a", ComponentFieldValue::String(AzString::from("x"))),
8894 named("b", ComponentFieldValue::Bool(true)),
8895 named("", ComponentFieldValue::U64(u64::MAX)),
8896 named("\u{1F600}", ComponentFieldValue::String(AzString::from("emoji"))),
8897 ]
8898 .into()
8899 }
8900
8901 #[test]
8902 fn named_value_vec_get_field_valid_minimal() {
8903 let v = named_vec();
8904 assert_eq!(
8905 v.get_field("a"),
8906 Some(&ComponentFieldValue::String(AzString::from("x")))
8907 );
8908 assert_eq!(v.get_field("b"), Some(&ComponentFieldValue::Bool(true)));
8909 }
8910
8911 #[test]
8912 fn named_value_vec_get_field_empty_whitespace_garbage_unicode() {
8913 let v = named_vec();
8914 assert_eq!(v.get_field(""), Some(&ComponentFieldValue::U64(u64::MAX)));
8916 assert_eq!(v.get_field(" "), None);
8917 assert_eq!(v.get_field("\t\n"), None);
8918 assert_eq!(v.get_field("\u{0}\u{7f}"), None);
8919 assert!(v.get_field("\u{1F600}").is_some());
8920 assert_eq!(v.get_field(" a "), None, "no trimming: lookup is exact");
8921 assert_eq!(v.get_field("a;garbage"), None);
8922 }
8923
8924 #[test]
8925 fn named_value_vec_get_field_on_empty_vec_and_long_key() {
8926 let empty = ComponentFieldNamedValueVec::from_const_slice(&[]);
8927 assert_eq!(empty.get_field("a"), None);
8928 assert_eq!(empty.get_string("a"), None);
8929 assert_eq!(named_vec().get_field(&"z".repeat(LONG)), None);
8930 }
8931
8932 #[test]
8933 fn named_value_vec_get_string_only_matches_string_variant() {
8934 let v = named_vec();
8935 assert_eq!(v.get_string("a").map(AzString::as_str), Some("x"));
8936 assert_eq!(v.get_string("b"), None, "Bool is not a String");
8937 assert_eq!(v.get_string(""), None, "U64 is not a String");
8938 assert_eq!(v.get_string("missing"), None);
8939 }
8940
8941 #[test]
8942 fn named_value_vec_boundary_numeric_keys() {
8943 let v: ComponentFieldNamedValueVec = vec![
8944 named("0", ComponentFieldValue::I32(0)),
8945 named("-0", ComponentFieldValue::I32(i32::MIN)),
8946 named("9223372036854775807", ComponentFieldValue::I64(i64::MAX)),
8947 named("NaN", ComponentFieldValue::F32(f32::NAN)),
8948 ]
8949 .into();
8950 assert_eq!(v.get_field("0"), Some(&ComponentFieldValue::I32(0)));
8951 assert_eq!(v.get_field("-0"), Some(&ComponentFieldValue::I32(i32::MIN)));
8952 assert_eq!(
8953 v.get_field("9223372036854775807"),
8954 Some(&ComponentFieldValue::I64(i64::MAX))
8955 );
8956 assert!(matches!(v.get_field("NaN"), Some(ComponentFieldValue::F32(f)) if f.is_nan()));
8958 }
8959
8960 fn model_with_text() -> ComponentDataModel {
8965 dm(
8966 "M",
8967 vec![
8968 data_field(
8969 "text",
8970 ComponentFieldType::String,
8971 Some(ComponentDefaultValue::String(AzString::from("hi"))),
8972 "",
8973 ),
8974 data_field("count", ComponentFieldType::U32, Some(ComponentDefaultValue::U32(3)), ""),
8975 data_field("required_one", ComponentFieldType::String, None, ""),
8976 ],
8977 )
8978 }
8979
8980 #[test]
8981 fn data_model_get_field_valid_minimal_and_missing() {
8982 let m = model_with_text();
8983 assert!(m.get_field("text").is_some());
8984 assert!(m.get_field("count").is_some());
8985 assert!(m.get_field("missing").is_none());
8986 assert!(m.get_field("").is_none());
8987 assert!(m.get_field(" ").is_none());
8988 assert!(m.get_field(" text ").is_none(), "exact match, no trimming");
8989 assert!(m.get_field("\u{1F600}").is_none());
8990 assert!(m.get_field(&"z".repeat(LONG)).is_none());
8991 }
8992
8993 #[test]
8994 fn data_model_get_field_on_empty_model() {
8995 let m = dm("Empty", Vec::new());
8996 assert!(m.get_field("anything").is_none());
8997 assert!(m.get_default_string("anything").is_none());
8998 }
8999
9000 #[test]
9001 fn data_model_get_default_string_only_for_string_defaults() {
9002 let m = model_with_text();
9003 assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
9004 assert_eq!(m.get_default_string("count"), None, "U32 default is not a String");
9005 assert_eq!(m.get_default_string("required_one"), None, "no default at all");
9006 assert_eq!(m.get_default_string("missing"), None);
9007 }
9008
9009 #[test]
9010 fn data_model_required_flag_follows_default_presence() {
9011 let m = model_with_text();
9012 assert!(!m.get_field("text").unwrap().required);
9013 assert!(
9014 m.get_field("required_one").unwrap().required,
9015 "a field with no default must be marked required"
9016 );
9017 }
9018
9019 #[test]
9020 fn data_model_with_default_overrides_and_preserves_len() {
9021 let m = model_with_text();
9022 let before = m.fields.as_ref().len();
9023 let m = m.with_default("text", ComponentDefaultValue::String(AzString::from("bye")));
9024 assert_eq!(m.fields.as_ref().len(), before, "len is preserved");
9025 assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("bye"));
9026 }
9027
9028 #[test]
9029 fn data_model_with_default_on_missing_field_is_a_no_op() {
9030 let m = model_with_text();
9031 let m = m.with_default("nope", ComponentDefaultValue::Bool(true));
9032 assert_eq!(m.fields.as_ref().len(), 3);
9033 assert_eq!(m.get_default_string("text").map(AzString::as_str), Some("hi"));
9034 assert!(m.get_field("nope").is_none(), "no field is inserted");
9035 }
9036
9037 #[test]
9038 fn data_model_with_default_extreme_names_and_values_no_panic() {
9039 let m = model_with_text()
9040 .with_default("", ComponentDefaultValue::None)
9041 .with_default(&"z".repeat(10_000), ComponentDefaultValue::F64(f64::NAN))
9042 .with_default("count", ComponentDefaultValue::Usize(usize::MAX))
9043 .with_default("text", ComponentDefaultValue::I64(i64::MIN));
9044 assert_eq!(m.fields.as_ref().len(), 3);
9045 assert!(matches!(
9046 m.get_field("count").unwrap().default_value,
9047 OptionComponentDefaultValue::Some(ComponentDefaultValue::Usize(usize::MAX))
9048 ));
9049 assert_eq!(
9050 m.get_default_string("text"),
9051 None,
9052 "text is now an I64 default, no longer a String"
9053 );
9054 }
9055
9056 #[test]
9057 fn data_model_with_default_fills_only_the_first_match() {
9058 let m = dm(
9060 "Dup",
9061 vec![
9062 data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("1"))), ""),
9063 data_field("x", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from("2"))), ""),
9064 ],
9065 )
9066 .with_default("x", ComponentDefaultValue::String(AzString::from("3")));
9067 let vals: Vec<&str> = m
9068 .fields
9069 .as_ref()
9070 .iter()
9071 .filter_map(|f| match &f.default_value {
9072 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s.as_str()),
9073 _ => None,
9074 })
9075 .collect();
9076 assert_eq!(vals, vec!["3", "2"], "only the first duplicate is overridden");
9077 }
9078
9079 #[test]
9084 fn component_source_create_is_user_defined() {
9085 assert_eq!(ComponentSource::create(), ComponentSource::UserDefined);
9086 assert_eq!(ComponentSource::default(), ComponentSource::UserDefined);
9087 }
9088
9089 #[test]
9090 fn component_map_create_is_empty_and_all_lookups_return_none() {
9091 let m = ComponentMap::create();
9092 assert_eq!(m.libraries.as_ref().len(), 0);
9093 assert!(m.get("builtin", "div").is_none());
9094 assert!(m.get_unqualified("div").is_none());
9095 assert!(m.get_by_qualified_name("builtin:div").is_none());
9096 assert!(m.all_components().is_empty());
9097 assert!(m.get_exportable_libraries().is_empty());
9098 }
9099
9100 #[test]
9101 fn component_map_with_builtin_invariants() {
9102 let m = ComponentMap::with_builtin();
9103 assert_eq!(m.libraries.as_ref().len(), 1);
9104 let lib = &m.libraries.as_ref()[0];
9105 assert_eq!(lib.name.as_str(), "builtin");
9106 assert!(!lib.exportable, "builtins must never be exportable");
9107 assert!(!lib.modifiable, "builtins must never be modifiable");
9108 assert_eq!(
9109 m.all_components().len(),
9110 lib.components.as_ref().len(),
9111 "all_components must see every registered component"
9112 );
9113 assert!(
9114 m.get_exportable_libraries().is_empty(),
9115 "the builtin library is not exportable"
9116 );
9117 }
9118
9119 #[test]
9120 fn component_map_get_valid_minimal() {
9121 let m = ComponentMap::with_builtin();
9122 assert!(m.get("builtin", "div").is_some());
9123 assert!(m.get("builtin", "if").is_some());
9124 assert!(m.get("builtin", "for").is_some());
9125 assert!(m.get("builtin", "map").is_some());
9126 assert_eq!(
9127 m.get("builtin", "div").unwrap().id.qualified_name(),
9128 "builtin:div"
9129 );
9130 }
9131
9132 #[test]
9133 fn component_map_get_empty_whitespace_garbage_unicode() {
9134 let m = ComponentMap::with_builtin();
9135 assert!(m.get("", "").is_none());
9136 assert!(m.get("builtin", "").is_none());
9137 assert!(m.get("", "div").is_none());
9138 assert!(m.get("builtin", " ").is_none());
9139 assert!(m.get("builtin", " div ").is_none(), "no trimming");
9140 assert!(m.get("builtin", "DIV").is_none(), "lookup is case-sensitive");
9141 assert!(m.get("builtin", "\u{1F600}").is_none());
9142 assert!(m.get("builtin", "\u{0}\u{7f}").is_none());
9143 assert!(m.get("builtin", "div;garbage").is_none());
9144 }
9145
9146 #[test]
9147 fn component_map_get_extremely_long_name_terminates() {
9148 let m = ComponentMap::with_builtin();
9149 assert!(m.get(&"a".repeat(LONG), &"b".repeat(LONG)).is_none());
9150 assert!(m.get_unqualified(&"b".repeat(LONG)).is_none());
9151 assert!(m.get_by_qualified_name(&"c".repeat(LONG)).is_none());
9152 }
9153
9154 #[test]
9155 fn component_map_get_unqualified_only_searches_builtin() {
9156 let lib = ComponentLibrary {
9157 name: AzString::from("mylib"),
9158 version: AzString::from("1.0.0"),
9159 description: AzString::from(""),
9160 components: vec![user_def("", Vec::new())].into(),
9161 exportable: true,
9162 modifiable: true,
9163 data_models: Vec::new().into(),
9164 enum_models: Vec::new().into(),
9165 };
9166 let libs: ComponentLibraryVec = vec![lib].into();
9167 let m = ComponentMap::from_libraries(&libs);
9168
9169 assert!(m.get("mylib", "widget").is_some());
9170 assert!(
9171 m.get_unqualified("widget").is_none(),
9172 "unqualified lookup must NOT reach non-builtin libraries"
9173 );
9174 assert!(m.get_by_qualified_name("mylib:widget").is_some());
9175 assert_eq!(m.get_exportable_libraries().len(), 1);
9176 assert_eq!(m.all_components().len(), 1);
9177 }
9178
9179 #[test]
9180 fn component_map_get_by_qualified_name_boundary_forms() {
9181 let m = ComponentMap::with_builtin();
9182 assert!(m.get_by_qualified_name("div").is_some());
9184 assert!(m.get_by_qualified_name("builtin:div").is_some());
9186 assert!(m.get_by_qualified_name("builtin:div:extra").is_none());
9188 assert!(m.get_by_qualified_name(":").is_none());
9189 assert!(m.get_by_qualified_name(":div").is_none());
9190 assert!(m.get_by_qualified_name("builtin:").is_none());
9191 assert!(m.get_by_qualified_name("").is_none());
9192 assert!(m.get_by_qualified_name(" ").is_none());
9193 }
9194
9195 #[test]
9196 fn component_map_from_libraries_clones_without_losing_entries() {
9197 let src = ComponentMap::with_builtin();
9198 let copy = ComponentMap::from_libraries(&src.libraries);
9199 assert_eq!(copy.all_components().len(), src.all_components().len());
9200 assert!(copy.get_unqualified("div").is_some());
9201 }
9202
9203 #[test]
9204 fn register_builtin_components_is_stable_across_calls() {
9205 let a = register_builtin_components();
9206 let b = register_builtin_components();
9207 assert_eq!(a.components.as_ref().len(), b.components.as_ref().len());
9208 assert!(a.components.as_ref().len() > 50);
9209 assert_eq!(a.name.as_str(), "builtin");
9210 assert_eq!(a.version.as_str(), "1.0.0");
9211 assert!(a
9213 .components
9214 .as_ref()
9215 .iter()
9216 .all(|c| c.id.collection.as_str() == "builtin"));
9217 assert!(a
9218 .components
9219 .as_ref()
9220 .iter()
9221 .all(|c| c.source == ComponentSource::Builtin));
9222 }
9223
9224 #[test]
9229 fn xml_node_child_as_text_and_as_element_are_mutually_exclusive() {
9230 let t = txt("hello");
9231 assert_eq!(t.as_text(), Some("hello"));
9232 assert!(t.as_element().is_none());
9233
9234 let e = elem(node("div", &[], vec![]));
9235 assert!(e.as_text().is_none());
9236 assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("div"));
9237 }
9238
9239 #[test]
9240 fn xml_node_child_as_text_edge_values() {
9241 assert_eq!(txt("").as_text(), Some(""), "an empty text node is still text");
9242 assert_eq!(txt(" ").as_text(), Some(" "), "no trimming in the getter");
9243 assert_eq!(txt("\u{1F600}\u{0301}").as_text(), Some("\u{1F600}\u{0301}"));
9244 assert_eq!(txt("\u{0}").as_text(), Some("\u{0}"));
9245 }
9246
9247 #[test]
9248 fn xml_node_child_as_element_mut_allows_mutation_and_rejects_text() {
9249 let mut e = elem(node("div", &[], vec![]));
9250 e.as_element_mut().expect("is an element").node_type = "span".into();
9251 assert_eq!(e.as_element().map(|n| n.node_type.as_str()), Some("span"));
9252
9253 let mut t = txt("x");
9254 assert!(t.as_element_mut().is_none(), "text nodes have no element");
9255 }
9256
9257 #[test]
9258 fn xml_node_create_and_with_children_invariants() {
9259 let n = XmlNode::create("div");
9260 assert_eq!(n.node_type.as_str(), "div");
9261 assert_eq!(n.children.as_ref().len(), 0);
9262 assert_eq!(n.attributes.as_ref().len(), 0);
9263
9264 let n = n.with_children(vec![txt("a"), elem(XmlNode::create("b"))]);
9265 assert_eq!(n.children.as_ref().len(), 2);
9266 assert_eq!(n.node_type.as_str(), "div", "tag survives with_children");
9267
9268 let n = n.with_children(Vec::new());
9270 assert_eq!(n.children.as_ref().len(), 0);
9271 }
9272
9273 #[test]
9274 fn xml_node_create_extreme_tag_names_no_panic() {
9275 assert_eq!(XmlNode::create("").node_type.as_str(), "");
9276 assert_eq!(XmlNode::create("\u{1F600}").node_type.as_str(), "\u{1F600}");
9277 assert_eq!(
9278 XmlNode::create(&*"a".repeat(10_000)).node_type.as_str().len(),
9279 10_000
9280 );
9281 }
9282
9283 #[test]
9284 fn xml_node_get_text_content_concatenates_only_direct_text() {
9285 let n = node(
9286 "p",
9287 &[],
9288 vec![
9289 txt("a "),
9290 elem(node("span", &[], vec![txt("IGNORED")])),
9291 txt("b"),
9292 ],
9293 );
9294 assert_eq!(
9295 n.get_text_content(),
9296 "a b",
9297 "only DIRECT text children, nested element text is not included"
9298 );
9299 assert_eq!(XmlNode::default().get_text_content(), "");
9300 assert_eq!(node("p", &[], vec![txt(""), txt("")]).get_text_content(), "");
9301 }
9302
9303 #[test]
9304 fn xml_node_has_only_text_children_true_false_and_empty() {
9305 assert!(
9306 XmlNode::default().has_only_text_children(),
9307 "vacuously true for a childless node — callers must pair this with a text check"
9308 );
9309 assert!(node("p", &[], vec![txt("a"), txt("b")]).has_only_text_children());
9310 assert!(!node("p", &[], vec![txt("a"), elem(XmlNode::create("b"))]).has_only_text_children());
9311 assert!(!node("p", &[], vec![elem(XmlNode::create("b"))]).has_only_text_children());
9312 }
9313
9314 #[test]
9319 fn get_html_node_empty_input_is_no_html_node() {
9320 assert_eq!(get_html_node(&[]), Err(DomXmlParseError::NoHtmlNode));
9321 assert_eq!(get_html_node(&[txt("just text")]), Err(DomXmlParseError::NoHtmlNode));
9322 assert_eq!(
9323 get_html_node(&[elem(XmlNode::create("div"))]),
9324 Err(DomXmlParseError::NoHtmlNode)
9325 );
9326 }
9327
9328 #[test]
9329 fn get_html_node_valid_minimal_and_case_insensitive() {
9330 let roots = vec![elem(XmlNode::create("HTML"))];
9331 assert!(get_html_node(&roots).is_ok(), "tag casing is normalized");
9332 }
9333
9334 #[test]
9335 fn get_html_node_rejects_multiple_roots() {
9336 let roots = vec![elem(XmlNode::create("html")), elem(XmlNode::create("html"))];
9337 assert_eq!(
9338 get_html_node(&roots),
9339 Err(DomXmlParseError::MultipleHtmlRootNodes)
9340 );
9341 }
9342
9343 #[test]
9344 fn get_body_node_empty_and_missing() {
9345 assert_eq!(get_body_node(&[]), Err(DomXmlParseError::NoBodyInHtml));
9346 assert_eq!(
9347 get_body_node(&[elem(XmlNode::create("head"))]),
9348 Err(DomXmlParseError::NoBodyInHtml)
9349 );
9350 }
9351
9352 #[test]
9353 fn get_body_node_direct_and_nested() {
9354 let direct = vec![elem(XmlNode::create("body"))];
9355 assert!(get_body_node(&direct).is_ok());
9356
9357 let nested = vec![elem(node(
9359 "head",
9360 &[],
9361 vec![elem(node("div", &[], vec![elem(XmlNode::create("BODY"))]))],
9362 ))];
9363 assert!(
9364 get_body_node(&nested).is_ok(),
9365 "the recursive fallback finds a nested body"
9366 );
9367 }
9368
9369 fn wrap_divs(depth: usize, inner: XmlNode) -> XmlNode {
9371 let mut n = inner;
9372 for _ in 0..depth {
9373 n = node("div", &[], vec![elem(n)]);
9374 }
9375 n
9376 }
9377
9378 #[test]
9379 fn get_body_node_deep_nesting_is_depth_capped_not_stack_overflowing() {
9380 let ok = vec![elem(wrap_divs(
9382 MAX_XML_NESTING_DEPTH - 2,
9383 XmlNode::create("body"),
9384 ))];
9385 assert!(get_body_node(&ok).is_ok(), "body within the depth cap is found");
9386
9387 let too_deep = vec![elem(wrap_divs(2_000, XmlNode::create("body")))];
9389 assert_eq!(
9390 get_body_node(&too_deep),
9391 Err(DomXmlParseError::NoBodyInHtml),
9392 "past MAX_XML_NESTING_DEPTH the search gives up instead of crashing"
9393 );
9394 }
9395
9396 #[test]
9397 fn find_node_by_type_empty_garbage_unicode() {
9398 assert!(find_node_by_type(&[], "div").is_none());
9399 assert!(find_node_by_type(&[txt("x")], "div").is_none());
9400
9401 let roots = vec![elem(XmlNode::create("div"))];
9402 assert!(find_node_by_type(&roots, "").is_none());
9403 assert!(find_node_by_type(&roots, " ").is_none());
9404 assert!(find_node_by_type(&roots, "\u{1F600}").is_none());
9405 assert!(find_node_by_type(&roots, &"z".repeat(LONG)).is_none());
9406 assert!(find_node_by_type(&roots, "DIV").is_some());
9410 }
9411
9412 #[test]
9413 fn find_node_by_type_valid_minimal_and_recursive() {
9414 let roots = vec![elem(node(
9415 "html",
9416 &[],
9417 vec![elem(node("head", &[], vec![elem(XmlNode::create("STYLE"))]))],
9418 ))];
9419 assert!(find_node_by_type(&roots, "html").is_some());
9420 assert!(
9421 find_node_by_type(&roots, "style").is_some(),
9422 "search recurses into the whole tree"
9423 );
9424 assert!(find_node_by_type(&roots, "body").is_none());
9425 }
9426
9427 #[test]
9428 fn find_node_by_type_prefers_the_shallowest_match() {
9429 let roots = vec![
9430 elem(node("div", &[], vec![elem(XmlNode::create("span"))])),
9431 elem(node("span", &[("id", "shallow")], vec![])),
9432 ];
9433 let found = find_node_by_type(&roots, "span").expect("found");
9434 assert_eq!(
9435 found.attributes.get_key("id").map(AzString::as_str),
9436 Some("shallow"),
9437 "direct children are scanned before recursing"
9438 );
9439 }
9440
9441 #[test]
9442 fn find_attribute_valid_minimal_and_missing() {
9443 let n = node("a", &[("href", "x"), ("id", "y")], vec![]);
9444 assert_eq!(find_attribute(&n, "href").map(AzString::as_str), Some("x"));
9445 assert_eq!(find_attribute(&n, "id").map(AzString::as_str), Some("y"));
9446 assert!(find_attribute(&n, "missing").is_none());
9447 assert!(find_attribute(&n, "").is_none());
9448 assert!(find_attribute(&n, " ").is_none());
9449 assert!(find_attribute(&XmlNode::default(), "href").is_none());
9450 assert!(find_attribute(&n, &"z".repeat(LONG)).is_none());
9451 }
9452
9453 #[test]
9454 fn find_attribute_compares_against_the_normalized_key() {
9455 let n = node("button", &[("aria-label", "Save")], vec![]);
9458 assert_eq!(
9459 find_attribute(&n, "aria_label").map(AzString::as_str),
9460 Some("Save")
9461 );
9462 assert!(
9463 find_attribute(&n, "aria-label").is_none(),
9464 "the hyphenated spelling never matches (keys are normalized first)"
9465 );
9466 }
9467
9468 #[test]
9469 fn find_attribute_unicode_keys_no_panic() {
9470 let n = node("div", &[("\u{130}", "v"), ("\u{1F600}", "w")], vec![]);
9471 assert!(find_attribute(&n, "\u{1F600}").is_some(), "emoji keys pass through");
9472 assert!(find_attribute(&n, "\u{130}").is_none());
9474 }
9475
9476 #[test]
9481 fn normalize_casing_documented_forms() {
9482 assert_eq!(normalize_casing("abcDef"), "abc_def");
9483 assert_eq!(normalize_casing("AbcDef"), "abc_def");
9484 assert_eq!(normalize_casing("abc-def"), "abc_def");
9485 assert_eq!(normalize_casing("abc_def"), "abc_def");
9486 }
9487
9488 #[test]
9489 fn normalize_casing_edge_inputs() {
9490 assert_eq!(normalize_casing(""), "");
9491 assert_eq!(normalize_casing("---"), "", "separators alone produce no words");
9492 assert_eq!(normalize_casing("___"), "");
9493 assert_eq!(normalize_casing("A"), "a");
9494 assert_eq!(
9495 normalize_casing("ABC"),
9496 "a_b_c",
9497 "every uppercase char starts a new word"
9498 );
9499 assert_eq!(normalize_casing("h1"), "h1");
9500 assert_eq!(normalize_casing(" "), " ", "whitespace is not a separator");
9501 }
9502
9503 #[test]
9504 fn normalize_casing_unicode_and_long_no_panic() {
9505 assert_eq!(normalize_casing("\u{130}"), "i\u{307}");
9507 assert_eq!(normalize_casing("\u{1F600}"), "\u{1F600}");
9508 assert_eq!(normalize_casing(&"a".repeat(50_000)).len(), 50_000);
9509 assert_eq!(normalize_casing(&"A".repeat(50_000)).len(), 50_000 * 2 - 1);
9511 }
9512
9513 #[test]
9518 fn get_item_empty_hierarchy_returns_the_root() {
9519 let mut root = node("div", &[("id", "root")], vec![]);
9520 let got = get_item(&[], &mut root).expect("empty hierarchy => root");
9521 assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("root"));
9522 }
9523
9524 #[test]
9525 fn get_item_walks_nested_elements() {
9526 let mut root = node(
9527 "a",
9528 &[],
9529 vec![elem(node("b", &[], vec![elem(node("c", &[("id", "deep")], vec![]))]))],
9530 );
9531 let got = get_item(&[0, 0], &mut root).expect("a > b > c");
9532 assert_eq!(got.node_type.as_str(), "c");
9533 assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("deep"));
9534 }
9535
9536 #[test]
9537 fn get_item_out_of_bounds_and_text_nodes_return_none() {
9538 let mut root = node("a", &[], vec![txt("hello"), elem(XmlNode::create("b"))]);
9539 assert!(get_item(&[5], &mut root).is_none(), "out of bounds => None");
9540 assert!(
9541 get_item(&[usize::MAX], &mut root).is_none(),
9542 "usize::MAX index must not panic"
9543 );
9544 assert!(
9545 get_item(&[0], &mut root).is_none(),
9546 "index 0 is a TEXT node — not traversable"
9547 );
9548 assert!(get_item(&[1], &mut root).is_some());
9549 assert!(
9550 get_item(&[1, 0], &mut root).is_none(),
9551 "descending past a leaf => None"
9552 );
9553 }
9554
9555 #[test]
9556 fn get_item_deep_hierarchy_terminates() {
9557 let mut root = wrap_divs(400, node("div", &[("id", "bottom")], vec![]));
9559 let path = vec![0usize; 400];
9560 let got = get_item(&path, &mut root).expect("reaches the bottom");
9561 assert_eq!(got.attributes.get_key("id").map(AzString::as_str), Some("bottom"));
9562 }
9563
9564 #[test]
9569 fn decode_numeric_entity_valid_minimal() {
9570 assert_eq!(decode_numeric_entity("#65"), Some('A'));
9571 assert_eq!(decode_numeric_entity("#x41"), Some('A'));
9572 assert_eq!(decode_numeric_entity("#X41"), Some('A'), "uppercase X accepted");
9573 assert_eq!(decode_numeric_entity("#x1F600"), Some('\u{1F600}'));
9574 }
9575
9576 #[test]
9577 fn decode_numeric_entity_empty_whitespace_garbage() {
9578 assert_eq!(decode_numeric_entity(""), None);
9579 assert_eq!(decode_numeric_entity(" "), None);
9580 assert_eq!(decode_numeric_entity("\t\n"), None);
9581 assert_eq!(decode_numeric_entity("amp"), None, "named entity is not numeric");
9582 assert_eq!(decode_numeric_entity("#"), None);
9583 assert_eq!(decode_numeric_entity("#x"), None);
9584 assert_eq!(decode_numeric_entity("#zz"), None);
9585 assert_eq!(decode_numeric_entity("# 65"), None);
9586 assert_eq!(decode_numeric_entity("#65junk"), None);
9587 assert_eq!(decode_numeric_entity("\u{1F600}"), None);
9588 }
9589
9590 #[test]
9591 fn decode_numeric_entity_boundary_code_points() {
9592 assert_eq!(decode_numeric_entity("#0"), Some('\u{0}'), "NUL is a valid char");
9593 assert_eq!(decode_numeric_entity("#x10FFFF"), Some('\u{10FFFF}'), "max scalar");
9594 assert_eq!(
9595 decode_numeric_entity("#x110000"),
9596 None,
9597 "one past the max scalar value"
9598 );
9599 assert_eq!(
9600 decode_numeric_entity("#xD800"),
9601 None,
9602 "a lone surrogate is not a char"
9603 );
9604 assert_eq!(
9605 decode_numeric_entity("#4294967295"),
9606 None,
9607 "u32::MAX is not a scalar value"
9608 );
9609 assert_eq!(
9610 decode_numeric_entity("#4294967296"),
9611 None,
9612 "one past u32::MAX must not wrap — it must fail to parse"
9613 );
9614 assert_eq!(decode_numeric_entity("#-1"), None, "negative is rejected by u32");
9615 assert_eq!(
9616 decode_numeric_entity("#xFFFFFFFFFFFF"),
9617 None,
9618 "hex overflow is rejected, not truncated"
9619 );
9620 }
9621
9622 #[test]
9623 fn decode_numeric_entity_extremely_long_terminates() {
9624 let s = format!("#{}", "9".repeat(LONG));
9625 assert_eq!(s.len(), LONG + 1);
9626 assert_eq!(decode_numeric_entity(&s), None, "overflows u32 => None");
9627 }
9628
9629 #[test]
9634 fn decode_entities_leaves_unrecognized_sequences_verbatim() {
9635 assert_eq!(decode_entities(""), "");
9636 assert_eq!(decode_entities("&"), "&", "a bare '&' at EOF must not panic");
9637 assert_eq!(decode_entities("&;"), "&;", "empty entity body is not decoded");
9638 assert_eq!(decode_entities("&bogus;"), "&bogus;");
9639 assert_eq!(decode_entities(" "), " ", " is deliberately kept");
9640 assert_eq!(
9642 decode_entities("&averyveryverylongbody;"),
9643 "&averyveryverylongbody;"
9644 );
9645 }
9646
9647 #[test]
9648 fn decode_entities_single_pass_prevents_double_decoding() {
9649 assert_eq!(decode_entities("&lt;"), "<", "& must not re-open an entity");
9650 assert_eq!(decode_entities("<>&"'"), "<>&\"'");
9651 }
9652
9653 #[test]
9654 fn decode_entities_unicode_and_long_input_terminate() {
9655 assert_eq!(decode_entities("\u{1F600}\u{0301}\u{130}"), "\u{1F600}\u{0301}\u{130}");
9656 let amps = "&".repeat(20_000);
9659 assert_eq!(decode_entities(&s).len(), 20_000);
9660 }
9661
9662 #[test]
9663 fn prepare_string_empty_and_whitespace() {
9664 assert_eq!(prepare_string(""), "");
9665 assert_eq!(prepare_string(" "), "");
9666 assert_eq!(prepare_string("\t\n\r \n"), "");
9667 }
9668
9669 #[test]
9670 fn prepare_string_nbsp_becomes_a_space_that_survives_trim() {
9671 assert_eq!(prepare_string(" "), " ");
9672 assert_eq!(prepare_string("a b"), "a b");
9673 }
9674
9675 #[test]
9676 fn prepare_string_collapses_a_blank_line_into_a_single_return() {
9677 assert_eq!(prepare_string("a\n\nb"), "a\nb");
9678 assert_eq!(prepare_string("a\n\n\n\nb"), "a\nb", "runs of blanks collapse");
9679 }
9680
9681 #[test]
9682 fn prepare_string_unicode_and_long_input_no_panic() {
9683 assert_eq!(prepare_string("\u{1F600}"), "\u{1F600}");
9684 assert_eq!(prepare_string(" \u{130}\u{0301} "), "\u{130}\u{0301}");
9685 assert_eq!(prepare_string(&"x".repeat(LONG)).len(), LONG);
9686 }
9687
9688 #[test]
9692 fn prepare_string_joins_wrapped_lines_with_a_space() {
9693 assert_eq!(
9694 prepare_string("Hello\nworld"),
9695 "Hello world",
9696 "a single newline between words must collapse to a space, not vanish"
9697 );
9698 assert_eq!(prepare_string("a\nb\nc"), "a b c");
9699 }
9700
9701 #[test]
9706 fn parse_bool_valid_minimal_and_everything_else_is_none() {
9707 assert_eq!(parse_bool("true"), Some(true));
9708 assert_eq!(parse_bool("false"), Some(false));
9709
9710 for s in [
9711 "", " ", "\t\n", " true", "true ", "TRUE", "True", "FALSE", "1", "0", "-0", "yes",
9712 "no", "NaN", "inf", "9223372036854775807", "\u{1F600}", "true;garbage",
9713 ] {
9714 assert_eq!(parse_bool(s), None, "{s:?} must not parse as a bool");
9715 }
9716 assert_eq!(parse_bool(&"a".repeat(LONG)), None);
9717 }
9718
9719 fn args(kv: &[(&str, &str)]) -> ComponentArgumentVec {
9724 kv.iter()
9725 .map(|(n, t)| ComponentArgument {
9726 name: AzString::from(*n),
9727 arg_type: AzString::from(*t),
9728 })
9729 .collect::<Vec<_>>()
9730 .into()
9731 }
9732
9733 #[test]
9734 fn split_dynamic_string_empty_and_plain() {
9735 assert!(split_dynamic_string("").is_empty());
9736 assert_eq!(
9737 split_dynamic_string("abc"),
9738 vec![DynamicItem::Str("abc".to_string())]
9739 );
9740 }
9741
9742 #[test]
9743 fn split_dynamic_string_format_spec_is_split_on_the_first_colon() {
9744 assert_eq!(
9745 split_dynamic_string("{a:?}"),
9746 vec![DynamicItem::Var {
9747 name: "a".to_string(),
9748 format_spec: Some("?".to_string()),
9749 }]
9750 );
9751 assert_eq!(
9752 split_dynamic_string("{a:x:y}"),
9753 vec![DynamicItem::Var {
9754 name: "a".to_string(),
9755 format_spec: Some("x:y".to_string()),
9756 }],
9757 "only the FIRST colon separates name from spec"
9758 );
9759 }
9760
9761 #[test]
9762 fn split_dynamic_string_unterminated_var_stays_literal() {
9763 assert_eq!(
9765 split_dynamic_string("{unterminated"),
9766 vec![DynamicItem::Str("{unterminated".to_string())]
9767 );
9768 assert_eq!(
9770 split_dynamic_string("{a b}"),
9771 vec![DynamicItem::Str("{a b}".to_string())]
9772 );
9773 }
9774
9775 #[test]
9776 fn split_dynamic_string_unicode_and_long_input_terminate() {
9777 assert_eq!(
9778 split_dynamic_string("\u{1F600}{v}\u{130}"),
9779 vec![
9780 DynamicItem::Str("\u{1F600}".to_string()),
9781 DynamicItem::Var {
9782 name: "v".to_string(),
9783 format_spec: None
9784 },
9785 DynamicItem::Str("\u{130}".to_string()),
9786 ]
9787 );
9788 let bomb = "{a".repeat(50_000);
9791 let out = split_dynamic_string(&bomb);
9792 assert!(out.len() <= 2, "a never-closed variable collapses, got {}", out.len());
9793
9794 let braces = "{".repeat(100_000);
9795 assert!(split_dynamic_string(&braces).len() <= 1);
9796 }
9797
9798 #[test]
9799 fn format_args_dynamic_documented_example() {
9800 let vars = args(&[("a", "value1"), ("b", "value2")]);
9801 assert_eq!(
9802 format_args_dynamic("hello {a}, {b}{{ {c} }}", &vars),
9803 "hello value1, value2{ {c} }"
9804 );
9805 }
9806
9807 #[test]
9808 fn format_args_dynamic_unknown_var_is_preserved_verbatim() {
9809 let empty = no_args();
9810 assert_eq!(format_args_dynamic("{c}", &empty), "{c}");
9811 assert_eq!(
9812 format_args_dynamic("{c:?}", &empty),
9813 "{c:?}",
9814 "the format spec is re-attached when the var is unresolved"
9815 );
9816 assert_eq!(format_args_dynamic("{{}}", &empty), "{{}}");
9818 }
9819
9820 #[test]
9821 fn format_args_dynamic_variable_names_are_normalized() {
9822 let vars = args(&[("my_var", "V")]);
9823 assert_eq!(format_args_dynamic("{myVar}", &vars), "V", "camelCase is normalized");
9824 assert_eq!(format_args_dynamic("{ my-var }", &vars), "{ my-var }",
9825 "whitespace inside the braces aborts the variable scan entirely");
9826 assert_eq!(format_args_dynamic("{my-var}", &vars), "V");
9827 }
9828
9829 #[test]
9830 fn format_args_dynamic_edge_values_no_panic() {
9831 let empty = no_args();
9832 assert_eq!(format_args_dynamic("", &empty), "");
9833 assert_eq!(format_args_dynamic(" ", &empty), " ");
9834 assert_eq!(format_args_dynamic("\u{1F600}", &empty), "\u{1F600}");
9835 assert_eq!(format_args_dynamic(&"x".repeat(50_000), &empty).len(), 50_000);
9836 }
9837
9838 #[test]
9839 fn combine_and_replace_dynamic_items_on_empty_input() {
9840 assert_eq!(combine_and_replace_dynamic_items(&[], &no_args()), "");
9841 }
9842
9843 #[test]
9848 fn format_args_for_rust_code_empty_and_single_items() {
9849 assert_eq!(format_args_for_rust_code(""), "AzString::from_const_str(\"\")");
9850 assert_eq!(format_args_for_rust_code("hi"), "AzString::from_const_str(\"hi\")");
9851 assert_eq!(format_args_for_rust_code("{a}"), "a", "a lone var becomes a bare ident");
9852 assert_eq!(
9853 format_args_for_rust_code("{a:?}"),
9854 "format!(\"{:?}\", a).into()"
9855 );
9856 }
9857
9858 #[test]
9859 fn format_args_for_rust_code_multi_item_builds_a_format_call() {
9860 assert_eq!(
9861 format_args_for_rust_code("x={a} y={b}"),
9862 "format!(\"x={a} y={b}\", a, b).into()"
9863 );
9864 }
9865
9866 #[test]
9867 fn format_args_for_rust_code_escapes_quotes_in_literals() {
9868 let out = format_args_for_rust_code("say \"hi\" {a}");
9869 assert!(
9870 out.contains("say \\\"hi\\\""),
9871 "double quotes must be escaped for the emitted literal, got {out}"
9872 );
9873 }
9874
9875 #[test]
9876 fn compile_and_format_dynamic_items_edge_values() {
9877 assert_eq!(
9878 compile_and_format_dynamic_items(&[]),
9879 "AzString::from_const_str(\"\")"
9880 );
9881 assert_eq!(
9882 compile_and_format_dynamic_items(&[DynamicItem::Str(String::new())]),
9883 "AzString::from_const_str(\"\")"
9884 );
9885 assert_eq!(
9886 compile_and_format_dynamic_items(&[DynamicItem::Var {
9887 name: " spaced ".to_string(),
9888 format_spec: None,
9889 }]),
9890 "spaced",
9891 "the var name is trimmed + normalized"
9892 );
9893 }
9894
9895 #[test]
9900 fn cap_first_edge_inputs() {
9901 assert_eq!(cap_first(""), "", "empty input must not panic");
9902 assert_eq!(cap_first("h1"), "H1");
9903 assert_eq!(cap_first("button"), "Button");
9904 assert_eq!(cap_first("A"), "A", "already-uppercase is idempotent");
9905 assert_eq!(cap_first("\u{1F600}x"), "\u{1F600}x", "emoji has no uppercase form");
9906 assert_eq!(cap_first("\u{df}x"), "SSx");
9908 assert_eq!(cap_first(&"a".repeat(10_000)).len(), 10_000);
9909 }
9910
9911 #[test]
9912 fn camel_to_snake_documented_forms() {
9913 assert_eq!(camel_to_snake("ButtonNoA11y"), "button_no_a11y");
9914 assert_eq!(camel_to_snake("PWithText"), "p_with_text");
9915 assert_eq!(camel_to_snake("ANoA11y"), "a_no_a11y");
9916 assert_eq!(camel_to_snake("H1WithText"), "h1_with_text");
9917 assert_eq!(camel_to_snake("Div"), "div");
9918 }
9919
9920 #[test]
9921 fn camel_to_snake_edge_inputs() {
9922 assert_eq!(camel_to_snake(""), "");
9923 assert_eq!(camel_to_snake("A"), "a");
9924 assert_eq!(camel_to_snake("AB"), "ab", "an all-caps run is not split");
9925 assert_eq!(camel_to_snake("ABc"), "a_bc", "a caps run splits before the last cap");
9926 assert_eq!(camel_to_snake("\u{1F600}"), "\u{1F600}");
9927 assert_eq!(camel_to_snake(&"a".repeat(10_000)).len(), 10_000);
9928 }
9929
9930 #[test]
9931 fn esc_lit_escapes_backslash_before_quote() {
9932 assert_eq!(esc_lit(""), "");
9933 assert_eq!(esc_lit("plain"), "plain");
9934 assert_eq!(esc_lit("a\"b"), "a\\\"b");
9935 assert_eq!(esc_lit("a\\b"), "a\\\\b");
9936 assert_eq!(esc_lit("\\\""), "\\\\\\\"");
9938 assert_eq!(esc_lit("\u{1F600}"), "\u{1F600}");
9939 }
9940
9941 #[test]
9942 fn c_creator_suffix_edge_inputs() {
9943 assert_eq!(c_creator_suffix(""), "Div", "empty debug name falls back to Div");
9944 assert_eq!(c_creator_suffix("Div"), "Div");
9945 assert_eq!(c_creator_suffix("H1"), "H1");
9946 assert_eq!(c_creator_suffix("BlockQuote"), "Blockquote");
9947 assert_eq!(c_creator_suffix("FigCaption"), "Figcaption");
9948 assert_eq!(c_creator_suffix("\u{1F600}"), "\u{1F600}");
9949 }
9950
9951 #[test]
9956 fn safe_container_tag_falls_back_to_div_for_arg_taking_widgets() {
9957 assert_eq!(safe_container_tag(""), "Div");
9958 assert_eq!(safe_container_tag("Div"), "Div");
9959 assert_eq!(safe_container_tag("Span"), "Span");
9960 assert_eq!(safe_container_tag("H1"), "H1");
9961 assert_eq!(safe_container_tag("Button"), "Div");
9963 assert_eq!(safe_container_tag("Input"), "Div");
9964 assert_eq!(safe_container_tag("A"), "Div");
9965 assert_eq!(safe_container_tag("\u{1F600}"), "Div");
9966 assert_eq!(safe_container_tag(&"z".repeat(10_000)), "Div");
9967 }
9968
9969 #[test]
9976 fn safe_container_tag_matches_the_real_nodetype_debug_names() {
9977 for tag in [
9978 "blockquote",
9979 "figcaption",
9980 "thead",
9981 "tbody",
9982 "tfoot",
9983 "colgroup",
9984 ] {
9985 let dbg = format!("{:?}", tag_to_node_type(tag));
9986 assert_ne!(
9987 safe_container_tag(&dbg),
9988 "Div",
9989 "<{tag}> (NodeType debug name {dbg:?}) is a pure container and must keep \
9990 its own creator instead of degrading to a div"
9991 );
9992 }
9993 }
9994
9995 #[test]
10000 fn fmt_f32_lit_zero_and_negative() {
10001 assert_eq!(fmt_f32_lit(0.0), "0.0", "an integral value gains a decimal point");
10002 assert_eq!(fmt_f32_lit(-0.0), "-0.0");
10003 assert_eq!(fmt_f32_lit(-1.0), "-1.0");
10004 assert_eq!(fmt_f32_lit(1.5), "1.5");
10005 assert_eq!(fmt_f32_lit(-1.5), "-1.5");
10006 }
10007
10008 #[test]
10009 fn fmt_f32_lit_min_max_stay_parseable_float_literals() {
10010 for f in [f32::MAX, f32::MIN, f32::MIN_POSITIVE, f32::EPSILON] {
10011 let s = fmt_f32_lit(f);
10012 assert_eq!(
10013 s.parse::<f32>(),
10014 Ok(f),
10015 "{f:e} must round-trip through its emitted literal ({s})"
10016 );
10017 }
10018 }
10019
10020 #[test]
10021 fn fmt_f32_lit_nan_inf_produce_a_defined_result_and_do_not_panic() {
10022 assert_eq!(fmt_f32_lit(f32::NAN), "NaN");
10026 assert_eq!(fmt_f32_lit(f32::INFINITY), "inf");
10027 assert_eq!(fmt_f32_lit(f32::NEG_INFINITY), "-inf");
10028 }
10029
10030 #[test]
10036 fn node_direct_text_trims_and_skips_elements() {
10037 assert_eq!(node_direct_text(&XmlNode::default()), "");
10038 assert_eq!(node_direct_text(&node("p", &[], vec![txt(" Go ")])), "Go");
10039 assert_eq!(
10040 node_direct_text(&node(
10041 "p",
10042 &[],
10043 vec![txt("a"), elem(node("b", &[], vec![txt("IGNORED")])), txt("b")]
10044 )),
10045 "a b",
10046 "direct text children are joined with a single space"
10047 );
10048 assert_eq!(
10049 node_direct_text(&node("p", &[], vec![txt(" "), txt("\t\n")])),
10050 "",
10051 "whitespace-only children are dropped"
10052 );
10053 }
10054
10055 #[test]
10056 fn node_aria_label_ignores_empty_and_whitespace() {
10057 assert_eq!(node_aria_label(&XmlNode::default()), None);
10058 assert_eq!(node_aria_label(&node("b", &[("aria-label", "")], vec![])), None);
10059 assert_eq!(node_aria_label(&node("b", &[("aria-label", " ")], vec![])), None);
10060 assert_eq!(
10061 node_aria_label(&node("b", &[("aria-label", " Save ")], vec![])),
10062 Some("Save".to_string())
10063 );
10064 }
10065
10066 #[test]
10067 fn node_attr_or_returns_the_default_when_absent() {
10068 let n = node("a", &[("href", "/x"), ("empty", "")], vec![]);
10069 assert_eq!(node_attr_or(&n, "href", "FALLBACK"), "/x");
10070 assert_eq!(node_attr_or(&n, "missing", "FALLBACK"), "FALLBACK");
10071 assert_eq!(
10072 node_attr_or(&n, "empty", "FALLBACK"),
10073 "",
10074 "a present-but-empty attribute wins over the default"
10075 );
10076 assert_eq!(node_attr_or(&XmlNode::default(), "x", ""), "");
10077 }
10078
10079 #[test]
10080 fn node_attr_f32_zero_negative_and_defaults() {
10081 let n = node(
10082 "meter",
10083 &[("zero", "0"), ("negzero", "-0"), ("neg", "-2.5"), ("pad", " 1.5 ")],
10084 vec![],
10085 );
10086 assert_eq!(node_attr_f32(&n, "zero", 9.0), 0.0);
10087 assert!(node_attr_f32(&n, "negzero", 9.0).is_sign_negative());
10088 assert_eq!(node_attr_f32(&n, "neg", 9.0), -2.5);
10089 assert_eq!(node_attr_f32(&n, "pad", 9.0), 1.5, "the value is trimmed first");
10090 assert_eq!(node_attr_f32(&n, "missing", 9.0), 9.0);
10091 }
10092
10093 #[test]
10094 fn node_attr_f32_unparsable_falls_back_and_min_max_saturate() {
10095 let n = node(
10096 "meter",
10097 &[
10098 ("junk", "abc"),
10099 ("empty", ""),
10100 ("huge", "1e400"),
10101 ("tiny", "-1e400"),
10102 ("big", "340282350000000000000000000000000000000"),
10103 ],
10104 vec![],
10105 );
10106 assert_eq!(node_attr_f32(&n, "junk", 7.0), 7.0);
10107 assert_eq!(node_attr_f32(&n, "empty", 7.0), 7.0);
10108 assert!(
10109 node_attr_f32(&n, "huge", 7.0).is_infinite(),
10110 "an out-of-range literal saturates to inf, it does not panic"
10111 );
10112 assert_eq!(node_attr_f32(&n, "tiny", 7.0), f32::NEG_INFINITY);
10113 assert_eq!(node_attr_f32(&n, "big", 7.0), f32::MAX);
10114 }
10115
10116 #[test]
10117 fn node_attr_f32_accepts_nan_and_inf_spellings() {
10118 let n = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10121 assert!(node_attr_f32(&n, "value", 0.0).is_nan());
10122 assert_eq!(node_attr_f32(&n, "max", 1.0), f32::INFINITY);
10123 }
10124
10125 #[test]
10126 fn node_attr_f32_nan_default_is_returned_verbatim() {
10127 assert!(node_attr_f32(&XmlNode::default(), "x", f32::NAN).is_nan());
10128 assert_eq!(node_attr_f32(&XmlNode::default(), "x", f32::INFINITY), f32::INFINITY);
10129 }
10130
10131 #[test]
10132 fn first_caption_text_edges() {
10133 assert_eq!(first_caption_text(&XmlNode::default()), None);
10134 assert_eq!(
10135 first_caption_text(&node("table", &[], vec![elem(node("caption", &[], vec![]))])),
10136 None,
10137 "an empty caption yields None"
10138 );
10139 assert_eq!(
10140 first_caption_text(&node(
10141 "table",
10142 &[],
10143 vec![elem(node("CAPTION", &[], vec![txt(" Hi ")]))]
10144 )),
10145 Some("Hi".to_string()),
10146 "the tag match is ASCII-case-insensitive and the text is trimmed"
10147 );
10148 }
10149
10150 #[test]
10155 fn analyze_node_ctor_plain_for_unknown_and_empty_tags() {
10156 assert!(matches!(analyze_node_ctor("div", &XmlNode::default()), NodeCtor::Plain));
10157 assert!(matches!(analyze_node_ctor("", &XmlNode::default()), NodeCtor::Plain));
10158 assert!(matches!(
10159 analyze_node_ctor("\u{1F600}", &XmlNode::default()),
10160 NodeCtor::Plain
10161 ));
10162 let plain = analyze_node_ctor("div", &XmlNode::default());
10163 assert_eq!(plain.render_rust(), None);
10164 assert_eq!(plain.render_c(), None);
10165 assert_eq!(plain.render_fluent(&CompileTarget::Cpp), None);
10166 assert!(!plain.consumes_text());
10167 assert!(!plain.skip_caption());
10168 }
10169
10170 #[test]
10171 fn analyze_node_ctor_with_text_tier_requires_actual_text() {
10172 assert!(matches!(analyze_node_ctor("p", &XmlNode::default()), NodeCtor::Plain));
10175 let mixed = node("p", &[], vec![txt("a"), elem(XmlNode::create("span"))]);
10177 assert!(matches!(analyze_node_ctor("p", &mixed), NodeCtor::Plain));
10178
10179 let pure = node("p", &[], vec![txt(" Hello ")]);
10180 let ctor = analyze_node_ctor("p", &pure);
10181 assert!(ctor.consumes_text(), "the text is folded into the constructor");
10182 assert_eq!(
10183 ctor.render_rust().as_deref(),
10184 Some("Dom::create_p_with_text(AzString::from(\"Hello\"))")
10185 );
10186 assert_eq!(
10187 ctor.render_c().as_deref(),
10188 Some("AzDom_createPWithText(AZ_STR(\"Hello\"))")
10189 );
10190 assert_eq!(
10191 ctor.render_fluent(&CompileTarget::Python).as_deref(),
10192 Some("azul.Dom.create_p_with_text(\"Hello\")")
10193 );
10194 }
10195
10196 #[test]
10197 fn analyze_node_ctor_button_with_and_without_aria() {
10198 let plain_btn = node("button", &[], vec![txt("Go")]);
10199 assert_eq!(
10200 analyze_node_ctor("button", &plain_btn).render_rust().as_deref(),
10201 Some("Dom::create_button_no_a11y(AzString::from(\"Go\"))")
10202 );
10203
10204 let aria_btn = node("button", &[("aria-label", "Save")], vec![txt("Go")]);
10205 assert_eq!(
10206 analyze_node_ctor("button", &aria_btn).render_rust().as_deref(),
10207 Some(
10208 "Dom::create_button(AzString::from(\"Go\"), \
10209 SmallAriaInfo::label(AzString::from(\"Save\")))"
10210 )
10211 );
10212 }
10213
10214 #[test]
10215 fn analyze_node_ctor_escapes_quotes_and_backslashes_in_text() {
10216 let btn = node("button", &[], vec![txt("say \"hi\"\\now")]);
10217 let rust = analyze_node_ctor("button", &btn).render_rust().expect("semantic");
10218 assert!(
10219 rust.contains("say \\\"hi\\\"\\\\now"),
10220 "quotes and backslashes must be escaped for the literal, got {rust}"
10221 );
10222 }
10223
10224 #[test]
10225 fn analyze_node_ctor_anchor_uses_option_string_when_it_has_no_text() {
10226 let bare = node("a", &[], vec![]);
10227 assert_eq!(
10228 analyze_node_ctor("a", &bare).render_rust().as_deref(),
10229 Some("Dom::create_a_no_a11y(AzString::from(\"\"), OptionString::None)"),
10230 "a missing href defaults to an empty string, missing text to OptionString::None"
10231 );
10232
10233 let full = node("a", &[("href", "/x")], vec![txt("Home")]);
10234 assert_eq!(
10235 analyze_node_ctor("a", &full).render_rust().as_deref(),
10236 Some(
10237 "Dom::create_a_no_a11y(AzString::from(\"/x\"), \
10238 OptionString::Some(AzString::from(\"Home\")))"
10239 )
10240 );
10241 assert_eq!(
10242 analyze_node_ctor("a", &full).render_c().as_deref(),
10243 Some("AzDom_createANoA11y(AZ_STR(\"/x\"), AzOptionString_some(AZ_STR(\"Home\")))")
10244 );
10245 }
10246
10247 #[test]
10248 fn analyze_node_ctor_table_aria_form_skips_the_literal_caption() {
10249 let t = node(
10250 "table",
10251 &[("aria-label", "Prices")],
10252 vec![elem(node("caption", &[], vec![txt("Q1")]))],
10253 );
10254 let ctor = analyze_node_ctor("table", &t);
10255 assert!(ctor.skip_caption(), "the aria form injects its own caption child");
10256 assert_eq!(
10257 ctor.render_rust().as_deref(),
10258 Some(
10259 "Dom::create_table(AzString::from(\"Q1\"), \
10260 SmallAriaInfo::label(AzString::from(\"Prices\")))"
10261 )
10262 );
10263
10264 let plain = analyze_node_ctor("table", &node("table", &[], vec![]));
10265 assert!(!plain.skip_caption());
10266 assert_eq!(plain.render_rust().as_deref(), Some("Dom::create_table_no_a11y()"));
10267 }
10268
10269 #[test]
10270 fn analyze_node_ctor_scalar_widgets_use_defaults_and_emit_float_literals() {
10271 let p = node("progress", &[], vec![]);
10272 assert_eq!(
10273 analyze_node_ctor("progress", &p).render_rust().as_deref(),
10274 Some("Dom::create_progress_no_a11y(0.0, 1.0)"),
10275 "missing value/max fall back to 0.0 / 1.0 as float literals"
10276 );
10277
10278 let m = node("meter", &[("value", "5"), ("min", "-1"), ("max", "10")], vec![]);
10279 assert_eq!(
10280 analyze_node_ctor("meter", &m).render_c().as_deref(),
10281 Some("AzDom_createMeterNoA11y(5.0f, -1.0f, 10.0f)")
10282 );
10283 }
10284
10285 #[test]
10288 fn analyze_node_ctor_non_finite_attributes_emit_non_finite_literals() {
10289 let p = node("progress", &[("value", "NaN"), ("max", "inf")], vec![]);
10290 let rust = analyze_node_ctor("progress", &p).render_rust().expect("semantic");
10291 assert_eq!(rust, "Dom::create_progress_no_a11y(NaN, inf)");
10292 }
10293
10294 #[test]
10295 fn ctor_arg_render_targets_are_distinct() {
10296 let s = CtorArg::Str("a\"b".to_string());
10297 assert_eq!(s.render_rust(), "AzString::from(\"a\\\"b\")");
10298 assert_eq!(s.render_c(), "AZ_STR(\"a\\\"b\")");
10299 assert_eq!(s.render_cpp(), "String(\"a\\\"b\")");
10300 assert_eq!(s.render_python(), "\"a\\\"b\"");
10301
10302 assert_eq!(CtorArg::OptNone.render_rust(), "OptionString::None");
10303 assert_eq!(CtorArg::OptNone.render_c(), "AzOptionString_none()");
10304 assert_eq!(CtorArg::OptNone.render_cpp(), "OptionString::none()");
10305 assert_eq!(CtorArg::OptNone.render_python(), "azul.OptionString.none()");
10306
10307 assert_eq!(CtorArg::Float(0.0).render_rust(), "0.0");
10308 assert_eq!(CtorArg::Float(0.0).render_c(), "0.0f");
10309 assert_eq!(CtorArg::Float(f32::NAN).render_c(), "NaNf");
10310 }
10311
10312 #[test]
10313 fn node_ctor_render_fluent_returns_none_for_non_fluent_targets() {
10314 let ctor = analyze_node_ctor("p", &node("p", &[], vec![txt("x")]));
10315 assert!(ctor.render_fluent(&CompileTarget::Rust).is_none());
10316 assert!(ctor.render_fluent(&CompileTarget::C).is_none());
10317 assert!(ctor.render_fluent(&CompileTarget::Cpp).is_some());
10318 assert!(ctor.render_fluent(&CompileTarget::Python).is_some());
10319 }
10320
10321 #[test]
10326 fn format_component_args_empty_and_ordering() {
10327 assert_eq!(format_component_args(&no_args()), "");
10328 assert_eq!(
10330 format_component_args(&args(&[("a", "u32"), ("b", "String")])),
10331 "b: String, a: u32"
10332 );
10333 }
10334
10335 #[test]
10336 fn format_component_args_edge_values_no_panic() {
10337 let a = args(&[("", ""), ("\u{1F600}", "\u{130}")]);
10338 let out = format_component_args(&a);
10339 assert!(out.contains(": "), "still emits `name: type` pairs, got {out:?}");
10340 }
10341
10342 #[test]
10343 fn compile_component_emits_a_render_fn() {
10344 let ca = ComponentArguments {
10345 args: args(&[("count", "u32")]),
10346 accepts_text: false,
10347 };
10348 let out = compile_component("MyWidget", &ca, "Dom::create_div()");
10349 assert!(out.contains("pub fn render(count: u32) -> Dom {"), "got:\n{out}");
10350 assert!(out.contains("#[inline]"), "a one-line body is inlined");
10351 }
10352
10353 #[test]
10354 fn compile_component_accepts_text_prepends_the_text_param() {
10355 let ca = ComponentArguments {
10356 args: args(&[("count", "u32")]),
10357 accepts_text: true,
10358 };
10359 let out = compile_component("my-widget", &ca, "Dom::create_div()");
10360 assert!(
10361 out.contains("pub fn render(text: AzString, count: u32) -> Dom {"),
10362 "got:\n{out}"
10363 );
10364
10365 let ca_no_args = ComponentArguments {
10366 args: no_args(),
10367 accepts_text: true,
10368 };
10369 let out = compile_component("w", &ca_no_args, "Dom::create_div()");
10370 assert!(
10371 out.contains("pub fn render(text: AzString) -> Dom {"),
10372 "no trailing comma when there are no extra args, got:\n{out}"
10373 );
10374 }
10375
10376 #[test]
10377 fn compile_component_empty_name_and_body_no_panic() {
10378 let ca = ComponentArguments::default();
10379 let out = compile_component("", &ca, "");
10380 assert!(out.contains("pub fn render() -> Dom {"), "got:\n{out}");
10381 }
10382
10383 #[test]
10384 fn compile_components_of_an_empty_list_is_empty() {
10385 assert_eq!(compile_components(Vec::new()), "");
10386 }
10387
10388 #[test]
10393 fn parse_svg_float_none_empty_whitespace_garbage() {
10394 assert_eq!(parse_svg_float(None), None);
10395 let empty = AzString::from("");
10396 assert_eq!(parse_svg_float(Some(&empty)), None);
10397 let ws = AzString::from(" \t\n");
10398 assert_eq!(parse_svg_float(Some(&ws)), None);
10399 let junk = AzString::from("10px");
10400 assert_eq!(parse_svg_float(Some(&junk)), None, "units are not stripped");
10401 let uni = AzString::from("\u{1F600}");
10402 assert_eq!(parse_svg_float(Some(&uni)), None);
10403 }
10404
10405 #[test]
10406 fn parse_svg_float_valid_and_boundary_numbers() {
10407 let padded = AzString::from(" 1.5 ");
10408 assert_eq!(parse_svg_float(Some(&padded)), Some(1.5), "value is trimmed");
10409 let zero = AzString::from("0");
10410 assert_eq!(parse_svg_float(Some(&zero)), Some(0.0));
10411 let negzero = AzString::from("-0");
10412 assert!(parse_svg_float(Some(&negzero)).unwrap().is_sign_negative());
10413 let huge = AzString::from("1e400");
10414 assert!(
10415 parse_svg_float(Some(&huge)).unwrap().is_infinite(),
10416 "overflow saturates to inf rather than erroring"
10417 );
10418 let nan = AzString::from("NaN");
10419 assert!(parse_svg_float(Some(&nan)).unwrap().is_nan());
10420 let inf = AzString::from("-inf");
10421 assert_eq!(parse_svg_float(Some(&inf)), Some(f32::NEG_INFINITY));
10422 }
10423
10424 #[test]
10425 fn parse_svg_points_rejects_degenerate_input() {
10426 assert!(parse_svg_points("", false).is_none());
10427 assert!(parse_svg_points(" ", false).is_none());
10428 assert!(parse_svg_points("garbage", false).is_none());
10429 assert!(parse_svg_points("1 2", false).is_none(), "a single point is not a line");
10430 assert!(
10431 parse_svg_points("1 2 3", false).is_none(),
10432 "an odd coordinate count is rejected"
10433 );
10434 assert!(parse_svg_points("\u{1F600}", false).is_none());
10435 }
10436
10437 #[test]
10438 fn parse_svg_points_valid_minimal_and_close() {
10439 let open = parse_svg_points("0,0 10,0", false).expect("two points => one line");
10440 assert_eq!(open.rings.as_ref().len(), 1);
10441 assert_eq!(open.rings.as_ref()[0].items.as_ref().len(), 1);
10442
10443 let closed = parse_svg_points("0,0 10,0 10,10", true).expect("triangle");
10445 assert_eq!(
10446 closed.rings.as_ref()[0].items.as_ref().len(),
10447 3,
10448 "2 segments + 1 closing segment"
10449 );
10450
10451 let already = parse_svg_points("0,0 10,0 0,0", true).expect("closed ring");
10453 assert_eq!(already.rings.as_ref()[0].items.as_ref().len(), 2);
10454 }
10455
10456 #[test]
10457 fn parse_svg_points_skips_unparsable_tokens_and_handles_boundaries() {
10458 let p = parse_svg_points("0,0 junk 10,0", false).expect("junk token dropped");
10460 assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 1);
10461
10462 let nan = parse_svg_points("NaN,0 1,1", false).expect("NaN is a parseable f32");
10463 assert_eq!(nan.rings.as_ref()[0].items.as_ref().len(), 1);
10464 }
10465
10466 #[test]
10467 fn parse_svg_points_extremely_long_terminates() {
10468 let pts = "1,2 ".repeat(20_000);
10469 let p = parse_svg_points(&pts, false).expect("20k points");
10470 assert_eq!(p.rings.as_ref()[0].items.as_ref().len(), 19_999);
10471 }
10472
10473 #[test]
10478 fn compact_dom_builder_new_and_with_capacity_start_empty() {
10479 for b in [
10480 CompactDomBuilder::new(),
10481 CompactDomBuilder::with_capacity(0),
10482 CompactDomBuilder::with_capacity(1),
10483 CompactDomBuilder::with_capacity(4096),
10484 ] {
10485 let fd = b.finish();
10486 assert_eq!(fd.node_hierarchy.as_ref().len(), 0);
10487 assert_eq!(fd.node_data.as_ref().len(), 0);
10488 assert_eq!(fd.css.as_ref().len(), 0);
10489 }
10490 assert_eq!(
10491 CompactDomBuilder::default().finish().node_data.as_ref().len(),
10492 0
10493 );
10494 }
10495
10496 #[test]
10497 fn compact_dom_builder_close_node_on_an_empty_stack_is_a_no_op() {
10498 let mut b = CompactDomBuilder::new();
10499 b.close_node();
10500 b.close_node();
10501 assert_eq!(b.finish().node_hierarchy.as_ref().len(), 0, "no panic, no nodes");
10502 }
10503
10504 #[test]
10505 fn compact_dom_builder_keeps_hierarchy_and_data_parallel() {
10506 let mut b = CompactDomBuilder::new();
10507 b.open_node(NodeData::create_node(NodeType::Html));
10508 b.add_leaf(NodeData::create_text("a"));
10509 b.add_leaf(NodeData::create_text("b"));
10510 b.close_node();
10511 let fd = b.finish();
10512 assert_eq!(fd.node_data.as_ref().len(), 3);
10513 assert_eq!(
10514 fd.node_hierarchy.as_ref().len(),
10515 fd.node_data.as_ref().len(),
10516 "the two arenas must stay the same length"
10517 );
10518 }
10519
10520 #[test]
10521 fn compact_dom_builder_unclosed_node_still_finishes() {
10522 let mut b = CompactDomBuilder::new();
10523 b.open_node(NodeData::create_node(NodeType::Div));
10524 let fd = b.finish();
10526 assert_eq!(fd.node_hierarchy.as_ref().len(), 1);
10527 assert_eq!(
10528 fd.node_hierarchy.as_ref()[0].last_child, 0,
10529 "last_child stays unset when close_node() is never called"
10530 );
10531 }
10532
10533 #[test]
10534 fn compact_dom_builder_add_css_accepts_zero_and_usize_max_node_ids() {
10535 let mut b = CompactDomBuilder::new();
10536 b.add_css(0, Css::empty());
10537 b.add_css(usize::MAX, Css::empty());
10538 let fd = b.finish();
10539 assert_eq!(fd.css.as_ref().len(), 2);
10540 assert_eq!(fd.css.as_ref()[0].node_id, 0);
10541 assert_eq!(
10542 fd.css.as_ref()[1].node_id,
10543 usize::MAX,
10544 "an out-of-range node id is stored verbatim (no bounds check, no panic)"
10545 );
10546 }
10547
10548 #[test]
10553 fn xml_node_to_dom_fast_depth_zero_builds_children() {
10554 let map = ComponentMap::default();
10555 let n = node("div", &[], vec![txt("hi"), elem(XmlNode::create("span"))]);
10556 let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10557 assert_eq!(dom.children.as_ref().len(), 2);
10558 }
10559
10560 #[test]
10561 fn xml_node_to_dom_fast_at_and_past_the_depth_cap_truncates_instead_of_panicking() {
10562 let map = ComponentMap::default();
10563 let n = node("div", &[], vec![txt("hi")]);
10564
10565 let at_cap = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH).expect("ok");
10566 assert!(
10567 at_cap.children.as_ref().is_empty(),
10568 "at the cap the node is emitted without children"
10569 );
10570
10571 let saturated = xml_node_to_dom_fast(&n, &map, false, usize::MAX)
10572 .expect("usize::MAX depth must not overflow when computing depth + 1");
10573 assert!(saturated.children.as_ref().is_empty());
10574
10575 let below = xml_node_to_dom_fast(&n, &map, false, MAX_XML_NESTING_DEPTH - 1).expect("ok");
10576 assert_eq!(below.children.as_ref().len(), 1, "one below the cap still recurses");
10577 }
10578
10579 #[test]
10580 fn xml_node_to_fast_dom_at_the_depth_cap_still_emits_the_node() {
10581 let map = ComponentMap::default();
10582 let n = node("div", &[], vec![txt("hi")]);
10583
10584 let mut b = CompactDomBuilder::new();
10585 xml_node_to_fast_dom(&n, &map, false, &mut b, usize::MAX).expect("no overflow");
10586 let fd = b.finish();
10587 assert_eq!(
10588 fd.node_data.as_ref().len(),
10589 1,
10590 "the node itself is still opened+closed, only its children are dropped"
10591 );
10592
10593 let mut b2 = CompactDomBuilder::new();
10594 xml_node_to_fast_dom(&n, &map, false, &mut b2, 0).expect("ok");
10595 assert_eq!(b2.finish().node_data.as_ref().len(), 2, "node + text child");
10596 }
10597
10598 #[test]
10599 fn apply_xml_node_attributes_extreme_tabindex_does_not_panic() {
10600 let map = ComponentMap::default();
10601 for v in [
10602 "0",
10603 "-1",
10604 "2147483647",
10605 "9223372036854775807",
10606 "-9223372036854775808",
10607 "99999999999999999999999999999999",
10608 "abc",
10609 "",
10610 "\u{1F600}",
10611 ] {
10612 let n = node("div", &[("tabindex", v), ("focusable", "true")], vec![]);
10613 assert!(
10614 xml_node_to_dom_fast(&n, &map, false, 0).is_ok(),
10615 "tabindex={v:?} must not panic"
10616 );
10617 }
10618 }
10619
10620 #[test]
10621 fn apply_xml_node_attributes_img_width_height_garbage_falls_back_to_zero() {
10622 let map = ComponentMap::default();
10623 let n = node(
10624 "img",
10625 &[("src", "a.png"), ("width", "-5"), ("height", "not-a-number")],
10626 vec![],
10627 );
10628 let dom = xml_node_to_dom_fast(&n, &map, false, 0).expect("ok");
10629 match dom.root.get_node_type() {
10630 NodeType::Image(_) => {}
10631 other => panic!("expected an Image node, got {other:?}"),
10632 }
10633 }
10634
10635 #[test]
10640 fn set_stringified_attributes_zero_tabs_and_empty_attrs() {
10641 let mut s = String::new();
10642 set_stringified_attributes(&mut s, &attrs(&[]), &no_args(), 0);
10643 assert_eq!(s, "", "nothing to emit for an attribute-less node");
10644 }
10645
10646 #[test]
10647 fn set_stringified_attributes_splits_ids_and_classes_on_whitespace() {
10648 let mut s = String::new();
10649 set_stringified_attributes(
10650 &mut s,
10651 &attrs(&[("id", "a b"), ("class", "c\td")]),
10652 &no_args(),
10653 0,
10654 );
10655 assert!(s.contains(".with_id(\"a\")"), "got {s:?}");
10656 assert!(s.contains(".with_id(\"b\")"));
10657 assert!(s.contains(".with_class(\"c\")"));
10658 assert!(s.contains(".with_class(\"d\")"));
10659 }
10660
10661 #[test]
10662 fn set_stringified_attributes_tabindex_boundaries() {
10663 let cases: &[(&str, &str)] = &[
10664 ("0", "TabIndex::Auto"),
10665 ("5", "TabIndex::OverrideInParent(5)"),
10666 ("-1", "TabIndex::NoKeyboardFocus"),
10667 ];
10668 for (val, expected) in cases {
10669 let mut s = String::new();
10670 set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10671 assert!(s.contains(expected), "tabindex={val:?} => {s:?}");
10672 }
10673
10674 for val in ["abc", "", "99999999999999999999999999999999", "1.5"] {
10676 let mut s = String::new();
10677 set_stringified_attributes(&mut s, &attrs(&[("tabindex", val)]), &no_args(), 0);
10678 assert!(
10679 !s.contains("TabIndex"),
10680 "tabindex={val:?} must be ignored, got {s:?}"
10681 );
10682 }
10683 }
10684
10685 #[test]
10686 fn set_stringified_attributes_focusable_only_accepts_exact_true_false() {
10687 let mut s = String::new();
10688 set_stringified_attributes(&mut s, &attrs(&[("focusable", "true")]), &no_args(), 0);
10689 assert!(s.contains("TabIndex::Auto"));
10690
10691 let mut s = String::new();
10692 set_stringified_attributes(&mut s, &attrs(&[("focusable", "false")]), &no_args(), 0);
10693 assert!(s.contains("TabIndex::NoKeyboardFocus"));
10694
10695 let mut s = String::new();
10696 set_stringified_attributes(&mut s, &attrs(&[("focusable", "TRUE")]), &no_args(), 0);
10697 assert!(s.is_empty(), "casing other than `true`/`false` is ignored, got {s:?}");
10698 }
10699
10700 #[test]
10701 fn set_stringified_attributes_large_tab_depth_does_not_overflow() {
10702 let mut s = String::new();
10705 set_stringified_attributes(&mut s, &attrs(&[("id", "x")]), &no_args(), 1_000);
10706 assert!(s.contains(".with_id(\"x\")"));
10707 assert!(s.len() > 4_000, "the 1000-level indent is actually emitted");
10708 }
10709
10710 fn refs(v: &[CssPathSelector]) -> Vec<&CssPathSelector> {
10715 v.iter().collect()
10716 }
10717
10718 #[test]
10719 fn group_matches_global_matches_at_any_index() {
10720 let a = vec![CssPathSelector::Global];
10721 assert!(group_matches(&refs(&a), &[], 0, 0));
10722 assert!(
10723 group_matches(&refs(&a), &[], usize::MAX, usize::MAX),
10724 "usize::MAX indices must not overflow"
10725 );
10726 }
10727
10728 #[test]
10729 fn group_matches_type_class_id() {
10730 let div = vec![CssPathSelector::Type(NodeTypeTag::Div)];
10731 let p = vec![CssPathSelector::Type(NodeTypeTag::P)];
10732 assert!(group_matches(&refs(&div), &refs(&div), 0, 1));
10733 assert!(!group_matches(&refs(&div), &refs(&p), 0, 1));
10734 assert!(!group_matches(&refs(&div), &[], 0, 1), "an empty haystack never matches");
10735
10736 let cls = vec![CssPathSelector::Class(AzString::from("x"))];
10737 assert!(group_matches(&refs(&cls), &refs(&cls), 0, 1));
10738 let id = vec![CssPathSelector::Id(AzString::from("x"))];
10739 assert!(!group_matches(&refs(&id), &refs(&cls), 0, 1), "an id is not a class");
10740 }
10741
10742 #[test]
10743 fn group_matches_first_and_last_pseudo_at_boundaries() {
10744 let first = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::First)];
10745 assert!(group_matches(&refs(&first), &[], 0, 10));
10746 assert!(!group_matches(&refs(&first), &[], 1, 10));
10747
10748 let last = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last)];
10749 assert!(group_matches(&refs(&last), &[], 9, 10));
10750 assert!(!group_matches(&refs(&last), &[], 8, 10));
10751 assert!(
10752 group_matches(&refs(&last), &[], 0, 0),
10753 "parent_children == 0 saturates to 0, so index 0 counts as last"
10754 );
10755 }
10756
10757 #[test]
10758 fn group_matches_nth_child_even_odd_and_number() {
10759 let even = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10760 CssNthChildSelector::Even,
10761 ))];
10762 assert!(group_matches(&refs(&even), &[], 0, 0));
10763 assert!(!group_matches(&refs(&even), &[], 1, 0));
10764 assert!(
10765 !group_matches(&refs(&even), &[], usize::MAX, 0),
10766 "usize::MAX is odd"
10767 );
10768
10769 let odd = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10770 CssNthChildSelector::Odd,
10771 ))];
10772 assert!(group_matches(&refs(&odd), &[], 1, 0));
10773 assert!(!group_matches(&refs(&odd), &[], 2, 0));
10774
10775 let n = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10776 CssNthChildSelector::Number(u32::MAX),
10777 ))];
10778 assert!(group_matches(&refs(&n), &[], u32::MAX as usize, 0));
10779 assert!(!group_matches(&refs(&n), &[], 0, 0));
10780 }
10781
10782 #[test]
10783 fn group_matches_nth_child_pattern_zero_repeat_does_not_divide_by_zero() {
10784 let zero = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10785 CssNthChildSelector::Pattern(CssNthChildPattern {
10786 pattern_repeat: 0,
10787 offset: 0,
10788 }),
10789 ))];
10790 assert!(group_matches(&refs(&zero), &[], 0, 0));
10792 assert!(!group_matches(&refs(&zero), &[], 5, 0));
10793
10794 let offset_past = vec![CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
10795 CssNthChildSelector::Pattern(CssNthChildPattern {
10796 pattern_repeat: 2,
10797 offset: u32::MAX,
10798 }),
10799 ))];
10800 assert!(
10801 group_matches(&refs(&offset_past), &[], 0, 0),
10802 "index - offset saturates to 0 rather than underflowing"
10803 );
10804 }
10805
10806 #[test]
10807 fn group_matches_structural_combinators_never_match() {
10808 for sel in [
10809 CssPathSelector::Children,
10810 CssPathSelector::DirectChildren,
10811 CssPathSelector::AdjacentSibling,
10812 CssPathSelector::GeneralSibling,
10813 ] {
10814 let a = vec![sel.clone()];
10815 assert!(
10816 !group_matches(&refs(&a), &refs(&a), 0, 1),
10817 "{sel:?} is a combinator, not a matchable group member"
10818 );
10819 }
10820 }
10821
10822 #[test]
10823 fn css_matcher_empty_path_never_matches() {
10824 let m = CssMatcher {
10825 path: Vec::new(),
10826 indices_in_parent: vec![0],
10827 children_length: vec![0],
10828 };
10829 let path = CssPath {
10830 selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10831 };
10832 assert!(!m.matches(&path), "an empty matcher path can never match");
10833
10834 let m2 = CssMatcher {
10835 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10836 indices_in_parent: vec![0],
10837 children_length: vec![0],
10838 };
10839 let empty_path = CssPath {
10840 selectors: Vec::new().into(),
10841 };
10842 assert!(!m2.matches(&empty_path), "an empty CSS path can never match");
10843 }
10844
10845 #[test]
10846 fn css_matcher_get_hash_is_deterministic_and_path_sensitive() {
10847 let a = CssMatcher {
10848 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10849 indices_in_parent: vec![0],
10850 children_length: vec![0],
10851 };
10852 let b = CssMatcher {
10853 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10854 indices_in_parent: vec![9],
10855 children_length: vec![9],
10856 };
10857 let c = CssMatcher {
10858 path: vec![CssPathSelector::Type(NodeTypeTag::Div)],
10859 indices_in_parent: vec![0],
10860 children_length: vec![0],
10861 };
10862 assert_eq!(a.get_hash(), a.get_hash(), "stable across calls");
10863 assert_eq!(
10864 a.get_hash(),
10865 b.get_hash(),
10866 "the hash covers only `path`, not the sibling indices"
10867 );
10868 assert_ne!(a.get_hash(), c.get_hash());
10869
10870 let empty = CssMatcher {
10871 path: Vec::new(),
10872 indices_in_parent: Vec::new(),
10873 children_length: Vec::new(),
10874 };
10875 let _ = empty.get_hash(); }
10877
10878 #[test]
10879 fn css_matcher_mismatched_bookkeeping_vec_lengths_bail_out() {
10880 let m = CssMatcher {
10882 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10883 indices_in_parent: Vec::new(),
10884 children_length: Vec::new(),
10885 };
10886 let path = CssPath {
10887 selectors: vec![CssPathSelector::Type(NodeTypeTag::Body)].into(),
10888 };
10889 assert!(
10890 !m.matches(&path),
10891 "a desynced matcher must return false, not index out of bounds"
10892 );
10893 }
10894
10895 #[test]
10896 fn get_css_blocks_and_inline_string_on_empty_css() {
10897 let m = CssMatcher {
10898 path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
10899 indices_in_parent: vec![0],
10900 children_length: vec![0],
10901 };
10902 assert!(get_css_blocks(&Css::empty(), &m).is_empty());
10903 assert_eq!(css_blocks_to_inline_string(&[]), "");
10904 }
10905
10906 #[test]
10911 fn str_to_dom_rejects_documents_without_html_or_body() {
10912 let map = ComponentMap::with_builtin();
10913 assert_eq!(
10914 str_to_dom(&[], &map, None).unwrap_err(),
10915 DomXmlParseError::NoHtmlNode
10916 );
10917 let html_only = vec![elem(XmlNode::create("html"))];
10918 assert_eq!(
10919 str_to_dom(&html_only, &map, None).unwrap_err(),
10920 DomXmlParseError::NoBodyInHtml
10921 );
10922 assert!(str_to_dom_unstyled(&[], &map).is_err());
10923 }
10924
10925 #[test]
10926 fn str_to_dom_valid_minimal() {
10927 let map = ComponentMap::with_builtin();
10928 let d = doc("body { color: red; }", vec![elem(node("div", &[("id", "x")], vec![]))]);
10929 assert!(str_to_dom(&d, &map, None).is_ok());
10930 assert!(str_to_dom_unstyled(&d, &map).is_ok());
10931 }
10932
10933 #[test]
10934 fn str_to_dom_max_width_edge_values_do_not_panic() {
10935 let map = ComponentMap::with_builtin();
10936 let d = doc("", vec![elem(XmlNode::create("div"))]);
10937 for w in [
10938 Some(0.0f32),
10939 Some(-0.0),
10940 Some(-1.0),
10941 Some(f32::MAX),
10942 Some(f32::MIN),
10943 Some(f32::NAN),
10944 Some(f32::INFINITY),
10945 Some(f32::NEG_INFINITY),
10946 None,
10947 ] {
10948 assert!(
10949 str_to_dom(&d, &map, w).is_ok(),
10950 "max_width={w:?} is formatted straight into a CSS string and must not panic"
10951 );
10952 }
10953 }
10954
10955 #[test]
10956 fn str_to_dom_deeply_nested_body_is_depth_capped_not_stack_overflowing() {
10957 let map = ComponentMap::with_builtin();
10958 let deep = wrap_divs(2_000, node("div", &[("id", "bottom")], vec![]));
10959 let d = doc("", vec![elem(deep)]);
10960 assert!(
10961 str_to_dom(&d, &map, None).is_ok(),
10962 "children past MAX_XML_NESTING_DEPTH are dropped, not crashed on"
10963 );
10964 }
10965
10966 #[test]
10967 fn parse_page_style_and_body_and_body_matcher() {
10968 let d = doc("body { color: red; }", vec![elem(XmlNode::create("div"))]);
10969 let (css, body) = parse_page_style_and_body(&d).expect("well-formed page");
10970 assert_eq!(body.node_type.as_str(), "body");
10971 assert!(!css.rules.as_ref().is_empty(), "the <style> block is parsed");
10972
10973 let m = body_matcher(body);
10974 assert!(m.path.is_empty(), "the matcher starts with an empty path");
10975 assert_eq!(m.indices_in_parent, vec![0]);
10976 assert_eq!(m.children_length, vec![body.children.as_ref().len()]);
10977 }
10978
10979 #[test]
10980 fn parse_page_style_and_body_with_no_style_block() {
10981 let head = node("head", &[], vec![]);
10982 let body = node("body", &[], vec![]);
10983 let d = vec![elem(node("html", &[], vec![elem(head), elem(body)]))];
10984 let (css, body) = parse_page_style_and_body(&d).expect("ok");
10985 assert!(css.rules.as_ref().is_empty());
10986 assert_eq!(body.children.as_ref().len(), 0);
10987 }
10988
10989 #[test]
10994 fn str_to_rust_code_empty_input_is_an_error_not_a_panic() {
10995 let map = ComponentMap::with_builtin();
10996 assert!(matches!(
10997 str_to_rust_code(&[], "", &map),
10998 Err(CompileError::Xml(DomXmlParseError::NoHtmlNode))
10999 ));
11000 assert!(str_to_c_code(&[], &map).is_err());
11001 assert!(str_to_cpp_code(&[], &map).is_err());
11002 assert!(str_to_python_code(&[], &map).is_err());
11003 }
11004
11005 #[test]
11006 fn str_to_rust_code_whitespace_and_text_only_roots_are_errors() {
11007 let map = ComponentMap::with_builtin();
11008 for roots in [vec![txt(" ")], vec![txt("\t\n")], vec![txt("garbage")]] {
11009 assert!(
11010 str_to_rust_code(&roots, "", &map).is_err(),
11011 "a document with no <html> element must be rejected"
11012 );
11013 }
11014 }
11015
11016 #[test]
11017 fn str_to_rust_code_valid_minimal() {
11018 let map = ComponentMap::with_builtin();
11019 let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
11020 let src = str_to_rust_code(&d, "// imports", &map).expect("compiles");
11021 assert!(src.contains("Dom::create_body()"), "got:\n{src}");
11022 assert!(src.contains("Dom::create_p_with_text(AzString::from(\"Hi\"))"));
11023 assert!(src.contains("// imports"), "the imports blob is spliced in");
11024 assert!(src.contains("fn main()"));
11025 }
11026
11027 #[test]
11028 fn str_to_c_cpp_python_code_valid_minimal() {
11029 let map = ComponentMap::with_builtin();
11030 let d = doc("", vec![elem(node("p", &[], vec![txt("Hi")]))]);
11031
11032 let c = str_to_c_code(&d, &map).expect("compiles");
11033 assert!(c.contains("#include \"azul.h\""), "got:\n{c}");
11034 assert!(c.contains("AzDom n0 = AzDom_createBody();"));
11035 assert!(c.contains("AzDom_createPWithText(AZ_STR(\"Hi\"))"));
11036
11037 let cpp = str_to_cpp_code(&d, &map).expect("compiles");
11038 assert!(cpp.contains("#include \"azul20.hpp\""), "got:\n{cpp}");
11039 assert!(cpp.contains("Dom::create_p_with_text(String(\"Hi\"))"));
11040
11041 let py = str_to_python_code(&d, &map).expect("compiles");
11042 assert!(py.contains("import azul"), "got:\n{py}");
11043 assert!(py.contains("azul.Dom.create_p_with_text(\"Hi\")"));
11044 }
11045
11046 #[test]
11047 fn compile_targets_escape_quotes_in_text_content() {
11048 let map = ComponentMap::with_builtin();
11049 let d = doc("", vec![elem(node("div", &[], vec![txt("say \"hi\"")]))]);
11050
11051 let rust = str_to_rust_code(&d, "", &map).expect("compiles");
11052 assert!(rust.contains("say \\\"hi\\\""), "got:\n{rust}");
11053 let c = str_to_c_code(&d, &map).expect("compiles");
11054 assert!(c.contains("say \\\"hi\\\""), "got:\n{c}");
11055 }
11056
11057 #[test]
11058 fn compile_body_node_to_rust_code_on_an_empty_body() {
11059 let map = ComponentMap::with_builtin();
11060 let body = node("body", &[], vec![]);
11061 let mut extra = VecContents::default();
11062 let mut blocks = BTreeMap::new();
11063 let out = compile_body_node_to_rust_code(
11064 &body,
11065 &map,
11066 &mut extra,
11067 &mut blocks,
11068 &Css::empty(),
11069 body_matcher(&body),
11070 )
11071 .expect("ok");
11072 assert_eq!(out, "Dom::create_body()", "no children => no .with_children()");
11073 }
11074
11075 #[test]
11076 fn compile_body_node_to_rust_code_skips_whitespace_only_text_children() {
11077 let map = ComponentMap::with_builtin();
11078 let body = node("body", &[], vec![txt(" \n\t ")]);
11079 let mut extra = VecContents::default();
11080 let mut blocks = BTreeMap::new();
11081 let out = compile_body_node_to_rust_code(
11082 &body,
11083 &map,
11084 &mut extra,
11085 &mut blocks,
11086 &Css::empty(),
11087 body_matcher(&body),
11088 )
11089 .expect("ok");
11090 assert!(
11091 !out.contains("create_text"),
11092 "a whitespace-only text child emits nothing, got:\n{out}"
11093 );
11094 }
11095
11096 #[test]
11101 fn builtin_render_fn_for_a_text_and_a_textless_element() {
11102 let map = ComponentMap::with_builtin();
11103 let div = map.get_unqualified("div").expect("builtin div");
11104 assert!(matches!(
11105 builtin_render_fn(div, &div.data_model, &map),
11106 ResultStyledDomRenderDomError::Ok(_)
11107 ));
11108
11109 let p = map.get_unqualified("p").expect("builtin p");
11110 assert!(matches!(
11111 builtin_render_fn(p, &p.data_model, &map),
11112 ResultStyledDomRenderDomError::Ok(_)
11113 ));
11114 }
11115
11116 #[test]
11117 fn builtin_compile_fn_ignores_indent_so_usize_max_is_safe() {
11118 let map = ComponentMap::with_builtin();
11119 let div = map.get_unqualified("div").expect("builtin div");
11120 for indent in [0usize, 1, 1024, usize::MAX] {
11121 match builtin_compile_fn(div, &CompileTarget::Rust, &div.data_model, indent) {
11122 ResultStringCompileError::Ok(s) => assert_eq!(
11123 s.as_str(),
11124 "Dom::create_node(NodeType::Div)",
11125 "indent is unused by builtin_compile_fn (indent={indent})"
11126 ),
11127 ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11128 }
11129 }
11130 }
11131
11132 #[test]
11133 fn builtin_compile_fn_emits_text_and_escapes_it() {
11134 let map = ComponentMap::with_builtin();
11135 let p = map.get_unqualified("p").expect("builtin p");
11136 let data = p
11137 .data_model
11138 .clone()
11139 .with_default("text", ComponentDefaultValue::String(AzString::from("a\"b\\c")));
11140
11141 match builtin_compile_fn(p, &CompileTarget::Rust, &data, 0) {
11142 ResultStringCompileError::Ok(s) => {
11143 assert!(s.as_str().contains("a\\\"b\\\\c"), "got {}", s.as_str());
11144 }
11145 ResultStringCompileError::Err(e) => panic!("unexpected error: {e:?}"),
11146 }
11147 }
11148
11149 #[test]
11150 fn builtin_compile_fn_covers_every_target() {
11151 let map = ComponentMap::with_builtin();
11152 let div = map.get_unqualified("div").expect("builtin div");
11153 for target in [
11154 CompileTarget::Rust,
11155 CompileTarget::C,
11156 CompileTarget::Cpp,
11157 CompileTarget::Python,
11158 ] {
11159 match builtin_compile_fn(div, &target, &div.data_model, 0) {
11160 ResultStringCompileError::Ok(s) => {
11161 assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11162 }
11163 ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11164 }
11165 }
11166 }
11167
11168 fn every_default_kind() -> Vec<ComponentDataField> {
11173 use ComponentDefaultValue as D;
11174 vec![
11175 data_field("s", ComponentFieldType::String, Some(D::String(AzString::from("txt"))), ""),
11176 data_field("b", ComponentFieldType::Bool, Some(D::Bool(true)), ""),
11177 data_field("i32", ComponentFieldType::I32, Some(D::I32(i32::MIN)), ""),
11178 data_field("i64", ComponentFieldType::I64, Some(D::I64(i64::MIN)), ""),
11179 data_field("u32", ComponentFieldType::U32, Some(D::U32(u32::MAX)), ""),
11180 data_field("u64", ComponentFieldType::U64, Some(D::U64(u64::MAX)), ""),
11181 data_field("us", ComponentFieldType::Usize, Some(D::Usize(usize::MAX)), ""),
11182 data_field("f32", ComponentFieldType::F32, Some(D::F32(f32::NAN)), ""),
11183 data_field("f64", ComponentFieldType::F64, Some(D::F64(f64::INFINITY)), ""),
11184 data_field(
11185 "c",
11186 ComponentFieldType::ColorU,
11187 Some(D::ColorU(ColorU { r: 0, g: 0, b: 0, a: 0 })),
11188 "",
11189 ),
11190 data_field("cb", ComponentFieldType::StyledDom, Some(D::CallbackFnPointer(AzString::from("on_click"))), ""),
11191 data_field("j", ComponentFieldType::String, Some(D::Json(AzString::from("{}"))), ""),
11192 data_field("none", ComponentFieldType::String, Some(D::None), ""),
11193 data_field("missing", ComponentFieldType::String, None, ""),
11194 ]
11195 }
11196
11197 #[test]
11198 fn user_defined_render_fn_handles_every_default_value_kind() {
11199 let map = ComponentMap::with_builtin();
11200 let def = user_def("", every_default_kind());
11201 assert!(matches!(
11202 user_defined_render_fn(&def, &def.data_model, &map),
11203 ResultStyledDomRenderDomError::Ok(_)
11204 ));
11205 }
11206
11207 #[test]
11208 fn user_defined_render_fn_on_an_empty_model_and_with_css() {
11209 let map = ComponentMap::with_builtin();
11210 let empty = user_def("", Vec::new());
11211 assert!(matches!(
11212 user_defined_render_fn(&empty, &empty.data_model, &map),
11213 ResultStyledDomRenderDomError::Ok(_)
11214 ));
11215
11216 let styled = user_def(".widget { color: red; }", Vec::new());
11217 assert!(matches!(
11218 user_defined_render_fn(&styled, &styled.data_model, &map),
11219 ResultStyledDomRenderDomError::Ok(_)
11220 ));
11221 }
11222
11223 #[test]
11224 fn user_defined_render_fn_unknown_sub_component_renders_a_placeholder() {
11225 let map = ComponentMap::create(); let def = user_def(
11227 "",
11228 vec![data_field(
11229 "child",
11230 ComponentFieldType::StyledDom,
11231 Some(ComponentDefaultValue::ComponentInstance(ComponentInstanceDefault {
11232 library: AzString::from("nope"),
11233 component: AzString::from("missing"),
11234 field_overrides: Vec::new().into(),
11235 })),
11236 "",
11237 )],
11238 );
11239 assert!(
11240 matches!(
11241 user_defined_render_fn(&def, &def.data_model, &map),
11242 ResultStyledDomRenderDomError::Ok(_)
11243 ),
11244 "an unresolvable sub-component must render a placeholder, not error out"
11245 );
11246 }
11247
11248 #[test]
11249 fn user_defined_compile_fn_indent_zero_and_every_target() {
11250 let def = user_def("", every_default_kind());
11251 for target in [
11252 CompileTarget::Rust,
11253 CompileTarget::C,
11254 CompileTarget::Cpp,
11255 CompileTarget::Python,
11256 ] {
11257 match user_defined_compile_fn(&def, &target, &def.data_model, 0) {
11258 ResultStringCompileError::Ok(s) => {
11259 assert!(!s.as_str().is_empty(), "{target:?} emitted nothing");
11260 }
11261 ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11262 }
11263 }
11264 }
11265
11266 #[test]
11267 fn user_defined_compile_fn_indent_scales_the_leading_whitespace() {
11268 let def = user_def("", Vec::new());
11271 let mut prev = 0usize;
11272 for indent in [0usize, 1, 2, 8] {
11273 match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, indent) {
11274 ResultStringCompileError::Ok(s) => {
11275 let len = s.as_str().len();
11276 assert!(len > prev, "indent={indent} must widen the output");
11277 prev = len;
11278 }
11279 ResultStringCompileError::Err(e) => panic!("indent={indent}: {e:?}"),
11280 }
11281 }
11282 }
11283
11284 #[test]
11285 fn user_defined_compile_fn_escapes_string_defaults() {
11286 let def = user_def(
11287 "",
11288 vec![data_field(
11289 "s",
11290 ComponentFieldType::String,
11291 Some(ComponentDefaultValue::String(AzString::from("a\"b\\c"))),
11292 "",
11293 )],
11294 );
11295 match user_defined_compile_fn(&def, &CompileTarget::Rust, &def.data_model, 0) {
11296 ResultStringCompileError::Ok(s) => {
11297 assert!(s.as_str().contains("a\\\"b\\\\c"), "got:\n{}", s.as_str());
11298 }
11299 ResultStringCompileError::Err(e) => panic!("{e:?}"),
11300 }
11301 }
11302
11303 #[test]
11304 fn push_scalar_field_appends_one_div_per_call() {
11305 let mut children: Vec<Dom> = Vec::new();
11306 push_scalar_field(&mut children, "n", &i64::MIN);
11307 push_scalar_field(&mut children, "", &f32::NAN);
11308 push_scalar_field(&mut children, "\u{1F600}", &usize::MAX);
11309 assert_eq!(children.len(), 3);
11310 }
11311
11312 #[test]
11317 fn builtin_if_for_map_component_defs_are_well_formed() {
11318 for (def, model, field) in [
11319 (builtin_if_component(), "IfData", "condition"),
11320 (builtin_for_component(), "ForData", "count"),
11321 (builtin_map_component(), "MapData", "data_json"),
11322 ] {
11323 assert_eq!(def.id.collection.as_str(), "builtin");
11324 assert_eq!(def.data_model.name.as_str(), model);
11325 assert!(
11326 def.data_model.get_field(field).is_some(),
11327 "{model} must expose `{field}`"
11328 );
11329 }
11330 }
11331
11332 #[test]
11333 fn builtin_if_render_fn_defaults_to_the_else_branch() {
11334 let map = ComponentMap::create();
11335 let def = builtin_if_component();
11336 let empty = dm("IfData", Vec::new());
11338 assert!(matches!(
11339 builtin_if_render_fn(&def, &empty, &map),
11340 ResultStyledDomRenderDomError::Ok(_)
11341 ));
11342
11343 let truthy = def
11344 .data_model
11345 .clone()
11346 .with_default("condition", ComponentDefaultValue::Bool(true));
11347 assert!(matches!(
11348 builtin_if_render_fn(&def, &truthy, &map),
11349 ResultStyledDomRenderDomError::Ok(_)
11350 ));
11351 }
11352
11353 #[test]
11354 fn builtin_for_render_fn_handles_zero_and_a_wrongly_typed_count() {
11355 let map = ComponentMap::create();
11356 let def = builtin_for_component();
11357
11358 let zero = def
11359 .data_model
11360 .clone()
11361 .with_default("count", ComponentDefaultValue::U32(0));
11362 assert!(matches!(
11363 builtin_for_render_fn(&def, &zero, &map),
11364 ResultStyledDomRenderDomError::Ok(_)
11365 ));
11366
11367 let wrong_type = def
11369 .data_model
11370 .clone()
11371 .with_default("count", ComponentDefaultValue::String(AzString::from("9")));
11372 assert!(matches!(
11373 builtin_for_render_fn(&def, &wrong_type, &map),
11374 ResultStyledDomRenderDomError::Ok(_)
11375 ));
11376 }
11377
11378 #[test]
11379 fn builtin_map_render_fn_defaults_to_an_empty_json_array() {
11380 let map = ComponentMap::create();
11381 let def = builtin_map_component();
11382 assert!(matches!(
11383 builtin_map_render_fn(&def, &dm("MapData", Vec::new()), &map),
11384 ResultStyledDomRenderDomError::Ok(_)
11385 ));
11386 let garbage = def
11387 .data_model
11388 .clone()
11389 .with_default("data_json", ComponentDefaultValue::String(AzString::from("{{{")));
11390 assert!(
11391 matches!(
11392 builtin_map_render_fn(&def, &garbage, &map),
11393 ResultStyledDomRenderDomError::Ok(_)
11394 ),
11395 "malformed JSON must not panic — it is only echoed into a label"
11396 );
11397 }
11398
11399 #[test]
11400 fn structural_builtin_compile_fns_ignore_indent_entirely() {
11401 let cases: [(ComponentDef, ComponentCompileFn); 3] = [
11402 (builtin_if_component(), builtin_if_compile_fn),
11403 (builtin_for_component(), builtin_for_compile_fn),
11404 (builtin_map_component(), builtin_map_compile_fn),
11405 ];
11406 for (def, f) in cases {
11407 for target in [
11408 CompileTarget::Rust,
11409 CompileTarget::C,
11410 CompileTarget::Cpp,
11411 CompileTarget::Python,
11412 ] {
11413 for indent in [0usize, usize::MAX] {
11414 match f(&def, &target, &def.data_model, indent) {
11415 ResultStringCompileError::Ok(s) => {
11416 assert!(!s.as_str().is_empty(), "{target:?}/{indent} emitted nothing");
11417 }
11418 ResultStringCompileError::Err(e) => panic!("{target:?}: {e:?}"),
11419 }
11420 }
11421 }
11422 }
11423 }
11424
11425 #[test]
11430 fn data_field_required_is_the_inverse_of_having_a_default() {
11431 let with = data_field(
11432 "x",
11433 ComponentFieldType::String,
11434 Some(ComponentDefaultValue::String(AzString::from("v"))),
11435 "d",
11436 );
11437 assert!(!with.required);
11438 assert_eq!(with.description.as_str(), "d");
11439
11440 let without = data_field("x", ComponentFieldType::String, None, "");
11441 assert!(without.required);
11442 assert!(matches!(
11443 without.default_value,
11444 OptionComponentDefaultValue::None
11445 ));
11446 }
11447
11448 #[test]
11449 fn builtin_data_model_unknown_tag_is_empty() {
11450 assert!(builtin_data_model("").is_empty());
11451 assert!(builtin_data_model("div").is_empty());
11452 assert!(builtin_data_model("\u{1F600}").is_empty());
11453 assert!(builtin_data_model(&"z".repeat(10_000)).is_empty());
11454 }
11455
11456 #[test]
11457 fn builtin_data_model_known_tags_expose_their_attributes() {
11458 let a = builtin_data_model("a");
11459 assert!(
11460 a.iter().any(|f| f.name.as_str() == "href"),
11461 "<a> must expose href"
11462 );
11463 let img = builtin_data_model("img");
11465 let src = img
11466 .iter()
11467 .find(|f| f.name.as_str() == "src")
11468 .expect("img has src");
11469 assert!(src.required, "<img src> must be a required field");
11470 assert_eq!(builtin_data_model("image").len(), img.len());
11472 }
11473
11474 #[test]
11475 fn builtin_component_def_default_text_controls_the_text_field() {
11476 let with_text = builtin_component_def("p", "Paragraph", Some("Hi"), "");
11477 assert_eq!(
11478 with_text.data_model.get_default_string("text").map(AzString::as_str),
11479 Some("Hi")
11480 );
11481 assert_eq!(with_text.data_model.name.as_str(), "ParagraphData");
11482 assert_eq!(with_text.id.qualified_name(), "builtin:p");
11483
11484 let no_text = builtin_component_def("div", "Div", None, "");
11485 assert!(
11486 no_text.data_model.get_field("text").is_none(),
11487 "a `None` default_text means the element has no text field at all"
11488 );
11489
11490 let empty_text = builtin_component_def("span", "Span", Some(""), "");
11492 assert!(empty_text.data_model.get_field("text").is_some());
11493 assert_eq!(
11494 empty_text.data_model.get_default_string("text").map(AzString::as_str),
11495 Some("")
11496 );
11497 }
11498
11499 #[test]
11504 fn xml_attrs_to_data_model_overrides_defaults_from_attributes() {
11505 let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11506 let model = xml_attrs_to_data_model(&base, &attrs(&[("href", "/x")]), None);
11507 assert_eq!(
11508 model.get_default_string("href").map(AzString::as_str),
11509 Some("/x")
11510 );
11511 assert_eq!(
11512 model.get_default_string("text").map(AzString::as_str),
11513 Some("Link text"),
11514 "un-supplied fields keep their defaults"
11515 );
11516 assert_eq!(
11517 model.fields.as_ref().len(),
11518 base.fields.as_ref().len(),
11519 "no field is added or dropped"
11520 );
11521 }
11522
11523 #[test]
11524 fn xml_attrs_to_data_model_text_content_is_prepared_and_empty_text_is_ignored() {
11525 let base = builtin_component_def("a", "Link", Some("Link text"), "").data_model;
11526
11527 let with_text = xml_attrs_to_data_model(&base, &attrs(&[]), Some(" Hello & bye "));
11528 assert_eq!(
11529 with_text.get_default_string("text").map(AzString::as_str),
11530 Some("Hello & bye"),
11531 "text content is trimmed and entity-decoded"
11532 );
11533
11534 let blank = xml_attrs_to_data_model(&base, &attrs(&[]), Some(" \n\t "));
11535 assert_eq!(
11536 blank.get_default_string("text").map(AzString::as_str),
11537 Some("Link text"),
11538 "whitespace-only text content leaves the default intact"
11539 );
11540 }
11541
11542 #[test]
11543 fn xml_attrs_to_data_model_ignores_unknown_attributes() {
11544 let base = builtin_component_def("a", "Link", Some(""), "").data_model;
11545 let before = base.fields.as_ref().len();
11546 let model = xml_attrs_to_data_model(
11547 &base,
11548 &attrs(&[("data-nonsense", "1"), ("", ""), ("\u{1F600}", "x")]),
11549 None,
11550 );
11551 assert_eq!(
11552 model.fields.as_ref().len(),
11553 before,
11554 "unknown attributes must not create fields"
11555 );
11556 }
11557
11558 #[test]
11563 fn dom_xml_into_styled_dom_matches_the_from_impl() {
11564 let via_method: StyledDom = DomXml::default().into_styled_dom();
11565 let via_from: StyledDom = DomXml::default().into();
11566 assert_eq!(
11567 via_method, via_from,
11568 "into_styled_dom() must be exactly the From<DomXml> impl"
11569 );
11570 }
11571
11572 fn pos() -> XmlTextPos {
11577 XmlTextPos {
11578 row: u32::MAX,
11579 col: 0,
11580 }
11581 }
11582
11583 #[test]
11584 fn xml_text_pos_display_is_non_empty_for_edge_values() {
11585 assert_eq!(
11586 format!("{}", XmlTextPos { row: 0, col: 0 }),
11587 "line 0:0",
11588 "a zero position is still rendered"
11589 );
11590 assert_eq!(
11591 format!(
11592 "{}",
11593 XmlTextPos {
11594 row: u32::MAX,
11595 col: u32::MAX
11596 }
11597 ),
11598 "line 4294967295:4294967295"
11599 );
11600 }
11601
11602 #[test]
11603 fn xml_stream_error_display_covers_every_variant() {
11604 let variants = vec![
11605 XmlStreamError::UnexpectedEndOfStream,
11606 XmlStreamError::InvalidName,
11607 XmlStreamError::NonXmlChar(NonXmlCharError {
11608 ch: u32::MAX,
11609 pos: pos(),
11610 }),
11611 XmlStreamError::InvalidChar(InvalidCharError {
11612 expected: u8::MAX,
11613 got: 0,
11614 pos: pos(),
11615 }),
11616 XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
11617 expected: 0,
11618 got: Vec::<u8>::new().into(),
11619 pos: pos(),
11620 }),
11621 XmlStreamError::InvalidQuote(InvalidQuoteError { got: 0, pos: pos() }),
11622 XmlStreamError::InvalidSpace(InvalidSpaceError { got: 0, pos: pos() }),
11623 XmlStreamError::InvalidString(InvalidStringError {
11624 got: AzString::from(""),
11625 pos: pos(),
11626 }),
11627 XmlStreamError::InvalidReference,
11628 XmlStreamError::InvalidExternalID,
11629 XmlStreamError::InvalidCommentData,
11630 XmlStreamError::InvalidCommentEnd,
11631 XmlStreamError::InvalidCharacterData,
11632 ];
11633 for v in &variants {
11634 let s = format!("{v}");
11635 assert!(!s.is_empty(), "{v:?} must render a non-empty message");
11636 }
11637 assert!(format!("{}", variants[2]).contains("None"));
11639 }
11640
11641 #[test]
11642 fn xml_parse_error_display_covers_every_variant() {
11643 let te = XmlTextError {
11644 stream_error: XmlStreamError::InvalidName,
11645 pos: pos(),
11646 };
11647 let variants = vec![
11648 XmlParseError::InvalidDeclaration(te.clone()),
11649 XmlParseError::InvalidComment(te.clone()),
11650 XmlParseError::InvalidPI(te.clone()),
11651 XmlParseError::InvalidDoctype(te.clone()),
11652 XmlParseError::InvalidEntity(te.clone()),
11653 XmlParseError::InvalidElement(te.clone()),
11654 XmlParseError::InvalidAttribute(te.clone()),
11655 XmlParseError::InvalidCdata(te.clone()),
11656 XmlParseError::InvalidCharData(te),
11657 XmlParseError::UnknownToken(pos()),
11658 ];
11659 for v in &variants {
11660 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11661 }
11662 }
11663
11664 #[test]
11665 fn xml_error_display_covers_the_non_css_variants() {
11666 let variants = vec![
11667 XmlError::NoParserAvailable,
11668 XmlError::InvalidXmlPrefixUri(pos()),
11669 XmlError::UnexpectedXmlUri(pos()),
11670 XmlError::UnexpectedXmlnsUri(pos()),
11671 XmlError::InvalidElementNamePrefix(pos()),
11672 XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
11673 ns: AzString::from(""),
11674 pos: pos(),
11675 }),
11676 XmlError::UnknownNamespace(UnknownNamespaceError {
11677 ns: AzString::from("\u{1F600}"),
11678 pos: pos(),
11679 }),
11680 XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
11681 expected: AzString::from("a"),
11682 actual: AzString::from("b"),
11683 pos: pos(),
11684 }),
11685 XmlError::UnexpectedEntityCloseTag(pos()),
11686 XmlError::UnknownEntityReference(UnknownEntityReferenceError {
11687 entity: AzString::from("x"),
11688 pos: pos(),
11689 }),
11690 XmlError::MalformedEntityReference(pos()),
11691 XmlError::EntityReferenceLoop(pos()),
11692 XmlError::InvalidAttributeValue(pos()),
11693 XmlError::DuplicatedAttribute(DuplicatedAttributeError {
11694 attribute: AzString::from("id"),
11695 pos: pos(),
11696 }),
11697 XmlError::NoRootNode,
11698 XmlError::SizeLimit,
11699 XmlError::DtdDetected,
11700 XmlError::MalformedHierarchy(MalformedHierarchyError {
11701 expected: AzString::from("app"),
11702 got: AzString::from("p"),
11703 }),
11704 XmlError::ParserError(XmlParseError::UnknownToken(pos())),
11705 XmlError::UnclosedRootNode,
11706 XmlError::UnexpectedDeclaration(pos()),
11707 XmlError::NodesLimitReached,
11708 XmlError::AttributesLimitReached,
11709 XmlError::NamespacesLimitReached,
11710 XmlError::InvalidName(pos()),
11711 XmlError::NonXmlChar(pos()),
11712 XmlError::InvalidChar(pos()),
11713 XmlError::InvalidChar2(pos()),
11714 XmlError::InvalidString(pos()),
11715 XmlError::InvalidExternalID(pos()),
11716 XmlError::InvalidComment(pos()),
11717 XmlError::InvalidCharacterData(pos()),
11718 XmlError::UnknownToken(pos()),
11719 XmlError::UnexpectedEndOfStream,
11720 ];
11721 for v in &variants {
11722 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11723 }
11724 }
11725
11726 #[test]
11727 fn component_and_render_and_compile_error_display() {
11728 let unknown = ComponentError::UnknownComponent(AzString::from("\u{1F600}"));
11729 assert!(format!("{unknown}").contains("Unknown component"));
11730
11731 let useless = ComponentError::UselessFunctionArgument(UselessFunctionArgumentError {
11732 component_name: AzString::from("c"),
11733 argument_name: AzString::from("a"),
11734 valid_args: Vec::<AzString>::new().into(),
11735 });
11736 assert!(!format!("{useless}").is_empty());
11737
11738 let render: RenderDomError = unknown.clone().into();
11739 assert!(!format!("{render}").is_empty());
11740
11741 let compile: CompileError = render.clone().into();
11742 assert!(!format!("{compile}").is_empty());
11743
11744 let dom_xml: DomXmlParseError = render.into();
11745 assert!(!format!("{dom_xml}").is_empty());
11746 let compile2: CompileError = dom_xml.into();
11747 assert!(!format!("{compile2}").is_empty());
11748 }
11749
11750 #[test]
11751 fn dom_xml_parse_error_display_covers_the_non_css_variants() {
11752 let variants = vec![
11753 DomXmlParseError::NoHtmlNode,
11754 DomXmlParseError::MultipleHtmlRootNodes,
11755 DomXmlParseError::NoBodyInHtml,
11756 DomXmlParseError::MultipleBodyNodes,
11757 DomXmlParseError::Xml(XmlError::NoRootNode),
11758 DomXmlParseError::MalformedHierarchy(MalformedHierarchyError {
11759 expected: AzString::from("app"),
11760 got: AzString::from("p"),
11761 }),
11762 DomXmlParseError::RenderDom(RenderDomError::Component(
11763 ComponentError::UnknownComponent(AzString::from("x")),
11764 )),
11765 DomXmlParseError::Component(ComponentParseError::NotAComponent),
11766 ];
11767 for v in &variants {
11768 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11769 }
11770 }
11771
11772 #[test]
11773 fn component_parse_error_display_covers_the_non_css_variants() {
11774 let variants = vec![
11775 ComponentParseError::NotAComponent,
11776 ComponentParseError::UnnamedComponent,
11777 ComponentParseError::MissingName(usize::MAX),
11778 ComponentParseError::MissingType(MissingTypeError {
11779 arg_pos: 0,
11780 arg_name: AzString::from(""),
11781 }),
11782 ComponentParseError::WhiteSpaceInComponentName(WhiteSpaceInComponentNameError {
11783 arg_pos: usize::MAX,
11784 arg_name: AzString::from("a b"),
11785 }),
11786 ComponentParseError::WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError {
11787 arg_pos: 0,
11788 arg_name: AzString::from("a"),
11789 arg_type: AzString::from("b c"),
11790 }),
11791 ];
11792 for v in &variants {
11793 assert!(!format!("{v}").is_empty(), "{v:?} must render");
11794 }
11795 }
11796
11797 #[cfg(feature = "serde-json")]
11802 #[test]
11803 fn data_model_to_json_round_trips() {
11804 let m = model_with_text();
11805 let json = m.to_json().expect("serializes");
11806 let back = ComponentDataModel::from_json(&json).expect("deserializes");
11807 assert_eq!(back.name.as_str(), m.name.as_str());
11808 assert_eq!(back.fields.as_ref().len(), m.fields.as_ref().len());
11809 assert_eq!(back.get_default_string("text").map(AzString::as_str), Some("hi"));
11810 }
11811
11812 #[cfg(feature = "serde-json")]
11813 #[test]
11814 fn data_model_from_json_rejects_garbage_without_panicking() {
11815 for s in [
11816 "",
11817 " ",
11818 "\t\n",
11819 "not json",
11820 "{",
11821 "[]",
11822 "null",
11823 "0",
11824 "-0",
11825 "9223372036854775807",
11826 "NaN",
11827 "\u{1F600}",
11828 ] {
11829 assert!(
11830 ComponentDataModel::from_json(s).is_err(),
11831 "{s:?} is not a data model"
11832 );
11833 }
11834 }
11835
11836 #[cfg(feature = "serde-json")]
11837 #[test]
11838 fn data_model_from_json_deeply_nested_input_does_not_stack_overflow() {
11839 let bomb = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
11840 assert!(
11841 ComponentDataModel::from_json(&bomb).is_err(),
11842 "serde_json must reject the nesting bomb, not crash"
11843 );
11844 }
11845
11846 #[cfg(feature = "serde-json")]
11847 #[test]
11848 fn data_model_to_json_on_an_empty_model() {
11849 let m = dm("Empty", Vec::new());
11850 let json = m.to_json().expect("serializes");
11851 assert!(json.contains("\"fields\""), "got {json}");
11852 assert!(ComponentDataModel::from_json(&json).is_ok());
11853 }
11854}