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