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 codegen::format::VecContents,
31 css::{
32 Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
33 NodeTypeTag,
34 },
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]
244 pub fn new(s: &str) -> Self {
245 Self {
246 inner: AzString::from(s),
247 }
248 }
249
250 #[must_use]
251 pub fn from_extension(ext: &str) -> Self {
252 let mime = match ext.to_lowercase().as_str() {
253 "png" => "image/png",
255 "jpg" | "jpeg" => "image/jpeg",
256 "gif" => "image/gif",
257 "webp" => "image/webp",
258 "svg" => "image/svg+xml",
259 "ico" => "image/x-icon",
260 "bmp" => "image/bmp",
261 "avif" => "image/avif",
262 "ttf" => "font/ttf",
264 "otf" => "font/otf",
265 "woff" => "font/woff",
266 "woff2" => "font/woff2",
267 "eot" => "application/vnd.ms-fontobject",
268 "css" => "text/css",
270 "js" | "mjs" => "application/javascript",
272 "mp4" => "video/mp4",
274 "webm" => "video/webm",
275 "ogg" => "video/ogg",
276 "mp3" => "audio/mpeg",
278 "wav" => "audio/wav",
279 "flac" => "audio/flac",
280 _ => "application/octet-stream",
282 };
283 Self {
284 inner: AzString::from(mime),
285 }
286 }
287}
288
289impl_option!(
290 MimeTypeHint,
291 OptionMimeTypeHint,
292 copy = false,
293 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
294);
295
296#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
298#[repr(C)]
299pub struct ExternalResource {
300 pub url: AzString,
302 pub kind: ExternalResourceKind,
304 pub mime_type: OptionMimeTypeHint,
306 pub source_element: AzString,
308 pub source_attribute: AzString,
310}
311
312impl_option!(
313 ExternalResource,
314 OptionExternalResource,
315 copy = false,
316 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
317);
318
319impl_vec!(
320 ExternalResource,
321 ExternalResourceVec,
322 ExternalResourceVecDestructor,
323 ExternalResourceVecDestructorType,
324 ExternalResourceVecSlice,
325 OptionExternalResource
326);
327impl_vec_mut!(ExternalResource, ExternalResourceVec);
328impl_vec_debug!(ExternalResource, ExternalResourceVec);
329impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
330impl_vec_eq!(ExternalResource, ExternalResourceVec);
331impl_vec_partialord!(ExternalResource, ExternalResourceVec);
332impl_vec_ord!(ExternalResource, ExternalResourceVec);
333impl_vec_hash!(ExternalResource, ExternalResourceVec);
334impl_vec_clone!(
335 ExternalResource,
336 ExternalResourceVec,
337 ExternalResourceVecDestructor
338);
339
340const MAX_XML_NESTING_DEPTH: usize = 512;
349
350const MAX_TYPE_PARSE_DEPTH: usize = 64;
355
356#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
357#[repr(C)]
358pub struct Xml {
359 pub root: XmlNodeChildVec,
360}
361
362impl Xml {
363 #[must_use]
375 pub fn scan_external_resources(&self) -> ExternalResourceVec {
376 let mut resources = Vec::new();
377
378 let mut stack: Vec<(&XmlNodeChild, usize)> = Vec::new();
385 for child in self.root.as_ref() {
386 stack.push((child, 0));
387 }
388 while let Some((child, depth)) = stack.pop() {
389 match child {
390 XmlNodeChild::Text(text) => {
391 Self::extract_css_urls(text.as_str(), &mut resources);
393 }
394 XmlNodeChild::Element(node) => {
395 if depth > MAX_XML_NESTING_DEPTH {
396 continue;
398 }
399 Self::scan_node(node, &mut resources);
400 for c in node.children.as_ref() {
401 stack.push((c, depth + 1));
402 }
403 }
404 }
405 }
406
407 resources.into()
408 }
409
410 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
412 let tag_name = node.node_type.inner.as_str().to_lowercase();
413
414 let get_attr = |name: &str| -> Option<String> {
416 node.attributes
417 .inner
418 .as_ref()
419 .iter()
420 .find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
421 .map(|pair| pair.value.as_str().to_string())
422 };
423
424 match tag_name.as_str() {
425 "img" => {
426 if let Some(src) = get_attr("src") {
427 let mime = Self::guess_mime_from_url(&src, "image");
428 resources.push(ExternalResource {
429 url: AzString::from(src),
430 kind: ExternalResourceKind::Image,
431 mime_type: mime.into(),
432 source_element: AzString::from("img"),
433 source_attribute: AzString::from("src"),
434 });
435 }
436 if let Some(srcset) = get_attr("srcset") {
438 for src in Self::parse_srcset(&srcset) {
439 let mime = Self::guess_mime_from_url(&src, "image");
440 resources.push(ExternalResource {
441 url: AzString::from(src),
442 kind: ExternalResourceKind::Image,
443 mime_type: mime.into(),
444 source_element: AzString::from("img"),
445 source_attribute: AzString::from("srcset"),
446 });
447 }
448 }
449 }
450 "link" => {
451 if let Some(href) = get_attr("href") {
452 let rel = get_attr("rel").unwrap_or_default().to_lowercase();
453 let type_attr = get_attr("type");
454 let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
455
456 let (kind, category) = if rel.contains("stylesheet") {
457 (ExternalResourceKind::Stylesheet, "stylesheet")
458 } else if rel.contains("icon") || rel.contains("apple-touch-icon") {
459 (ExternalResourceKind::Icon, "image")
460 } else if as_attr == "font" {
461 (ExternalResourceKind::Font, "font")
462 } else if as_attr == "script" {
463 (ExternalResourceKind::Script, "script")
464 } else if as_attr == "image" {
465 (ExternalResourceKind::Image, "image")
466 } else {
467 (ExternalResourceKind::Unknown, "")
468 };
469
470 let mime = type_attr
471 .map(|t| MimeTypeHint::new(&t))
472 .or_else(|| Self::guess_mime_from_url(&href, category));
473
474 resources.push(ExternalResource {
475 url: AzString::from(href),
476 kind,
477 mime_type: mime.into(),
478 source_element: AzString::from("link"),
479 source_attribute: AzString::from("href"),
480 });
481 }
482 }
483 "script" => {
484 if let Some(src) = get_attr("src") {
485 let type_attr = get_attr("type");
486 let mime = type_attr
487 .map(|t| MimeTypeHint::new(&t))
488 .or_else(|| Some(MimeTypeHint::new("application/javascript")));
489
490 resources.push(ExternalResource {
491 url: AzString::from(src),
492 kind: ExternalResourceKind::Script,
493 mime_type: mime.into(),
494 source_element: AzString::from("script"),
495 source_attribute: AzString::from("src"),
496 });
497 }
498 }
499 "video" => {
500 if let Some(src) = get_attr("src") {
501 let mime = Self::guess_mime_from_url(&src, "video");
502 resources.push(ExternalResource {
503 url: AzString::from(src),
504 kind: ExternalResourceKind::Video,
505 mime_type: mime.into(),
506 source_element: AzString::from("video"),
507 source_attribute: AzString::from("src"),
508 });
509 }
510 if let Some(poster) = get_attr("poster") {
511 let mime = Self::guess_mime_from_url(&poster, "image");
512 resources.push(ExternalResource {
513 url: AzString::from(poster),
514 kind: ExternalResourceKind::Image,
515 mime_type: mime.into(),
516 source_element: AzString::from("video"),
517 source_attribute: AzString::from("poster"),
518 });
519 }
520 }
521 "audio" => {
522 if let Some(src) = get_attr("src") {
523 let mime = Self::guess_mime_from_url(&src, "audio");
524 resources.push(ExternalResource {
525 url: AzString::from(src),
526 kind: ExternalResourceKind::Audio,
527 mime_type: mime.into(),
528 source_element: AzString::from("audio"),
529 source_attribute: AzString::from("src"),
530 });
531 }
532 }
533 "source" => {
534 if let Some(src) = get_attr("src") {
535 let type_attr = get_attr("type");
536 let kind = if type_attr.as_ref().is_some_and(|t| t.starts_with("audio")) {
538 ExternalResourceKind::Audio
539 } else {
540 ExternalResourceKind::Video
541 };
542 let mime = type_attr.map(|t| MimeTypeHint::new(&t)).or_else(|| {
543 Self::guess_mime_from_url(
544 &src,
545 if kind == ExternalResourceKind::Audio {
546 "audio"
547 } else {
548 "video"
549 },
550 )
551 });
552
553 resources.push(ExternalResource {
554 url: AzString::from(src),
555 kind,
556 mime_type: mime.into(),
557 source_element: AzString::from("source"),
558 source_attribute: AzString::from("src"),
559 });
560 }
561 if let Some(srcset) = get_attr("srcset") {
563 for src in Self::parse_srcset(&srcset) {
564 let mime = Self::guess_mime_from_url(&src, "image");
565 resources.push(ExternalResource {
566 url: AzString::from(src),
567 kind: ExternalResourceKind::Image,
568 mime_type: mime.into(),
569 source_element: AzString::from("source"),
570 source_attribute: AzString::from("srcset"),
571 });
572 }
573 }
574 }
575 "a" => {
576 if let Some(href) = get_attr("href") {
577 if Self::looks_like_resource(&href) {
579 let mime = Self::guess_mime_from_url(&href, "");
580 resources.push(ExternalResource {
581 url: AzString::from(href),
582 kind: ExternalResourceKind::Unknown,
583 mime_type: mime.into(),
584 source_element: AzString::from("a"),
585 source_attribute: AzString::from("href"),
586 });
587 }
588 }
589 }
590 "virtualized-view" | "embed" | "object" => {
591 let src_attr = if tag_name == "object" { "data" } else { "src" };
592 if let Some(src) = get_attr(src_attr) {
593 resources.push(ExternalResource {
594 url: AzString::from(src),
595 kind: ExternalResourceKind::Unknown,
596 mime_type: OptionMimeTypeHint::None,
597 source_element: AzString::from(tag_name.clone()),
598 source_attribute: AzString::from(src_attr),
599 });
600 }
601 }
602 "style" => {
603 for child in node.children.as_ref() {
605 if let XmlNodeChild::Text(text) = child {
606 Self::extract_css_urls(text.as_str(), resources);
607 }
608 }
609 }
610 _ => {}
611 }
612
613 if let Some(style) = get_attr("style") {
615 Self::extract_css_urls(&style, resources);
616 }
617
618 if let Some(bg) = get_attr("background") {
620 let mime = Self::guess_mime_from_url(&bg, "image");
621 resources.push(ExternalResource {
622 url: AzString::from(bg),
623 kind: ExternalResourceKind::Image,
624 mime_type: mime.into(),
625 source_element: AzString::from(tag_name),
626 source_attribute: AzString::from("background"),
627 });
628 }
629
630 }
632
633 fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
635 let lower = css.to_ascii_lowercase();
646
647 let mut search_from = 0;
649 while let Some(rel) = lower[search_from..].find("url(") {
650 let url_start = search_from + rel;
651 let after = url_start + 4;
652 if lower[..url_start].trim_end().ends_with("@import") {
656 search_from = after;
657 continue;
658 }
659 let after_url = &css[after..];
660 if let Some(url) = Self::extract_url_value(after_url) {
661 let mime = Self::guess_mime_from_url(&url, "");
662 let kind = Self::guess_kind_from_url(&url);
663 resources.push(ExternalResource {
664 url: AzString::from(url),
665 kind,
666 mime_type: mime.into(),
667 source_element: AzString::from("style"),
668 source_attribute: AzString::from("url()"),
669 });
670 }
671 search_from = after;
672 }
673
674 let mut search_from = 0;
676 while let Some(rel) = lower[search_from..].find("@import") {
677 let after = search_from + rel + 7;
678 let after_import = &css[after..];
679 let trimmed = after_import.trim_start();
680
681 let import_url = if trimmed
685 .get(..4)
686 .is_some_and(|p| p.eq_ignore_ascii_case("url("))
687 {
688 Self::extract_url_value(&trimmed[4..])
689 } else {
690 Self::extract_quoted_string(trimmed)
691 };
692
693 if let Some(url) = import_url {
694 resources.push(ExternalResource {
695 url: AzString::from(url),
696 kind: ExternalResourceKind::Stylesheet,
697 mime_type: Some(MimeTypeHint::new("text/css")).into(),
698 source_element: AzString::from("style"),
699 source_attribute: AzString::from("@import"),
700 });
701 }
702
703 search_from = after;
704 }
705 }
706
707 fn extract_url_value(s: &str) -> Option<String> {
709 let trimmed = s.trim_start();
710 if trimmed.starts_with('"') {
711 Self::extract_quoted_string(trimmed)
712 } else if let Some(rest) = trimmed.strip_prefix('\'') {
713 let end = rest.find('\'')?;
714 Some(rest[..end].to_string())
715 } else {
716 let end = trimmed.find(')')?;
717 Some(trimmed[..end].trim().to_string())
718 }
719 }
720
721 fn extract_quoted_string(s: &str) -> Option<String> {
723 if let Some(rest) = s.strip_prefix('"') {
724 let end = rest.find('"')?;
725 Some(rest[..end].to_string())
726 } else if let Some(rest) = s.strip_prefix('\'') {
727 let end = rest.find('\'')?;
728 Some(rest[..end].to_string())
729 } else {
730 None
731 }
732 }
733
734 fn parse_srcset(srcset: &str) -> Vec<String> {
736 srcset
737 .split(',')
738 .filter_map(|entry| {
739 let trimmed = entry.trim();
740 trimmed
742 .split_whitespace()
743 .next()
744 .map(alloc::string::ToString::to_string)
745 })
746 .filter(|url| !url.is_empty())
747 .collect()
748 }
749
750 fn looks_like_resource(url: &str) -> bool {
752 let lower = url.to_lowercase();
753 let resource_exts = [
755 ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp", ".ttf", ".otf",
756 ".woff", ".woff2", ".eot", ".css", ".js", ".mp4", ".webm", ".ogg", ".mp3", ".wav",
757 ".pdf", ".zip", ".tar", ".gz",
758 ];
759 resource_exts.iter().any(|ext| lower.ends_with(ext))
760 }
761
762 #[allow(clippy::case_sensitive_file_extension_comparisons)]
766 fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
767 let lower = url.to_lowercase();
768 let path = lower.split('?').next().unwrap_or(&lower);
770 if path.ends_with(".png")
771 || path.ends_with(".jpg")
772 || path.ends_with(".jpeg")
773 || path.ends_with(".gif")
774 || path.ends_with(".webp")
775 || path.ends_with(".svg")
776 || path.ends_with(".bmp")
777 || path.ends_with(".avif")
778 {
779 ExternalResourceKind::Image
780 } else if path.ends_with(".ttf")
781 || path.ends_with(".otf")
782 || path.ends_with(".woff")
783 || path.ends_with(".woff2")
784 || path.ends_with(".eot")
785 {
786 ExternalResourceKind::Font
787 } else if path.ends_with(".css") {
788 ExternalResourceKind::Stylesheet
789 } else if path.ends_with(".js") || path.ends_with(".mjs") {
790 ExternalResourceKind::Script
791 } else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
792 ExternalResourceKind::Video
793 } else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
794 ExternalResourceKind::Audio
795 } else if path.ends_with(".ico") {
796 ExternalResourceKind::Icon
797 } else {
798 ExternalResourceKind::Unknown
799 }
800 }
801
802 fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
804 let lower = url.to_lowercase();
805 let ext = lower.rsplit('.').next()?;
807 let ext = ext.split('?').next()?;
809
810 let valid_exts = [
812 "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif", "ttf", "otf", "woff",
813 "woff2", "eot", "css", "js", "mjs", "mp4", "webm", "ogg", "mp3", "wav", "flac",
814 ];
815
816 if valid_exts.contains(&ext) {
817 Some(MimeTypeHint::from_extension(ext))
818 } else if !category.is_empty() {
819 match category {
821 "image" => Some(MimeTypeHint::new("image/*")),
822 "font" => Some(MimeTypeHint::new("font/*")),
823 "stylesheet" => Some(MimeTypeHint::new("text/css")),
824 "script" => Some(MimeTypeHint::new("application/javascript")),
825 "video" => Some(MimeTypeHint::new("video/*")),
826 "audio" => Some(MimeTypeHint::new("audio/*")),
827 _ => None,
828 }
829 } else {
830 None
831 }
832 }
833}
834
835#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
836#[repr(C)]
837pub struct NonXmlCharError {
838 pub ch: u32, pub pos: XmlTextPos,
840}
841
842#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
843#[repr(C)]
844pub struct InvalidCharError {
845 pub expected: u8,
846 pub got: u8,
847 pub pos: XmlTextPos,
848}
849
850#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
851#[repr(C)]
852pub struct InvalidCharMultipleError {
853 pub expected: u8,
854 pub got: U8Vec,
855 pub pos: XmlTextPos,
856}
857
858#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
859#[repr(C)]
860pub struct InvalidQuoteError {
861 pub got: u8,
862 pub pos: XmlTextPos,
863}
864
865#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
866#[repr(C)]
867pub struct InvalidSpaceError {
868 pub got: u8,
869 pub pos: XmlTextPos,
870}
871
872#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
873#[repr(C)]
874pub struct InvalidStringError {
875 pub got: AzString,
876 pub pos: XmlTextPos,
877}
878
879#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
880#[repr(C, u8)]
881pub enum XmlStreamError {
882 UnexpectedEndOfStream,
883 InvalidName,
884 NonXmlChar(NonXmlCharError),
885 InvalidChar(InvalidCharError),
886 InvalidCharMultiple(InvalidCharMultipleError),
887 InvalidQuote(InvalidQuoteError),
888 InvalidSpace(InvalidSpaceError),
889 InvalidString(InvalidStringError),
890 InvalidReference,
891 InvalidExternalID,
892 InvalidCommentData,
893 InvalidCommentEnd,
894 InvalidCharacterData,
895}
896
897impl fmt::Display for XmlStreamError {
898 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
899 use self::XmlStreamError::{
900 InvalidChar, InvalidCharMultiple, InvalidCharacterData, InvalidCommentData,
901 InvalidCommentEnd, InvalidExternalID, InvalidName, InvalidQuote, InvalidReference,
902 InvalidSpace, InvalidString, NonXmlChar, UnexpectedEndOfStream,
903 };
904 match self {
905 UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
906 InvalidName => write!(f, "Invalid name"),
907 NonXmlChar(nx) => write!(
908 f,
909 "Non-XML character: {:?} at {}",
910 core::char::from_u32(nx.ch),
911 nx.pos
912 ),
913 InvalidChar(ic) => write!(
914 f,
915 "Invalid character: expected: {}, got: {} at {}",
916 ic.expected as char, ic.got as char, ic.pos
917 ),
918 InvalidCharMultiple(imc) => write!(
919 f,
920 "Multiple invalid characters: expected: {}, got: {:?} at {}",
921 imc.expected,
922 imc.got.as_ref(),
923 imc.pos
924 ),
925 InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
926 InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
927 InvalidString(ise) => write!(
928 f,
929 "Invalid string: got \"{}\" at {}",
930 ise.got.as_str(),
931 ise.pos
932 ),
933 InvalidReference => write!(f, "Invalid reference"),
934 InvalidExternalID => write!(f, "Invalid external ID"),
935 InvalidCommentData => write!(f, "Invalid comment data"),
936 InvalidCommentEnd => write!(f, "Invalid comment end"),
937 InvalidCharacterData => write!(f, "Invalid character data"),
938 }
939 }
940}
941
942#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Ord, Hash, Eq)]
943#[repr(C)]
944pub struct XmlTextPos {
945 pub row: u32,
946 pub col: u32,
947}
948
949impl fmt::Display for XmlTextPos {
950 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951 write!(f, "line {}:{}", self.row, self.col)
952 }
953}
954
955#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
956#[repr(C)]
957pub struct XmlTextError {
958 pub stream_error: XmlStreamError,
959 pub pos: XmlTextPos,
960}
961
962#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
963#[repr(C, u8)]
964pub enum XmlParseError {
965 InvalidDeclaration(XmlTextError),
966 InvalidComment(XmlTextError),
967 InvalidPI(XmlTextError),
968 InvalidDoctype(XmlTextError),
969 InvalidEntity(XmlTextError),
970 InvalidElement(XmlTextError),
971 InvalidAttribute(XmlTextError),
972 InvalidCdata(XmlTextError),
973 InvalidCharData(XmlTextError),
974 UnknownToken(XmlTextPos),
975}
976
977impl fmt::Display for XmlParseError {
978 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
979 use self::XmlParseError::{
980 InvalidAttribute, InvalidCdata, InvalidCharData, InvalidComment, InvalidDeclaration,
981 InvalidDoctype, InvalidElement, InvalidEntity, InvalidPI, UnknownToken,
982 };
983 match self {
984 InvalidDeclaration(e) => {
985 write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
986 }
987 InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
988 InvalidPI(e) => write!(
989 f,
990 "Invalid processing instruction: {} at {}",
991 e.stream_error, e.pos
992 ),
993 InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
994 InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
995 InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
996 InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
997 InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
998 InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
999 UnknownToken(e) => write!(f, "Unknown token at {e}"),
1000 }
1001 }
1002}
1003
1004impl_result!(
1005 Xml,
1006 XmlError,
1007 ResultXmlXmlError,
1008 copy = false,
1009 [Debug, PartialEq, Eq, PartialOrd, Clone]
1010);
1011
1012#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1013#[repr(C)]
1014pub struct DuplicatedNamespaceError {
1015 pub ns: AzString,
1016 pub pos: XmlTextPos,
1017}
1018
1019#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1020#[repr(C)]
1021pub struct UnknownNamespaceError {
1022 pub ns: AzString,
1023 pub pos: XmlTextPos,
1024}
1025
1026#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1027#[repr(C)]
1028pub struct UnexpectedCloseTagError {
1029 pub expected: AzString,
1030 pub actual: AzString,
1031 pub pos: XmlTextPos,
1032}
1033
1034#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1035#[repr(C)]
1036pub struct UnknownEntityReferenceError {
1037 pub entity: AzString,
1038 pub pos: XmlTextPos,
1039}
1040
1041#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1042#[repr(C)]
1043pub struct DuplicatedAttributeError {
1044 pub attribute: AzString,
1045 pub pos: XmlTextPos,
1046}
1047
1048#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1050#[repr(C)]
1051pub struct MalformedHierarchyError {
1052 pub expected: AzString,
1054 pub got: AzString,
1056}
1057
1058#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
1059#[repr(C, u8)]
1060pub enum XmlError {
1061 NoParserAvailable,
1062 InvalidXmlPrefixUri(XmlTextPos),
1063 UnexpectedXmlUri(XmlTextPos),
1064 UnexpectedXmlnsUri(XmlTextPos),
1065 InvalidElementNamePrefix(XmlTextPos),
1066 DuplicatedNamespace(DuplicatedNamespaceError),
1067 UnknownNamespace(UnknownNamespaceError),
1068 UnexpectedCloseTag(UnexpectedCloseTagError),
1069 UnexpectedEntityCloseTag(XmlTextPos),
1070 UnknownEntityReference(UnknownEntityReferenceError),
1071 MalformedEntityReference(XmlTextPos),
1072 EntityReferenceLoop(XmlTextPos),
1073 InvalidAttributeValue(XmlTextPos),
1074 DuplicatedAttribute(DuplicatedAttributeError),
1075 NoRootNode,
1076 SizeLimit,
1077 DtdDetected,
1078 MalformedHierarchy(MalformedHierarchyError),
1080 ParserError(XmlParseError),
1081 UnclosedRootNode,
1082 UnexpectedDeclaration(XmlTextPos),
1083 NodesLimitReached,
1084 AttributesLimitReached,
1085 NamespacesLimitReached,
1086 InvalidName(XmlTextPos),
1087 NonXmlChar(XmlTextPos),
1088 InvalidChar(XmlTextPos),
1089 InvalidChar2(XmlTextPos),
1090 InvalidString(XmlTextPos),
1091 InvalidExternalID(XmlTextPos),
1092 InvalidComment(XmlTextPos),
1093 InvalidCharacterData(XmlTextPos),
1094 UnknownToken(XmlTextPos),
1095 UnexpectedEndOfStream,
1096}
1097
1098impl fmt::Display for XmlError {
1099 #[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1101 use self::XmlError::{
1102 AttributesLimitReached, DtdDetected, DuplicatedAttribute, DuplicatedNamespace,
1103 EntityReferenceLoop, InvalidAttributeValue, InvalidChar, InvalidChar2,
1104 InvalidCharacterData, InvalidComment, InvalidElementNamePrefix, InvalidExternalID,
1105 InvalidName, InvalidString, InvalidXmlPrefixUri, MalformedEntityReference,
1106 MalformedHierarchy, NamespacesLimitReached, NoParserAvailable, NoRootNode,
1107 NodesLimitReached, NonXmlChar, ParserError, SizeLimit, UnclosedRootNode,
1108 UnexpectedCloseTag, UnexpectedDeclaration, UnexpectedEndOfStream,
1109 UnexpectedEntityCloseTag, UnexpectedXmlUri, UnexpectedXmlnsUri, UnknownEntityReference,
1110 UnknownNamespace, UnknownToken,
1111 };
1112 match self {
1113 NoParserAvailable => write!(
1114 f,
1115 "Library was compiled without XML parser (XML parser not available)"
1116 ),
1117 InvalidXmlPrefixUri(pos) => {
1118 write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
1119 }
1120 UnexpectedXmlUri(pos) => {
1121 write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
1122 }
1123 UnexpectedXmlnsUri(pos) => write!(
1124 f,
1125 "Unexpected XML namespace URI at line {}:{}",
1126 pos.row, pos.col
1127 ),
1128 InvalidElementNamePrefix(pos) => write!(
1129 f,
1130 "Invalid element name prefix at line {}:{}",
1131 pos.row, pos.col
1132 ),
1133 DuplicatedNamespace(ns) => write!(
1134 f,
1135 "Duplicated namespace: \"{}\" at {}",
1136 ns.ns.as_str(),
1137 ns.pos
1138 ),
1139 UnknownNamespace(uns) => write!(
1140 f,
1141 "Unknown namespace: \"{}\" at {}",
1142 uns.ns.as_str(),
1143 uns.pos
1144 ),
1145 UnexpectedCloseTag(ct) => write!(
1146 f,
1147 "Unexpected close tag: expected \"{}\", got \"{}\" at {}",
1148 ct.expected.as_str(),
1149 ct.actual.as_str(),
1150 ct.pos
1151 ),
1152 UnexpectedEntityCloseTag(pos) => write!(
1153 f,
1154 "Unexpected entity close tag at line {}:{}",
1155 pos.row, pos.col
1156 ),
1157 UnknownEntityReference(uer) => write!(
1158 f,
1159 "Unexpected entity reference: \"{}\" at {}",
1160 uer.entity, uer.pos
1161 ),
1162 MalformedEntityReference(pos) => write!(
1163 f,
1164 "Malformed entity reference at line {}:{}",
1165 pos.row, pos.col
1166 ),
1167 EntityReferenceLoop(pos) => write!(
1168 f,
1169 "Entity reference loop (recursive entity reference) at line {}:{}",
1170 pos.row, pos.col
1171 ),
1172 InvalidAttributeValue(pos) => {
1173 write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
1174 }
1175 DuplicatedAttribute(ae) => write!(
1176 f,
1177 "Duplicated attribute \"{}\" at line {}:{}",
1178 ae.attribute.as_str(),
1179 ae.pos.row,
1180 ae.pos.col
1181 ),
1182 NoRootNode => write!(f, "No root node found"),
1183 SizeLimit => write!(f, "XML file too large (size limit reached)"),
1184 DtdDetected => write!(f, "Document type descriptor detected"),
1185 MalformedHierarchy(e) => write!(
1186 f,
1187 "Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
1188 e.expected.as_str(),
1189 e.got.as_str()
1190 ),
1191 ParserError(p) => write!(f, "{p}"),
1192 UnclosedRootNode => write!(f, "unclosed root node"),
1193 UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
1194 NodesLimitReached => write!(f, "nodes limit reached"),
1195 AttributesLimitReached => write!(f, "attributes limit reached"),
1196 NamespacesLimitReached => write!(f, "namespaces limit reached"),
1197 InvalidName(tp) => write!(f, "invalid name at {tp}"),
1198 NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
1199 InvalidChar(tp) => write!(f, "invalid char at {tp}"),
1200 InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
1201 InvalidString(tp) => write!(f, "invalid string at {tp}"),
1202 InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
1203 InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
1204 InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
1205 UnknownToken(tp) => write!(f, "unknown token at {tp}"),
1206 UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
1207 }
1208 }
1209}
1210
1211#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1219#[repr(C)]
1220pub struct ComponentId {
1221 pub collection: AzString,
1223 pub name: AzString,
1225}
1226
1227impl ComponentId {
1228 #[must_use]
1229 pub fn builtin(name: &str) -> Self {
1230 Self {
1231 collection: AzString::from_const_str("builtin"),
1232 name: AzString::from(name),
1233 }
1234 }
1235
1236 #[must_use]
1237 pub fn new(collection: &str, name: &str) -> Self {
1238 Self {
1239 collection: AzString::from(collection),
1240 name: AzString::from(name),
1241 }
1242 }
1243
1244 #[must_use]
1246 pub fn qualified_name(&self) -> String {
1247 format!("{}:{}", self.collection.as_str(), self.name.as_str())
1248 }
1249}
1250
1251#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1257#[repr(C)]
1258pub struct ComponentCallbackArg {
1259 pub name: AzString,
1261 pub arg_type: ComponentFieldType,
1263}
1264
1265impl_vec!(
1266 ComponentCallbackArg,
1267 ComponentCallbackArgVec,
1268 ComponentCallbackArgVecDestructor,
1269 ComponentCallbackArgVecDestructorType,
1270 ComponentCallbackArgVecSlice,
1271 OptionComponentCallbackArg
1272);
1273impl_option!(
1274 ComponentCallbackArg,
1275 OptionComponentCallbackArg,
1276 copy = false,
1277 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1278);
1279impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
1280impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
1281impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
1282impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
1283impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
1284impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
1285impl_vec_clone!(
1286 ComponentCallbackArg,
1287 ComponentCallbackArgVec,
1288 ComponentCallbackArgVecDestructor
1289);
1290
1291#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1293#[repr(C)]
1294pub struct ComponentCallbackSignature {
1295 pub return_type: AzString,
1297 pub args: ComponentCallbackArgVec,
1299}
1300
1301#[repr(C)]
1304pub struct ComponentFieldTypeBox {
1305 pub ptr: *mut ComponentFieldType,
1306}
1307
1308impl ComponentFieldTypeBox {
1309 #[must_use]
1310 pub fn new(t: ComponentFieldType) -> Self {
1311 Self {
1312 ptr: Box::into_raw(Box::new(t)),
1313 }
1314 }
1315
1316 #[must_use]
1317 pub fn as_ref(&self) -> &ComponentFieldType {
1318 unsafe { &*self.ptr }
1319 }
1320}
1321
1322impl Clone for ComponentFieldTypeBox {
1323 fn clone(&self) -> Self {
1324 Self::new(unsafe { (*self.ptr).clone() })
1325 }
1326}
1327
1328impl Drop for ComponentFieldTypeBox {
1329 fn drop(&mut self) {
1330 let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1337 if !ptr.is_null() {
1338 unsafe {
1339 drop(Box::from_raw(ptr));
1340 }
1341 }
1342 }
1343}
1344
1345impl fmt::Debug for ComponentFieldTypeBox {
1346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1347 if self.ptr.is_null() {
1348 write!(f, "ComponentFieldTypeBox(null)")
1349 } else {
1350 write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
1351 }
1352 }
1353}
1354
1355impl PartialEq for ComponentFieldTypeBox {
1356 fn eq(&self, other: &Self) -> bool {
1357 if self.ptr.is_null() && other.ptr.is_null() {
1358 return true;
1359 }
1360 if self.ptr.is_null() || other.ptr.is_null() {
1361 return false;
1362 }
1363 unsafe { *self.ptr == *other.ptr }
1364 }
1365}
1366
1367impl Eq for ComponentFieldTypeBox {}
1368
1369impl PartialOrd for ComponentFieldTypeBox {
1370 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1371 Some(self.cmp(other))
1372 }
1373}
1374
1375impl Ord for ComponentFieldTypeBox {
1376 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1377 match (self.ptr.is_null(), other.ptr.is_null()) {
1378 (true, true) => core::cmp::Ordering::Equal,
1379 (true, false) => core::cmp::Ordering::Less,
1380 (false, true) => core::cmp::Ordering::Greater,
1381 (false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
1382 }
1383 }
1384}
1385
1386impl Hash for ComponentFieldTypeBox {
1387 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1388 if !self.ptr.is_null() {
1389 unsafe {
1390 (*self.ptr).hash(state);
1391 }
1392 }
1393 }
1394}
1395
1396#[repr(C)]
1399pub struct ComponentFieldValueBox {
1400 pub ptr: *mut ComponentFieldValue,
1401}
1402
1403impl ComponentFieldValueBox {
1404 #[must_use]
1405 pub fn new(v: ComponentFieldValue) -> Self {
1406 Self {
1407 ptr: Box::into_raw(Box::new(v)),
1408 }
1409 }
1410
1411 #[must_use]
1412 pub fn as_ref(&self) -> &ComponentFieldValue {
1413 unsafe { &*self.ptr }
1414 }
1415}
1416
1417impl Clone for ComponentFieldValueBox {
1418 fn clone(&self) -> Self {
1419 Self::new(unsafe { (*self.ptr).clone() })
1420 }
1421}
1422
1423impl Drop for ComponentFieldValueBox {
1424 fn drop(&mut self) {
1425 let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
1428 if !ptr.is_null() {
1429 unsafe {
1430 drop(Box::from_raw(ptr));
1431 }
1432 }
1433 }
1434}
1435
1436impl fmt::Debug for ComponentFieldValueBox {
1437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1438 if self.ptr.is_null() {
1439 write!(f, "ComponentFieldValueBox(null)")
1440 } else {
1441 write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
1442 }
1443 }
1444}
1445
1446impl PartialEq for ComponentFieldValueBox {
1447 fn eq(&self, other: &Self) -> bool {
1448 if self.ptr.is_null() && other.ptr.is_null() {
1449 return true;
1450 }
1451 if self.ptr.is_null() || other.ptr.is_null() {
1452 return false;
1453 }
1454 unsafe { *self.ptr == *other.ptr }
1455 }
1456}
1457
1458#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1462#[repr(C, u8)]
1463pub enum ComponentFieldType {
1464 String,
1465 Bool,
1466 I32,
1467 I64,
1468 U32,
1469 U64,
1470 Usize,
1471 F32,
1472 F64,
1473 ColorU,
1474 CssProperty,
1475 ImageRef,
1476 FontRef,
1477 StyledDom,
1479 Callback(ComponentCallbackSignature),
1481 RefAny(AzString),
1483 OptionType(ComponentFieldTypeBox),
1485 VecType(ComponentFieldTypeBox),
1487 StructRef(AzString),
1489 EnumRef(AzString),
1491}
1492
1493impl ComponentFieldType {
1494 #[must_use]
1498 pub fn parse(s: &str) -> Option<Self> {
1499 Self::parse_depth(s, 0)
1500 }
1501
1502 fn parse_depth(s: &str, depth: usize) -> Option<Self> {
1509 if depth > MAX_TYPE_PARSE_DEPTH {
1510 return None;
1511 }
1512 let s = s.trim();
1513 match s {
1514 "String" | "string" => return Some(Self::String),
1515 "Bool" | "bool" => return Some(Self::Bool),
1516 "I32" | "i32" => return Some(Self::I32),
1517 "I64" | "i64" => return Some(Self::I64),
1518 "U32" | "u32" => return Some(Self::U32),
1519 "U64" | "u64" => return Some(Self::U64),
1520 "Usize" | "usize" => return Some(Self::Usize),
1521 "F32" | "f32" => return Some(Self::F32),
1522 "F64" | "f64" => return Some(Self::F64),
1523 "ColorU" => return Some(Self::ColorU),
1524 "CssProperty" => return Some(Self::CssProperty),
1525 "ImageRef" => return Some(Self::ImageRef),
1526 "FontRef" => return Some(Self::FontRef),
1527 "StyledDom" => return Some(Self::StyledDom),
1528 "RefAny" => return Some(Self::RefAny(AzString::from(""))),
1529 _ => {}
1530 }
1531
1532 if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
1534 let inner_type = Self::parse_depth(inner, depth + 1)?;
1535 return Some(Self::OptionType(ComponentFieldTypeBox::new(inner_type)));
1536 }
1537
1538 if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
1540 let inner_type = Self::parse_depth(inner, depth + 1)?;
1541 return Some(Self::VecType(ComponentFieldTypeBox::new(inner_type)));
1542 }
1543
1544 if let Some(sig) = s
1546 .strip_prefix("Callback(")
1547 .and_then(|r| r.strip_suffix(')'))
1548 {
1549 return Some(Self::Callback(ComponentCallbackSignature {
1550 return_type: AzString::from(sig),
1551 args: Vec::new().into(),
1552 }));
1553 }
1554
1555 if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
1557 return Some(Self::RefAny(AzString::from(hint)));
1558 }
1559
1560 if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
1562 return Some(Self::EnumRef(AzString::from(name)));
1563 }
1564
1565 if let Some(name) = s
1567 .strip_prefix("StructRef(")
1568 .and_then(|r| r.strip_suffix(')'))
1569 {
1570 return Some(Self::StructRef(AzString::from(name)));
1571 }
1572
1573 if s.chars().next().is_some_and(char::is_uppercase) {
1575 return Some(Self::StructRef(AzString::from(s)));
1576 }
1577
1578 None
1579 }
1580
1581 #[must_use]
1584 pub fn format(&self) -> String {
1585 match self {
1586 Self::String => "String".to_string(),
1587 Self::Bool => "Bool".to_string(),
1588 Self::I32 => "I32".to_string(),
1589 Self::I64 => "I64".to_string(),
1590 Self::U32 => "U32".to_string(),
1591 Self::U64 => "U64".to_string(),
1592 Self::Usize => "Usize".to_string(),
1593 Self::F32 => "F32".to_string(),
1594 Self::F64 => "F64".to_string(),
1595 Self::ColorU => "ColorU".to_string(),
1596 Self::CssProperty => "CssProperty".to_string(),
1597 Self::ImageRef => "ImageRef".to_string(),
1598 Self::FontRef => "FontRef".to_string(),
1599 Self::StyledDom => "StyledDom".to_string(),
1600 Self::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
1601 Self::RefAny(hint) => {
1602 if hint.as_str().is_empty() {
1603 "RefAny".to_string()
1604 } else {
1605 format!("RefAny({})", hint.as_str())
1606 }
1607 }
1608 Self::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
1609 Self::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
1610 Self::StructRef(name) | Self::EnumRef(name) => name.as_str().to_string(),
1611 }
1612 }
1613}
1614
1615impl fmt::Display for ComponentFieldType {
1616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1617 f.write_str(&self.format())
1618 }
1619}
1620
1621#[derive(Debug, Clone, PartialEq)]
1623#[repr(C)]
1624pub struct ComponentEnumVariant {
1625 pub name: AzString,
1627 pub description: AzString,
1629 pub fields: ComponentDataFieldVec,
1631}
1632
1633impl_vec!(
1634 ComponentEnumVariant,
1635 ComponentEnumVariantVec,
1636 ComponentEnumVariantVecDestructor,
1637 ComponentEnumVariantVecDestructorType,
1638 ComponentEnumVariantVecSlice,
1639 OptionComponentEnumVariant
1640);
1641impl_option!(
1642 ComponentEnumVariant,
1643 OptionComponentEnumVariant,
1644 copy = false,
1645 [Debug, Clone, PartialEq]
1646);
1647impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
1648impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
1649impl_vec_clone!(
1650 ComponentEnumVariant,
1651 ComponentEnumVariantVec,
1652 ComponentEnumVariantVecDestructor
1653);
1654
1655#[derive(Debug, Clone, PartialEq)]
1658#[repr(C)]
1659pub struct ComponentEnumModel {
1660 pub name: AzString,
1662 pub description: AzString,
1664 pub variants: ComponentEnumVariantVec,
1666}
1667
1668impl_vec!(
1669 ComponentEnumModel,
1670 ComponentEnumModelVec,
1671 ComponentEnumModelVecDestructor,
1672 ComponentEnumModelVecDestructorType,
1673 ComponentEnumModelVecSlice,
1674 OptionComponentEnumModel
1675);
1676impl_option!(
1677 ComponentEnumModel,
1678 OptionComponentEnumModel,
1679 copy = false,
1680 [Debug, Clone, PartialEq]
1681);
1682impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
1683impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
1684impl_vec_clone!(
1685 ComponentEnumModel,
1686 ComponentEnumModelVec,
1687 ComponentEnumModelVecDestructor
1688);
1689
1690#[derive(Debug, Clone, PartialEq)]
1692#[repr(C, u8)]
1693pub enum ComponentDefaultValue {
1694 None,
1696 String(AzString),
1698 Bool(bool),
1700 I32(i32),
1702 I64(i64),
1704 U32(u32),
1706 U64(u64),
1708 Usize(usize),
1710 F32(f32),
1712 F64(f64),
1714 ColorU(ColorU),
1716 ComponentInstance(ComponentInstanceDefault),
1718 CallbackFnPointer(AzString),
1720 Json(AzString),
1722}
1723
1724impl_option!(
1725 ComponentDefaultValue,
1726 OptionComponentDefaultValue,
1727 copy = false,
1728 [Debug, Clone, PartialEq]
1729);
1730
1731#[derive(Debug, Clone, PartialEq)]
1733#[repr(C)]
1734pub struct ComponentInstanceDefault {
1735 pub library: AzString,
1737 pub component: AzString,
1739 pub field_overrides: ComponentFieldOverrideVec,
1741}
1742
1743#[derive(Debug, Clone, PartialEq, Eq)]
1745#[repr(C)]
1746pub struct ComponentFieldOverride {
1747 pub field_name: AzString,
1749 pub source: ComponentFieldValueSource,
1751}
1752
1753impl_vec!(
1754 ComponentFieldOverride,
1755 ComponentFieldOverrideVec,
1756 ComponentFieldOverrideVecDestructor,
1757 ComponentFieldOverrideVecDestructorType,
1758 ComponentFieldOverrideVecSlice,
1759 OptionComponentFieldOverride
1760);
1761impl_option!(
1762 ComponentFieldOverride,
1763 OptionComponentFieldOverride,
1764 copy = false,
1765 [Debug, Clone, PartialEq, Eq]
1766);
1767impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
1768impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
1769impl_vec_clone!(
1770 ComponentFieldOverride,
1771 ComponentFieldOverrideVec,
1772 ComponentFieldOverrideVecDestructor
1773);
1774
1775#[derive(Debug, Clone, PartialEq, Eq)]
1777#[repr(C, u8)]
1778pub enum ComponentFieldValueSource {
1779 Default,
1781 Literal(AzString),
1783 Binding(AzString),
1785}
1786#[allow(variant_size_differences)]
1787#[derive(Debug, Clone, PartialEq)]
1791#[repr(C, u8)]
1792#[allow(clippy::large_enum_variant)] pub enum ComponentFieldValue {
1794 String(AzString),
1795 Bool(bool),
1796 I32(i32),
1797 I64(i64),
1798 U32(u32),
1799 U64(u64),
1800 Usize(usize),
1801 F32(f32),
1802 F64(f64),
1803 ColorU(ColorU),
1804 None,
1806 Some(ComponentFieldValueBox),
1808 Vec(ComponentFieldValueVec),
1810 StyledDom(StyledDom),
1812 Struct(ComponentFieldNamedValueVec),
1814 Enum {
1816 variant: AzString,
1817 fields: ComponentFieldNamedValueVec,
1818 },
1819 Callback(AzString),
1821 RefAny(crate::refany::RefAny),
1823}
1824
1825#[derive(Debug, Clone, PartialEq)]
1827#[repr(C)]
1828pub struct ComponentFieldNamedValue {
1829 pub name: AzString,
1830 pub value: ComponentFieldValue,
1831}
1832
1833impl_vec!(
1834 ComponentFieldNamedValue,
1835 ComponentFieldNamedValueVec,
1836 ComponentFieldNamedValueVecDestructor,
1837 ComponentFieldNamedValueVecDestructorType,
1838 ComponentFieldNamedValueVecSlice,
1839 OptionComponentFieldNamedValue
1840);
1841impl_option!(
1842 ComponentFieldNamedValue,
1843 OptionComponentFieldNamedValue,
1844 copy = false,
1845 [Debug, Clone, PartialEq]
1846);
1847impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1848impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1849impl_vec_clone!(
1850 ComponentFieldNamedValue,
1851 ComponentFieldNamedValueVec,
1852 ComponentFieldNamedValueVecDestructor
1853);
1854
1855impl ComponentFieldNamedValueVec {
1856 #[must_use]
1858 pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
1859 self.as_ref().iter().find_map(|v| {
1860 if v.name.as_str() == name {
1861 Some(&v.value)
1862 } else {
1863 None
1864 }
1865 })
1866 }
1867
1868 #[must_use]
1870 pub fn get_string(&self, name: &str) -> Option<&AzString> {
1871 match self.get_field(name) {
1872 Some(ComponentFieldValue::String(s)) => Some(s),
1873 _ => None,
1874 }
1875 }
1876}
1877
1878impl_vec!(
1879 ComponentFieldValue,
1880 ComponentFieldValueVec,
1881 ComponentFieldValueVecDestructor,
1882 ComponentFieldValueVecDestructorType,
1883 ComponentFieldValueVecSlice,
1884 OptionComponentFieldValue
1885);
1886impl_option!(
1887 ComponentFieldValue,
1888 OptionComponentFieldValue,
1889 copy = false,
1890 [Debug, Clone, PartialEq]
1891);
1892impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
1893impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
1894impl_vec_clone!(
1895 ComponentFieldValue,
1896 ComponentFieldValueVec,
1897 ComponentFieldValueVecDestructor
1898);
1899
1900#[derive(Debug, Clone, PartialEq)]
1902#[repr(C)]
1903pub struct ComponentDataField {
1904 pub name: AzString,
1906 pub field_type: ComponentFieldType,
1908 pub default_value: OptionComponentDefaultValue,
1910 pub required: bool,
1912 pub description: AzString,
1914}
1915
1916impl_vec!(
1917 ComponentDataField,
1918 ComponentDataFieldVec,
1919 ComponentDataFieldVecDestructor,
1920 ComponentDataFieldVecDestructorType,
1921 ComponentDataFieldVecSlice,
1922 OptionComponentDataField
1923);
1924impl_option!(
1925 ComponentDataField,
1926 OptionComponentDataField,
1927 copy = false,
1928 [Debug, Clone, PartialEq]
1929);
1930impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
1931impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
1932impl_vec_clone!(
1933 ComponentDataField,
1934 ComponentDataFieldVec,
1935 ComponentDataFieldVecDestructor
1936);
1937
1938#[derive(Debug, Clone)]
1945#[repr(C)]
1946pub struct ComponentDataModel {
1947 pub name: AzString,
1949 pub description: AzString,
1951 pub fields: ComponentDataFieldVec,
1953}
1954
1955impl ComponentDataModel {
1956 #[must_use]
1958 pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
1959 self.fields
1960 .as_ref()
1961 .iter()
1962 .find(|f| f.name.as_str() == name)
1963 }
1964
1965 #[must_use]
1967 pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
1968 self.get_field(name).and_then(|f| match &f.default_value {
1969 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
1970 _ => None,
1971 })
1972 }
1973
1974 #[must_use]
1977 pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
1978 let mut fields_vec = core::mem::replace(
1979 &mut self.fields,
1980 ComponentDataFieldVec::from_const_slice(&[]),
1981 )
1982 .into_library_owned_vec();
1983 for f in &mut fields_vec {
1984 if f.name.as_str() == name {
1985 f.default_value = OptionComponentDefaultValue::Some(value);
1986 break;
1987 }
1988 }
1989 self.fields = ComponentDataFieldVec::from_vec(fields_vec);
1990 self
1991 }
1992}
1993
1994impl_vec!(
1995 ComponentDataModel,
1996 ComponentDataModelVec,
1997 ComponentDataModelVecDestructor,
1998 ComponentDataModelVecDestructorType,
1999 ComponentDataModelVecSlice,
2000 OptionComponentDataModel
2001);
2002impl_option!(
2003 ComponentDataModel,
2004 OptionComponentDataModel,
2005 copy = false,
2006 [Debug, Clone]
2007);
2008impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
2009impl_vec_clone!(
2010 ComponentDataModel,
2011 ComponentDataModelVec,
2012 ComponentDataModelVecDestructor
2013);
2014impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
2015
2016#[cfg(feature = "serde-json")]
2021mod serde_impl {
2022 #[allow(clippy::wildcard_imports)] use super::*;
2024 use serde::ser::SerializeStruct;
2025 use serde::{Deserialize, Deserializer, Serialize, Serializer};
2026
2027 fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
2030 serializer.serialize_str(s.as_str())
2031 }
2032
2033 fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
2034 let s = String::deserialize(deserializer)?;
2035 Ok(AzString::from(s.as_str()))
2036 }
2037
2038 impl Serialize for ComponentFieldType {
2041 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2042 serializer.serialize_str(&field_type_to_string(self))
2043 }
2044 }
2045
2046 impl<'de> Deserialize<'de> for ComponentFieldType {
2047 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2048 let s = String::deserialize(deserializer)?;
2049 Ok(string_to_field_type(&s))
2050 }
2051 }
2052
2053 fn field_type_to_string(ft: &ComponentFieldType) -> String {
2054 match ft {
2055 ComponentFieldType::String => "String".into(),
2056 ComponentFieldType::Bool => "bool".into(),
2057 ComponentFieldType::I32 => "i32".into(),
2058 ComponentFieldType::I64 => "i64".into(),
2059 ComponentFieldType::U32 => "u32".into(),
2060 ComponentFieldType::U64 => "u64".into(),
2061 ComponentFieldType::Usize => "usize".into(),
2062 ComponentFieldType::F32 => "f32".into(),
2063 ComponentFieldType::F64 => "f64".into(),
2064 ComponentFieldType::ColorU => "ColorU".into(),
2065 ComponentFieldType::CssProperty => "CssProperty".into(),
2066 ComponentFieldType::ImageRef => "ImageRef".into(),
2067 ComponentFieldType::FontRef => "FontRef".into(),
2068 ComponentFieldType::StyledDom => "Dom".into(),
2069 ComponentFieldType::Callback(sig) => {
2070 alloc::format!("Callback({})", sig.return_type.as_str())
2071 }
2072 ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
2073 ComponentFieldType::OptionType(inner) => {
2074 alloc::format!("Option<{}>", field_type_to_string(inner.as_ref()))
2075 }
2076 ComponentFieldType::VecType(inner) => {
2077 alloc::format!("Vec<{}>", field_type_to_string(inner.as_ref()))
2078 }
2079 ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
2080 ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
2081 }
2082 }
2083
2084 #[allow(clippy::option_if_let_else)]
2088 fn string_to_field_type(s: &str) -> ComponentFieldType {
2089 match s {
2090 "String" | "string" => ComponentFieldType::String,
2091 "bool" | "Bool" => ComponentFieldType::Bool,
2092 "i32" | "I32" => ComponentFieldType::I32,
2093 "i64" | "I64" => ComponentFieldType::I64,
2094 "u32" | "U32" => ComponentFieldType::U32,
2095 "u64" | "U64" => ComponentFieldType::U64,
2096 "usize" | "Usize" => ComponentFieldType::Usize,
2097 "f32" | "F32" => ComponentFieldType::F32,
2098 "f64" | "F64" => ComponentFieldType::F64,
2099 "ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
2100 "CssProperty" => ComponentFieldType::CssProperty,
2101 "ImageRef" | "Image" => ComponentFieldType::ImageRef,
2102 "FontRef" | "Font" => ComponentFieldType::FontRef,
2103 "Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
2104 other => {
2105 if let Some(inner) = other
2106 .strip_prefix("Option<")
2107 .and_then(|s| s.strip_suffix('>'))
2108 {
2109 ComponentFieldType::OptionType(ComponentFieldTypeBox::new(
2110 string_to_field_type(inner),
2111 ))
2112 } else if let Some(inner) =
2113 other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>'))
2114 {
2115 ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(
2116 inner,
2117 )))
2118 } else if let Some(name) = other.strip_prefix("struct:") {
2119 ComponentFieldType::StructRef(AzString::from(name))
2120 } else if let Some(name) = other.strip_prefix("enum:") {
2121 ComponentFieldType::EnumRef(AzString::from(name))
2122 } else if other.starts_with("Callback") {
2123 let ret = other
2124 .strip_prefix("Callback(")
2125 .and_then(|s| s.strip_suffix(')'))
2126 .unwrap_or("()");
2127 ComponentFieldType::Callback(ComponentCallbackSignature {
2128 return_type: AzString::from(ret),
2129 args: ComponentCallbackArgVec::from_const_slice(&[]),
2130 })
2131 } else if other.starts_with("RefAny") {
2132 let hint = other
2133 .strip_prefix("RefAny(")
2134 .and_then(|s| s.strip_suffix(')'))
2135 .unwrap_or("");
2136 ComponentFieldType::RefAny(AzString::from(hint))
2137 } else {
2138 ComponentFieldType::String }
2140 }
2141 }
2142 }
2143
2144 impl Serialize for ComponentDefaultValue {
2147 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2148 use serde::ser::SerializeMap;
2149 match self {
2150 Self::None => serializer.serialize_none(),
2151 Self::String(s) => serializer.serialize_str(s.as_str()),
2152 Self::Bool(b) => serializer.serialize_bool(*b),
2153 Self::I32(v) => serializer.serialize_i32(*v),
2154 Self::I64(v) => serializer.serialize_i64(*v),
2155 Self::U32(v) => serializer.serialize_u32(*v),
2156 Self::U64(v) => serializer.serialize_u64(*v),
2157 Self::Usize(v) => serializer.serialize_u64(*v as u64),
2158 Self::F32(v) => serializer.serialize_f32(*v),
2159 Self::F64(v) => serializer.serialize_f64(*v),
2160 Self::ColorU(c) => serializer.serialize_str(&alloc::format!(
2161 "#{:02x}{:02x}{:02x}{:02x}",
2162 c.r,
2163 c.g,
2164 c.b,
2165 c.a
2166 )),
2167 Self::ComponentInstance(ci) => {
2168 let mut map = serializer.serialize_map(Some(2))?;
2169 map.serialize_entry("library", ci.library.as_str())?;
2170 map.serialize_entry("component", ci.component.as_str())?;
2171 map.end()
2172 }
2173 Self::CallbackFnPointer(name) => serializer.serialize_str(name.as_str()),
2174 Self::Json(json_str) => {
2175 match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
2177 Ok(v) => v.serialize(serializer),
2178 Err(_) => serializer.serialize_str(json_str.as_str()),
2179 }
2180 }
2181 }
2182 }
2183 }
2184
2185 impl<'de> Deserialize<'de> for ComponentDefaultValue {
2186 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2187 let val = serde_json::Value::deserialize(deserializer)?;
2188 Ok(match val {
2191 serde_json::Value::Bool(b) => Self::Bool(b),
2192 serde_json::Value::Number(n) => n.as_i64().map_or_else(
2193 || n.as_f64().map_or(Self::None, Self::F64),
2194 |i| i32::try_from(i).map_or(Self::I64(i), Self::I32),
2195 ),
2196 serde_json::Value::String(s) => Self::String(AzString::from(s.as_str())),
2197 _ => Self::None,
2198 })
2199 }
2200 }
2201
2202 impl Serialize for OptionComponentDefaultValue {
2205 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2206 match self {
2207 Self::Some(v) => v.serialize(serializer),
2208 Self::None => serializer.serialize_none(),
2209 }
2210 }
2211 }
2212
2213 impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
2214 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2215 let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
2216 Ok(val.map_or(Self::None, Self::Some))
2217 }
2218 }
2219
2220 impl Serialize for ComponentDataField {
2223 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2224 let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
2225 s.serialize_field("name", self.name.as_str())?;
2226 s.serialize_field("type", &self.field_type)?;
2227 s.serialize_field("default", &self.default_value)?;
2228 s.serialize_field("required", &self.required)?;
2229 s.serialize_field("description", self.description.as_str())?;
2230 s.end()
2231 }
2232 }
2233
2234 impl<'de> Deserialize<'de> for ComponentDataField {
2235 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2236 #[derive(Deserialize)]
2237 struct Helper {
2238 name: String,
2239 #[serde(rename = "type", default = "default_type")]
2240 field_type: ComponentFieldType,
2241 #[serde(default)]
2242 default: OptionComponentDefaultValue,
2243 #[serde(default)]
2244 required: bool,
2245 #[serde(default)]
2246 description: String,
2247 }
2248 const fn default_type() -> ComponentFieldType {
2249 ComponentFieldType::String
2250 }
2251
2252 let h = Helper::deserialize(deserializer)?;
2253 Ok(Self {
2254 name: AzString::from(h.name.as_str()),
2255 field_type: h.field_type,
2256 default_value: h.default,
2257 required: h.required,
2258 description: AzString::from(h.description.as_str()),
2259 })
2260 }
2261 }
2262
2263 impl Serialize for ComponentDataModel {
2266 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2267 let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
2268 s.serialize_field("name", self.name.as_str())?;
2269 s.serialize_field("description", self.description.as_str())?;
2270 let fields: Vec<&ComponentDataField> = self.fields.as_ref().iter().collect();
2271 s.serialize_field("fields", &fields)?;
2272 s.end()
2273 }
2274 }
2275
2276 impl<'de> Deserialize<'de> for ComponentDataModel {
2277 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2285 use serde::de::{IgnoredAny, MapAccess, Visitor};
2286
2287 struct ModelVisitor;
2288
2289 impl<'de> Visitor<'de> for ModelVisitor {
2290 type Value = ComponentDataModel;
2291
2292 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2293 f.write_str("a data model object with `name`, `description` and `fields`")
2294 }
2295
2296 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
2297 let mut name: Option<String> = None;
2298 let mut description: Option<String> = None;
2299 let mut fields: Option<Vec<ComponentDataField>> = None;
2300
2301 while let Some(key) = map.next_key::<String>()? {
2302 match key.as_str() {
2303 "name" => name = Some(map.next_value()?),
2304 "description" => description = Some(map.next_value()?),
2305 "fields" => fields = Some(map.next_value()?),
2306 _ => {
2309 map.next_value::<IgnoredAny>()?;
2310 }
2311 }
2312 }
2313
2314 Ok(ComponentDataModel {
2315 name: AzString::from(name.unwrap_or_default().as_str()),
2316 description: AzString::from(description.unwrap_or_default().as_str()),
2317 fields: ComponentDataFieldVec::from_vec(fields.unwrap_or_default()),
2318 })
2319 }
2320 }
2321
2322 deserializer.deserialize_map(ModelVisitor)
2323 }
2324 }
2325}
2326
2327#[cfg(feature = "serde-json")]
2332impl ComponentDataModel {
2333 pub fn to_json(&self) -> Result<String, String> {
2340 serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{e}"))
2341 }
2342
2343 pub fn from_json(json: &str) -> Result<Self, String> {
2350 serde_json::from_str(json).map_err(|e| alloc::format!("{e}"))
2351 }
2352}
2353
2354#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2356#[repr(C)]
2357#[derive(Default)]
2358pub enum ComponentSource {
2359 Builtin,
2361 Compiled,
2363 #[default]
2365 UserDefined,
2366}
2367
2368impl ComponentSource {
2369 #[must_use]
2370 pub fn create() -> Self {
2371 Self::default()
2372 }
2373}
2374
2375#[allow(missing_copy_implementations)]
2380#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2381#[repr(C)]
2382pub enum CompileTarget {
2383 Rust,
2384 C,
2385 Cpp,
2386 Python,
2387}
2388
2389impl_result!(
2390 StyledDom,
2391 RenderDomError,
2392 ResultStyledDomRenderDomError,
2393 copy = false,
2394 [Debug, Clone, PartialEq]
2395);
2396
2397impl_result!(
2398 AzString,
2399 CompileError,
2400 ResultStringCompileError,
2401 copy = false,
2402 [Debug, Clone, PartialEq]
2403);
2404
2405pub type ComponentRenderFn =
2412 fn(&ComponentDef, &ComponentDataModel, &ComponentMap) -> ResultStyledDomRenderDomError;
2413
2414pub type ComponentCompileFn = fn(
2416 &ComponentDef,
2417 &CompileTarget,
2418 &ComponentDataModel,
2419 indent: usize,
2420) -> ResultStringCompileError;
2421
2422pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
2425
2426#[repr(C)]
2434pub struct RegisterComponentFn {
2435 pub cb: RegisterComponentFnType,
2436 pub ctx: crate::refany::OptionRefAny,
2439}
2440
2441impl_callback!(RegisterComponentFn, RegisterComponentFnType);
2442
2443pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
2446
2447#[repr(C)]
2455pub struct RegisterComponentLibraryFn {
2456 pub cb: RegisterComponentLibraryFnType,
2457 pub ctx: crate::refany::OptionRefAny,
2460}
2461
2462impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
2463
2464#[derive(Clone)]
2468#[repr(C)]
2469pub struct ComponentDef {
2470 pub id: ComponentId,
2472 pub display_name: AzString,
2474 pub description: AzString,
2476 pub css: AzString,
2478 pub source: ComponentSource,
2480 pub data_model: ComponentDataModel,
2486 pub render_fn: ComponentRenderFn,
2488 pub compile_fn: ComponentCompileFn,
2490 pub render_fn_source: OptionString,
2492 pub compile_fn_source: OptionString,
2494}
2495
2496impl fmt::Debug for ComponentDef {
2497 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2498 f.debug_struct("ComponentDef")
2499 .field("id", &self.id)
2500 .field("display_name", &self.display_name)
2501 .field("source", &self.source)
2502 .field("data_model", &self.data_model.name)
2503 .finish_non_exhaustive()
2504 }
2505}
2506
2507impl_vec!(
2508 ComponentDef,
2509 ComponentDefVec,
2510 ComponentDefVecDestructor,
2511 ComponentDefVecDestructorType,
2512 ComponentDefVecSlice,
2513 OptionComponentDef
2514);
2515impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
2516impl_vec_debug!(ComponentDef, ComponentDefVec);
2517impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
2518impl_vec_mut!(ComponentDef, ComponentDefVec);
2519
2520#[derive(Debug, Clone)]
2522#[repr(C)]
2523pub struct ComponentLibrary {
2524 pub name: AzString,
2526 pub version: AzString,
2528 pub description: AzString,
2530 pub components: ComponentDefVec,
2532 pub exportable: bool,
2534 pub modifiable: bool,
2537 pub data_models: ComponentDataModelVec,
2540 pub enum_models: ComponentEnumModelVec,
2543}
2544
2545impl_vec!(
2546 ComponentLibrary,
2547 ComponentLibraryVec,
2548 ComponentLibraryVecDestructor,
2549 ComponentLibraryVecDestructorType,
2550 ComponentLibraryVecSlice,
2551 OptionComponentLibrary
2552);
2553impl_option!(
2554 ComponentLibrary,
2555 OptionComponentLibrary,
2556 copy = false,
2557 [Debug, Clone]
2558);
2559impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
2560impl_vec_clone!(
2561 ComponentLibrary,
2562 ComponentLibraryVec,
2563 ComponentLibraryVecDestructor
2564);
2565impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
2566
2567#[derive(Debug, Clone)]
2569#[repr(C)]
2570pub struct ComponentMap {
2571 pub libraries: ComponentLibraryVec,
2573}
2574
2575impl ComponentMap {
2576 #[must_use]
2578 pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
2579 self.libraries
2580 .iter()
2581 .find(|lib| lib.name.as_str() == collection)
2582 .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
2583 }
2584
2585 #[must_use]
2587 pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
2588 self.get("builtin", name)
2589 }
2590
2591 #[must_use]
2593 pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
2594 if let Some((collection, name)) = qualified.split_once(':') {
2595 self.get(collection, name)
2596 } else {
2597 self.get_unqualified(qualified)
2598 }
2599 }
2600
2601 #[must_use]
2603 pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
2604 self.libraries.iter().filter(|lib| lib.exportable).collect()
2605 }
2606
2607 #[must_use]
2609 pub fn all_components(&self) -> Vec<&ComponentDef> {
2610 self.libraries
2611 .iter()
2612 .flat_map(|lib| lib.components.iter())
2613 .collect()
2614 }
2615}
2616
2617macro_rules! html_tag_node_types {
2629 ($($tag:literal => $variant:ident),* $(,)?) => {
2630 #[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
2633 match tag {
2634 "img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
2639 crate::resources::ImageRef::null_image(
2640 0,
2641 0,
2642 crate::resources::RawImageFormat::RGBA8,
2643 alloc::vec::Vec::new(),
2644 ),
2645 )),
2646 "icon" => NodeType::Icon(azul_css::css::BoxOrStatic::heap(
2652 azul_css::AzString::from_const_str(""),
2653 )),
2654 "transient-window" => NodeType::TransientWindow(
2660 crate::transient::TransientWindowConfig::closed(),
2661 ),
2662 $($tag => NodeType::$variant,)*
2663 _ => NodeType::Div,
2664 }
2665 }
2666
2667 fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
2670 match tag {
2671 "img" | "image" => NodeTypeTag::Img,
2674 "icon" => NodeTypeTag::Icon,
2675 "transient-window" => NodeTypeTag::TransientWindow,
2676 $($tag => NodeTypeTag::$variant,)*
2677 _ => NodeTypeTag::Div,
2678 }
2679 }
2680 };
2681}
2682
2683html_tag_node_types! {
2684 "html" => Html,
2686 "head" => Head,
2687 "title" => Title,
2688 "body" => Body,
2689 "div" => Div,
2691 "header" => Header,
2692 "footer" => Footer,
2693 "section" => Section,
2694 "article" => Article,
2695 "aside" => Aside,
2696 "nav" => Nav,
2697 "main" => Main,
2698 "figure" => Figure,
2699 "figcaption" => FigCaption,
2700 "address" => Address,
2701 "details" => Details,
2702 "summary" => Summary,
2703 "dialog" => Dialog,
2704 "h1" => H1,
2706 "h2" => H2,
2707 "h3" => H3,
2708 "h4" => H4,
2709 "h5" => H5,
2710 "h6" => H6,
2711 "p" => P,
2713 "span" => Span,
2714 "pre" => Pre,
2715 "code" => Code,
2716 "blockquote" => BlockQuote,
2717 "br" => Br,
2718 "hr" => Hr,
2719 "pagebreak" => PageBreak,
2720 "ul" => Ul,
2722 "ol" => Ol,
2723 "li" => Li,
2724 "dl" => Dl,
2725 "dt" => Dt,
2726 "dd" => Dd,
2727 "menu" => Menu,
2728 "menuitem" => MenuItem,
2729 "dir" => Dir,
2730 "table" => Table,
2732 "caption" => Caption,
2733 "thead" => THead,
2734 "tbody" => TBody,
2735 "tfoot" => TFoot,
2736 "tr" => Tr,
2737 "th" => Th,
2738 "td" => Td,
2739 "colgroup" => ColGroup,
2740 "col" => Col,
2741 "form" => Form,
2743 "fieldset" => FieldSet,
2744 "legend" => Legend,
2745 "label" => Label,
2746 "input" => Input,
2747 "button" => Button,
2748 "select" => Select,
2749 "optgroup" => OptGroup,
2750 "option" => SelectOption,
2751 "textarea" => TextArea,
2752 "output" => Output,
2753 "progress" => Progress,
2754 "meter" => Meter,
2755 "datalist" => DataList,
2756 "a" => A,
2758 "strong" => Strong,
2759 "em" => Em,
2760 "b" => B,
2761 "i" => I,
2762 "u" => U,
2763 "s" => S,
2764 "small" => Small,
2765 "mark" => Mark,
2766 "del" => Del,
2767 "ins" => Ins,
2768 "samp" => Samp,
2769 "kbd" => Kbd,
2770 "var" => Var,
2771 "cite" => Cite,
2772 "dfn" => Dfn,
2773 "abbr" => Abbr,
2774 "acronym" => Acronym,
2775 "q" => Q,
2776 "time" => Time,
2777 "sub" => Sub,
2778 "sup" => Sup,
2779 "big" => Big,
2780 "bdo" => Bdo,
2781 "bdi" => Bdi,
2782 "wbr" => Wbr,
2783 "ruby" => Ruby,
2784 "rt" => Rt,
2785 "rtc" => Rtc,
2786 "rp" => Rp,
2787 "data" => Data,
2788 "canvas" => Canvas,
2790 "object" => Object,
2791 "param" => Param,
2792 "embed" => Embed,
2793 "audio" => Audio,
2794 "video" => Video,
2795 "source" => Source,
2796 "track" => Track,
2797 "map" => Map,
2798 "area" => Area,
2799 "svg" => Svg,
2801 "g" => SvgG,
2802 "defs" => SvgDefs,
2803 "symbol" => SvgSymbol,
2804 "use" => SvgUse,
2805 "switch" => SvgSwitch,
2806 "path" => SvgPath,
2807 "circle" => SvgCircle,
2808 "rect" => SvgRect,
2809 "ellipse" => SvgEllipse,
2810 "line" => SvgLine,
2811 "polygon" => SvgPolygon,
2812 "polyline" => SvgPolyline,
2813 "tspan" => SvgTspan,
2814 "textpath" => SvgTextPath,
2815 "lineargradient" => SvgLinearGradient,
2816 "radialgradient" => SvgRadialGradient,
2817 "stop" => SvgStop,
2818 "pattern" => SvgPattern,
2819 "clippath" => SvgClipPathElement,
2820 "mask" => SvgMask,
2821 "filter" => SvgFilter,
2822 "feblend" => SvgFeBlend,
2823 "fecolormatrix" => SvgFeColorMatrix,
2824 "fecomponenttransfer" => SvgFeComponentTransfer,
2825 "fecomposite" => SvgFeComposite,
2826 "feconvolvematrix" => SvgFeConvolveMatrix,
2827 "fediffuselighting" => SvgFeDiffuseLighting,
2828 "fedisplacementmap" => SvgFeDisplacementMap,
2829 "fedistantlight" => SvgFeDistantLight,
2830 "fedropshadow" => SvgFeDropShadow,
2831 "feflood" => SvgFeFlood,
2832 "fefuncr" => SvgFeFuncR,
2833 "fefuncg" => SvgFeFuncG,
2834 "fefuncb" => SvgFeFuncB,
2835 "fefunca" => SvgFeFuncA,
2836 "fegaussianblur" => SvgFeGaussianBlur,
2837 "feimage" => SvgFeImage,
2838 "femerge" => SvgFeMerge,
2839 "femergenode" => SvgFeMergeNode,
2840 "femorphology" => SvgFeMorphology,
2841 "feoffset" => SvgFeOffset,
2842 "fepointlight" => SvgFePointLight,
2843 "fespecularlighting" => SvgFeSpecularLighting,
2844 "fespotlight" => SvgFeSpotLight,
2845 "fetile" => SvgFeTile,
2846 "feturbulence" => SvgFeTurbulence,
2847 "foreignobject" => SvgForeignObject,
2848 "desc" => SvgDesc,
2849 "view" => SvgView,
2850 "animate" => SvgAnimate,
2851 "animatemotion" => SvgAnimateMotion,
2852 "animatetransform" => SvgAnimateTransform,
2853 "set" => SvgSet,
2854 "mpath" => SvgMpath,
2855 "meta" => Meta,
2857 "link" => Link,
2858 "script" => Script,
2859 "style" => Style,
2860 "base" => Base,
2861}
2862
2863fn builtin_render_fn(
2866 def: &ComponentDef,
2867 data: &ComponentDataModel,
2868 _component_map: &ComponentMap,
2869) -> ResultStyledDomRenderDomError {
2870 let node_type = tag_to_node_type(def.id.name.as_str());
2871 let mut dom = Dom::create_node(node_type);
2872 if let Some(text_str) = data.get_default_string("text") {
2873 let prepared = prepare_string(text_str);
2874 if !prepared.is_empty() {
2875 dom = dom.with_children(
2876 alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
2877 prepared
2878 )]
2879 .into(),
2880 );
2881 }
2882 }
2883 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2884 r.into()
2885}
2886
2887fn builtin_compile_fn(
2890 def: &ComponentDef,
2891 target: &CompileTarget,
2892 data: &ComponentDataModel,
2893 indent: usize,
2894) -> ResultStringCompileError {
2895 let node_type = tag_to_node_type(def.id.name.as_str());
2896 let type_name = format!("{node_type:?}"); let text = data.get_default_string("text");
2898
2899 let r: Result<AzString, CompileError> = match target {
2900 CompileTarget::Rust => {
2901 text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
2902 "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text_do_not_use_without_block_level_wrapper(\"{}\")])",
2903 type_name,
2904 text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
2905 ).into()))
2906 }
2907 CompileTarget::C => {
2908 text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
2909 "AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{}\"))",
2910 text_str
2911 .as_str()
2912 .replace('\\', "\\\\")
2913 .replace('"', "\\\"")
2914 )
2915 .into()))
2916 }
2917 CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
2918 CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
2919 };
2920 r.into()
2921}
2922
2923fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn fmt::Display) {
2925 use crate::dom::{Dom, NodeType};
2926 let text = alloc::format!("{field_name}: {value}");
2927 children.push(
2928 Dom::create_node(NodeType::Div).with_children(
2929 alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
2930 text
2931 )]
2932 .into(),
2933 ),
2934 );
2935}
2936
2937#[allow(clippy::too_many_lines)] #[must_use]
2950pub fn user_defined_render_fn(
2951 def: &ComponentDef,
2952 data: &ComponentDataModel,
2953 component_map: &ComponentMap,
2954) -> ResultStyledDomRenderDomError {
2955 use crate::dom::{Dom, NodeType};
2956 use azul_css::css::Css;
2957
2958 let mut children: Vec<Dom> = Vec::new();
2959
2960 for field in data.fields.as_ref() {
2961 let field_name = field.name.as_str();
2962
2963 match &field.default_value {
2965 OptionComponentDefaultValue::None => {
2966 }
2968 OptionComponentDefaultValue::Some(default_val) => {
2969 match default_val {
2970 ComponentDefaultValue::String(s) => {
2971 let text = s.as_str().trim();
2972 if !text.is_empty() {
2973 let label_dom = Dom::create_node(NodeType::Div).with_children(
2974 alloc::vec![
2975 Dom::create_text_do_not_use_without_block_level_wrapper(
2976 text.to_string()
2977 )
2978 ]
2979 .into(),
2980 );
2981 children.push(label_dom);
2982 }
2983 }
2984 ComponentDefaultValue::Bool(v) => {
2985 push_scalar_field(&mut children, field_name, v);
2986 }
2987 ComponentDefaultValue::I32(v) => {
2988 push_scalar_field(&mut children, field_name, v);
2989 }
2990 ComponentDefaultValue::I64(v) => {
2991 push_scalar_field(&mut children, field_name, v);
2992 }
2993 ComponentDefaultValue::U32(v) => {
2994 push_scalar_field(&mut children, field_name, v);
2995 }
2996 ComponentDefaultValue::U64(v) => {
2997 push_scalar_field(&mut children, field_name, v);
2998 }
2999 ComponentDefaultValue::Usize(v) => {
3000 push_scalar_field(&mut children, field_name, v);
3001 }
3002 ComponentDefaultValue::F32(v) => {
3003 push_scalar_field(&mut children, field_name, v);
3004 }
3005 ComponentDefaultValue::F64(v) => {
3006 push_scalar_field(&mut children, field_name, v);
3007 }
3008 ComponentDefaultValue::ColorU(c) => {
3009 let text = alloc::format!(
3010 "{}: #{:02x}{:02x}{:02x}{:02x}",
3011 field_name,
3012 c.r,
3013 c.g,
3014 c.b,
3015 c.a
3016 );
3017 children.push(
3018 Dom::create_node(NodeType::Div).with_children(
3019 alloc::vec![
3020 Dom::create_text_do_not_use_without_block_level_wrapper(text)
3021 ]
3022 .into(),
3023 ),
3024 );
3025 }
3026 ComponentDefaultValue::ComponentInstance(ci) => {
3027 if let Some(sub_comp) =
3029 component_map.get(ci.library.as_str(), ci.component.as_str())
3030 {
3031 let sub_data = sub_comp.data_model.clone();
3032 match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
3033 ResultStyledDomRenderDomError::Ok(_styled_dom) => {
3034 let text = alloc::format!(
3037 "[{}:{}]",
3038 ci.library.as_str(),
3039 ci.component.as_str()
3040 );
3041 children.push(
3042 Dom::create_node(NodeType::Div).with_children(
3043 alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into(),
3044 ),
3045 );
3046 }
3047 ResultStyledDomRenderDomError::Err(_) => {
3048 let text = alloc::format!(
3050 "[Error rendering {}:{}]",
3051 ci.library.as_str(),
3052 ci.component.as_str()
3053 );
3054 children.push(
3055 Dom::create_node(NodeType::Div).with_children(
3056 alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into(),
3057 ),
3058 );
3059 }
3060 }
3061 } else {
3062 let text = alloc::format!(
3063 "[Unknown component {}:{}]",
3064 ci.library.as_str(),
3065 ci.component.as_str()
3066 );
3067 children.push(
3068 Dom::create_node(NodeType::Div).with_children(
3069 alloc::vec![
3070 Dom::create_text_do_not_use_without_block_level_wrapper(
3071 text
3072 )
3073 ]
3074 .into(),
3075 ),
3076 );
3077 }
3078 }
3079 ComponentDefaultValue::CallbackFnPointer(name) => {
3080 let text = alloc::format!("{}: fn({})", field_name, name.as_str());
3082 children.push(
3083 Dom::create_node(NodeType::Div).with_children(
3084 alloc::vec![
3085 Dom::create_text_do_not_use_without_block_level_wrapper(text)
3086 ]
3087 .into(),
3088 ),
3089 );
3090 }
3091 ComponentDefaultValue::Json(json_str) => {
3092 let text = alloc::format!("{}: {}", field_name, json_str.as_str());
3093 children.push(
3094 Dom::create_node(NodeType::Div).with_children(
3095 alloc::vec![
3096 Dom::create_text_do_not_use_without_block_level_wrapper(text)
3097 ]
3098 .into(),
3099 ),
3100 );
3101 }
3102 ComponentDefaultValue::None => {
3103 }
3105 }
3106 }
3107 }
3108 }
3109
3110 let mut wrapper = Dom::create_node(NodeType::Div);
3111 if !children.is_empty() {
3112 wrapper = wrapper.with_children(children.into());
3113 }
3114
3115 let css = if def.css.as_str().is_empty() {
3117 Css::empty()
3118 } else {
3119 Css::from_string(def.css.clone())
3120 };
3121
3122 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
3123 r.into()
3124}
3125
3126#[allow(clippy::too_many_lines)] #[must_use]
3136pub fn user_defined_compile_fn(
3137 def: &ComponentDef,
3138 target: &CompileTarget,
3139 data: &ComponentDataModel,
3140 indent: usize,
3141) -> ResultStringCompileError {
3142 let tag = def.id.name.as_str();
3143 let indent_str = " ".repeat(indent * 4);
3144 let inner_indent = " ".repeat((indent + 1) * 4);
3145
3146 let r: Result<AzString, CompileError> = match target {
3147 CompileTarget::Rust => {
3148 let mut lines = Vec::new();
3149 lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3150 lines.push(alloc::format!(
3151 "{indent_str}let mut children: Vec<Dom> = Vec::new();"
3152 ));
3153
3154 for field in data.fields.as_ref() {
3155 let fname = field.name.as_str();
3156 match &field.default_value {
3157 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3158 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3159 lines.push(alloc::format!(
3160 "{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"));"
3161 ));
3162 }
3163 OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
3164 lines.push(alloc::format!(
3165 "{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
3166 ));
3167 }
3168 OptionComponentDefaultValue::Some(
3169 ComponentDefaultValue::ComponentInstance(ci),
3170 ) => {
3171 let fn_name =
3172 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3173 lines.push(alloc::format!(
3174 "{}children.push({}()); // sub-component {}:{}",
3175 inner_indent,
3176 fn_name,
3177 ci.library.as_str(),
3178 ci.component.as_str()
3179 ));
3180 }
3181 _ => {
3182 lines.push(alloc::format!(
3184 "{}// field '{}': {:?}",
3185 inner_indent,
3186 fname,
3187 field.field_type
3188 ));
3189 }
3190 }
3191 }
3192
3193 lines.push(alloc::format!(
3194 "{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
3195 ));
3196 Ok(lines.join("\n").into())
3197 }
3198 CompileTarget::C => {
3199 let mut lines = Vec::new();
3200 lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
3201 lines.push(alloc::format!(
3202 "{indent_str}AzDom root = AzDom_createDiv();"
3203 ));
3204
3205 for field in data.fields.as_ref() {
3206 let fname = field.name.as_str();
3207 match &field.default_value {
3208 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3209 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3210 lines.push(alloc::format!(
3211 "{inner_indent}AzDom_addChild(&root, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{escaped}\")));"
3212 ));
3213 }
3214 OptionComponentDefaultValue::Some(
3215 ComponentDefaultValue::ComponentInstance(ci),
3216 ) => {
3217 let fn_name =
3218 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3219 lines.push(alloc::format!(
3220 "{inner_indent}AzDom_addChild(&root, {fn_name}());"
3221 ));
3222 }
3223 _ => {
3224 lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
3225 }
3226 }
3227 }
3228
3229 lines.push(alloc::format!("{indent_str}return root;"));
3230 Ok(lines.join("\n").into())
3231 }
3232 CompileTarget::Cpp => {
3233 let mut lines = Vec::new();
3234 lines.push(alloc::format!("{indent_str}// Component: {tag}"));
3235 lines.push(alloc::format!("{indent_str}auto root = Dom::create_div();"));
3236
3237 for field in data.fields.as_ref() {
3238 let fname = field.name.as_str();
3239 match &field.default_value {
3240 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3241 let escaped = s.as_str().replace('\\', "\\\\").replace('"', "\\\"");
3242 lines.push(alloc::format!(
3243 "{inner_indent}root.add_child(Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{escaped}\")));"
3244 ));
3245 }
3246 OptionComponentDefaultValue::Some(
3247 ComponentDefaultValue::ComponentInstance(ci),
3248 ) => {
3249 let fn_name =
3250 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3251 lines.push(alloc::format!("{inner_indent}root.add_child({fn_name}());"));
3252 }
3253 _ => {
3254 lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
3255 }
3256 }
3257 }
3258
3259 lines.push(alloc::format!("{indent_str}return root;"));
3260 Ok(lines.join("\n").into())
3261 }
3262 CompileTarget::Python => {
3263 let mut lines = Vec::new();
3264 lines.push(alloc::format!("{indent_str}# Component: {tag}"));
3265 lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
3266
3267 for field in data.fields.as_ref() {
3268 let fname = field.name.as_str();
3269 match &field.default_value {
3270 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
3271 let escaped = s
3272 .as_str()
3273 .replace('\\', "\\\\")
3274 .replace('"', "\\\"")
3275 .replace('\'', "\\'");
3276 lines.push(alloc::format!(
3277 "{inner_indent}root = root.with_child(Dom.create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"))"
3278 ));
3279 }
3280 OptionComponentDefaultValue::Some(
3281 ComponentDefaultValue::ComponentInstance(ci),
3282 ) => {
3283 let fn_name =
3284 alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
3285 lines.push(alloc::format!(
3286 "{inner_indent}root = root.with_child({fn_name}())"
3287 ));
3288 }
3289 _ => {
3290 lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
3291 }
3292 }
3293 }
3294
3295 lines.push(alloc::format!("{indent_str}return root"));
3296 Ok(lines.join("\n").into())
3297 }
3298 };
3299 r.into()
3300}
3301
3302fn builtin_component_def(
3314 tag: &str,
3315 display_name: &str,
3316 default_text: Option<&str>,
3317 css: &str,
3318) -> ComponentDef {
3319 let mut fields = builtin_data_model(tag);
3320 if let Some(text) = default_text {
3322 fields.push(data_field(
3323 "text",
3324 ComponentFieldType::String,
3325 Some(ComponentDefaultValue::String(AzString::from(text))),
3326 "Text content of the element",
3327 ));
3328 }
3329 let model_name = format!("{display_name}Data");
3330 ComponentDef {
3331 id: ComponentId::builtin(tag),
3332 display_name: AzString::from(display_name),
3333 description: AzString::from(format!("HTML <{tag}> element").as_str()),
3334 css: AzString::from(css),
3335 source: ComponentSource::Builtin,
3336 data_model: ComponentDataModel {
3337 name: AzString::from(model_name.as_str()),
3338 description: AzString::from(format!("Data model for <{tag}>").as_str()),
3339 fields: fields.into(),
3340 },
3341 render_fn: builtin_render_fn,
3342 compile_fn: builtin_compile_fn,
3343 render_fn_source: None.into(),
3344 compile_fn_source: None.into(),
3345 }
3346}
3347
3348fn data_field(
3350 name: &str,
3351 ft: ComponentFieldType,
3352 default: Option<ComponentDefaultValue>,
3353 description: &str,
3354) -> ComponentDataField {
3355 let required = default.is_none();
3356 ComponentDataField {
3357 name: AzString::from(name),
3358 field_type: ft,
3359 default_value: default.map_or_else(
3360 || OptionComponentDefaultValue::None,
3361 OptionComponentDefaultValue::Some,
3362 ),
3363 required,
3364 description: AzString::from(description),
3365 }
3366}
3367
3368#[allow(clippy::too_many_lines)] fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
3375 use ComponentDefaultValue as D;
3376 use ComponentFieldType::{Bool, String, I32};
3377 match tag {
3378 "a" => alloc::vec![
3379 data_field(
3380 "href",
3381 String,
3382 Some(D::String(AzString::from_const_str(""))),
3383 "URL the link points to"
3384 ),
3385 data_field(
3386 "target",
3387 String,
3388 Some(D::String(AzString::from_const_str(""))),
3389 "Where to open the linked document (_blank, _self, _parent, _top)"
3390 ),
3391 data_field(
3392 "rel",
3393 String,
3394 Some(D::String(AzString::from_const_str(""))),
3395 "Relationship between current and linked document"
3396 ),
3397 ],
3398 "img" | "image" => alloc::vec![
3399 data_field("src", String, None, "URL of the image"),
3400 data_field(
3401 "alt",
3402 String,
3403 Some(D::String(AzString::from_const_str(""))),
3404 "Alternative text for the image"
3405 ),
3406 data_field(
3407 "width",
3408 String,
3409 Some(D::String(AzString::from_const_str(""))),
3410 "Width of the image"
3411 ),
3412 data_field(
3413 "height",
3414 String,
3415 Some(D::String(AzString::from_const_str(""))),
3416 "Height of the image"
3417 ),
3418 ],
3419 "form" => alloc::vec![
3420 data_field(
3421 "action",
3422 String,
3423 Some(D::String(AzString::from_const_str(""))),
3424 "URL where form data is submitted"
3425 ),
3426 data_field(
3427 "method",
3428 String,
3429 Some(D::String(AzString::from_const_str("GET"))),
3430 "HTTP method for form submission (GET or POST)"
3431 ),
3432 ],
3433 "label" => alloc::vec![data_field(
3434 "for",
3435 String,
3436 Some(D::String(AzString::from_const_str(""))),
3437 "ID of the form element this label is for"
3438 ),],
3439 "button" => alloc::vec![
3440 data_field(
3441 "type",
3442 String,
3443 Some(D::String(AzString::from_const_str("button"))),
3444 "Button type (button, submit, reset)"
3445 ),
3446 data_field(
3447 "disabled",
3448 Bool,
3449 Some(D::Bool(false)),
3450 "Whether the button is disabled"
3451 ),
3452 ],
3453 "td" | "th" => alloc::vec![
3454 data_field(
3455 "colspan",
3456 I32,
3457 Some(D::I32(1)),
3458 "Number of columns the cell spans"
3459 ),
3460 data_field(
3461 "rowspan",
3462 I32,
3463 Some(D::I32(1)),
3464 "Number of rows the cell spans"
3465 ),
3466 ],
3467 "icon" => alloc::vec![data_field(
3468 "name",
3469 String,
3470 Some(D::String(AzString::from_const_str(""))),
3471 "Icon name"
3472 ),],
3473 "ol" => alloc::vec![
3474 data_field(
3475 "start",
3476 I32,
3477 Some(D::I32(1)),
3478 "Start value for the ordered list"
3479 ),
3480 data_field(
3481 "type",
3482 String,
3483 Some(D::String(AzString::from_const_str("1"))),
3484 "Numbering type (1, A, a, I, i)"
3485 ),
3486 ],
3487 "input" => alloc::vec![
3489 data_field(
3490 "type",
3491 String,
3492 Some(D::String(AzString::from_const_str("text"))),
3493 "Input type (text, password, email, number, checkbox, radio, etc.)"
3494 ),
3495 data_field(
3496 "name",
3497 String,
3498 Some(D::String(AzString::from_const_str(""))),
3499 "Name of the input for form submission"
3500 ),
3501 data_field(
3502 "value",
3503 String,
3504 Some(D::String(AzString::from_const_str(""))),
3505 "Current value of the input"
3506 ),
3507 data_field(
3508 "placeholder",
3509 String,
3510 Some(D::String(AzString::from_const_str(""))),
3511 "Placeholder text"
3512 ),
3513 data_field(
3514 "disabled",
3515 Bool,
3516 Some(D::Bool(false)),
3517 "Whether the input is disabled"
3518 ),
3519 data_field(
3520 "required",
3521 Bool,
3522 Some(D::Bool(false)),
3523 "Whether the input is required"
3524 ),
3525 data_field(
3526 "readonly",
3527 Bool,
3528 Some(D::Bool(false)),
3529 "Whether the input is read-only"
3530 ),
3531 data_field(
3532 "checked",
3533 Bool,
3534 Some(D::Bool(false)),
3535 "Whether the checkbox/radio is checked"
3536 ),
3537 data_field(
3538 "min",
3539 String,
3540 Some(D::String(AzString::from_const_str(""))),
3541 "Minimum value (for number, range, date)"
3542 ),
3543 data_field(
3544 "max",
3545 String,
3546 Some(D::String(AzString::from_const_str(""))),
3547 "Maximum value (for number, range, date)"
3548 ),
3549 data_field(
3550 "step",
3551 String,
3552 Some(D::String(AzString::from_const_str(""))),
3553 "Step increment (for number, range)"
3554 ),
3555 data_field(
3556 "pattern",
3557 String,
3558 Some(D::String(AzString::from_const_str(""))),
3559 "Regex pattern for validation"
3560 ),
3561 data_field(
3562 "maxlength",
3563 String,
3564 Some(D::String(AzString::from_const_str(""))),
3565 "Maximum number of characters"
3566 ),
3567 ],
3568 "select" => alloc::vec![
3569 data_field(
3570 "name",
3571 String,
3572 Some(D::String(AzString::from_const_str(""))),
3573 "Name for form submission"
3574 ),
3575 data_field(
3576 "multiple",
3577 Bool,
3578 Some(D::Bool(false)),
3579 "Whether multiple options can be selected"
3580 ),
3581 data_field(
3582 "disabled",
3583 Bool,
3584 Some(D::Bool(false)),
3585 "Whether the select is disabled"
3586 ),
3587 data_field(
3588 "required",
3589 Bool,
3590 Some(D::Bool(false)),
3591 "Whether selection is required"
3592 ),
3593 data_field(
3594 "size",
3595 String,
3596 Some(D::String(AzString::from_const_str(""))),
3597 "Number of visible options"
3598 ),
3599 ],
3600 "option" => alloc::vec![
3601 data_field(
3602 "value",
3603 String,
3604 Some(D::String(AzString::from_const_str(""))),
3605 "Value submitted with the form"
3606 ),
3607 data_field(
3608 "selected",
3609 Bool,
3610 Some(D::Bool(false)),
3611 "Whether this option is selected"
3612 ),
3613 data_field(
3614 "disabled",
3615 Bool,
3616 Some(D::Bool(false)),
3617 "Whether this option is disabled"
3618 ),
3619 ],
3620 "optgroup" => alloc::vec![
3621 data_field(
3622 "label",
3623 String,
3624 Some(D::String(AzString::from_const_str(""))),
3625 "Label for the option group"
3626 ),
3627 data_field(
3628 "disabled",
3629 Bool,
3630 Some(D::Bool(false)),
3631 "Whether the group is disabled"
3632 ),
3633 ],
3634 "textarea" => alloc::vec![
3635 data_field(
3636 "name",
3637 String,
3638 Some(D::String(AzString::from_const_str(""))),
3639 "Name for form submission"
3640 ),
3641 data_field(
3642 "placeholder",
3643 String,
3644 Some(D::String(AzString::from_const_str(""))),
3645 "Placeholder text"
3646 ),
3647 data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
3648 data_field(
3649 "cols",
3650 I32,
3651 Some(D::I32(20)),
3652 "Visible width in average character widths"
3653 ),
3654 data_field(
3655 "disabled",
3656 Bool,
3657 Some(D::Bool(false)),
3658 "Whether the textarea is disabled"
3659 ),
3660 data_field(
3661 "required",
3662 Bool,
3663 Some(D::Bool(false)),
3664 "Whether content is required"
3665 ),
3666 data_field(
3667 "readonly",
3668 Bool,
3669 Some(D::Bool(false)),
3670 "Whether the textarea is read-only"
3671 ),
3672 data_field(
3673 "maxlength",
3674 String,
3675 Some(D::String(AzString::from_const_str(""))),
3676 "Maximum number of characters"
3677 ),
3678 ],
3679 "fieldset" => alloc::vec![data_field(
3680 "disabled",
3681 Bool,
3682 Some(D::Bool(false)),
3683 "Whether all controls in the fieldset are disabled"
3684 ),],
3685 "output" => alloc::vec![
3686 data_field(
3687 "for",
3688 String,
3689 Some(D::String(AzString::from_const_str(""))),
3690 "IDs of elements that contributed to the output"
3691 ),
3692 data_field(
3693 "name",
3694 String,
3695 Some(D::String(AzString::from_const_str(""))),
3696 "Name for form submission"
3697 ),
3698 ],
3699 "progress" => alloc::vec![
3700 data_field(
3701 "value",
3702 String,
3703 Some(D::String(AzString::from_const_str(""))),
3704 "Current progress value"
3705 ),
3706 data_field(
3707 "max",
3708 String,
3709 Some(D::String(AzString::from_const_str("1"))),
3710 "Maximum value"
3711 ),
3712 ],
3713 "meter" => alloc::vec![
3714 data_field(
3715 "value",
3716 String,
3717 Some(D::String(AzString::from_const_str(""))),
3718 "Current value"
3719 ),
3720 data_field(
3721 "min",
3722 String,
3723 Some(D::String(AzString::from_const_str("0"))),
3724 "Minimum value"
3725 ),
3726 data_field(
3727 "max",
3728 String,
3729 Some(D::String(AzString::from_const_str("1"))),
3730 "Maximum value"
3731 ),
3732 data_field(
3733 "low",
3734 String,
3735 Some(D::String(AzString::from_const_str(""))),
3736 "Low threshold"
3737 ),
3738 data_field(
3739 "high",
3740 String,
3741 Some(D::String(AzString::from_const_str(""))),
3742 "High threshold"
3743 ),
3744 data_field(
3745 "optimum",
3746 String,
3747 Some(D::String(AzString::from_const_str(""))),
3748 "Optimum value"
3749 ),
3750 ],
3751 "details" => alloc::vec![data_field(
3753 "open",
3754 Bool,
3755 Some(D::Bool(false)),
3756 "Whether the details are visible"
3757 ),],
3758 "dialog" => alloc::vec![data_field(
3759 "open",
3760 Bool,
3761 Some(D::Bool(false)),
3762 "Whether the dialog is active and can be interacted with"
3763 ),],
3764 "audio" | "video" => alloc::vec![
3766 data_field(
3767 "src",
3768 String,
3769 Some(D::String(AzString::from_const_str(""))),
3770 "URL of the media resource"
3771 ),
3772 data_field(
3773 "controls",
3774 Bool,
3775 Some(D::Bool(false)),
3776 "Whether to show playback controls"
3777 ),
3778 data_field(
3779 "autoplay",
3780 Bool,
3781 Some(D::Bool(false)),
3782 "Whether to start playing automatically"
3783 ),
3784 data_field(
3785 "loop",
3786 Bool,
3787 Some(D::Bool(false)),
3788 "Whether to loop playback"
3789 ),
3790 data_field(
3791 "muted",
3792 Bool,
3793 Some(D::Bool(false)),
3794 "Whether audio is muted"
3795 ),
3796 data_field(
3797 "preload",
3798 String,
3799 Some(D::String(AzString::from_const_str("auto"))),
3800 "Preload hint (none, metadata, auto)"
3801 ),
3802 ],
3803 "source" => alloc::vec![
3804 data_field("src", String, None, "URL of the media resource"),
3805 data_field(
3806 "type",
3807 String,
3808 Some(D::String(AzString::from_const_str(""))),
3809 "MIME type of the resource"
3810 ),
3811 ],
3812 "track" => alloc::vec![
3813 data_field("src", String, None, "URL of the track file"),
3814 data_field(
3815 "kind",
3816 String,
3817 Some(D::String(AzString::from_const_str("subtitles"))),
3818 "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"
3819 ),
3820 data_field(
3821 "srclang",
3822 String,
3823 Some(D::String(AzString::from_const_str(""))),
3824 "Language of the track text"
3825 ),
3826 data_field(
3827 "label",
3828 String,
3829 Some(D::String(AzString::from_const_str(""))),
3830 "User-readable title for the track"
3831 ),
3832 data_field(
3833 "default",
3834 Bool,
3835 Some(D::Bool(false)),
3836 "Whether this is the default track"
3837 ),
3838 ],
3839 "canvas" => alloc::vec![
3840 data_field(
3841 "width",
3842 String,
3843 Some(D::String(AzString::from_const_str("300"))),
3844 "Width of the canvas in pixels"
3845 ),
3846 data_field(
3847 "height",
3848 String,
3849 Some(D::String(AzString::from_const_str("150"))),
3850 "Height of the canvas in pixels"
3851 ),
3852 ],
3853 "embed" => alloc::vec![
3854 data_field("src", String, None, "URL of the resource to embed"),
3855 data_field(
3856 "type",
3857 String,
3858 Some(D::String(AzString::from_const_str(""))),
3859 "MIME type of the embedded content"
3860 ),
3861 data_field(
3862 "width",
3863 String,
3864 Some(D::String(AzString::from_const_str(""))),
3865 "Width"
3866 ),
3867 data_field(
3868 "height",
3869 String,
3870 Some(D::String(AzString::from_const_str(""))),
3871 "Height"
3872 ),
3873 ],
3874 "object" => alloc::vec![
3875 data_field(
3876 "data",
3877 String,
3878 Some(D::String(AzString::from_const_str(""))),
3879 "URL of the resource"
3880 ),
3881 data_field(
3882 "type",
3883 String,
3884 Some(D::String(AzString::from_const_str(""))),
3885 "MIME type of the resource"
3886 ),
3887 data_field(
3888 "width",
3889 String,
3890 Some(D::String(AzString::from_const_str(""))),
3891 "Width"
3892 ),
3893 data_field(
3894 "height",
3895 String,
3896 Some(D::String(AzString::from_const_str(""))),
3897 "Height"
3898 ),
3899 ],
3900 "param" => alloc::vec![
3901 data_field("name", String, None, "Name of the parameter"),
3902 data_field(
3903 "value",
3904 String,
3905 Some(D::String(AzString::from_const_str(""))),
3906 "Value of the parameter"
3907 ),
3908 ],
3909 "area" => alloc::vec![
3910 data_field(
3911 "shape",
3912 String,
3913 Some(D::String(AzString::from_const_str("default"))),
3914 "Shape of the area (default, rect, circle, poly)"
3915 ),
3916 data_field(
3917 "coords",
3918 String,
3919 Some(D::String(AzString::from_const_str(""))),
3920 "Coordinates of the area"
3921 ),
3922 data_field(
3923 "href",
3924 String,
3925 Some(D::String(AzString::from_const_str(""))),
3926 "URL for the area link"
3927 ),
3928 data_field(
3929 "alt",
3930 String,
3931 Some(D::String(AzString::from_const_str(""))),
3932 "Alternative text"
3933 ),
3934 data_field(
3935 "target",
3936 String,
3937 Some(D::String(AzString::from_const_str(""))),
3938 "Where to open the linked document"
3939 ),
3940 ],
3941 "map" => alloc::vec![data_field(
3942 "name",
3943 String,
3944 None,
3945 "Name of the image map (referenced by usemap)"
3946 ),],
3947 "time" => alloc::vec![data_field(
3949 "datetime",
3950 String,
3951 Some(D::String(AzString::from_const_str(""))),
3952 "Machine-readable date/time value"
3953 ),],
3954 "data" => alloc::vec![data_field(
3955 "value",
3956 String,
3957 Some(D::String(AzString::from_const_str(""))),
3958 "Machine-readable value"
3959 ),],
3960 "abbr" | "acronym" | "dfn" => alloc::vec![data_field(
3961 "title",
3962 String,
3963 Some(D::String(AzString::from_const_str(""))),
3964 "Full expansion or definition"
3965 ),],
3966 "q" | "blockquote" => alloc::vec![data_field(
3967 "cite",
3968 String,
3969 Some(D::String(AzString::from_const_str(""))),
3970 "URL of the source of the quotation"
3971 ),],
3972 "del" | "ins" => alloc::vec![
3973 data_field(
3974 "cite",
3975 String,
3976 Some(D::String(AzString::from_const_str(""))),
3977 "URL explaining the change"
3978 ),
3979 data_field(
3980 "datetime",
3981 String,
3982 Some(D::String(AzString::from_const_str(""))),
3983 "Date/time of the change"
3984 ),
3985 ],
3986 "bdo" => alloc::vec![data_field(
3987 "dir",
3988 String,
3989 Some(D::String(AzString::from_const_str("ltr"))),
3990 "Text direction (ltr, rtl)"
3991 ),],
3992 "col" | "colgroup" => alloc::vec![data_field(
3993 "span",
3994 I32,
3995 Some(D::I32(1)),
3996 "Number of columns the element spans"
3997 ),],
3998 "meta" => alloc::vec![
4000 data_field(
4001 "name",
4002 String,
4003 Some(D::String(AzString::from_const_str(""))),
4004 "Metadata name"
4005 ),
4006 data_field(
4007 "content",
4008 String,
4009 Some(D::String(AzString::from_const_str(""))),
4010 "Metadata value"
4011 ),
4012 data_field(
4013 "charset",
4014 String,
4015 Some(D::String(AzString::from_const_str(""))),
4016 "Character encoding"
4017 ),
4018 data_field(
4019 "http-equiv",
4020 String,
4021 Some(D::String(AzString::from_const_str(""))),
4022 "HTTP header equivalent"
4023 ),
4024 ],
4025 "link" => alloc::vec![
4026 data_field("rel", String, None, "Relationship type"),
4027 data_field(
4028 "href",
4029 String,
4030 Some(D::String(AzString::from_const_str(""))),
4031 "URL of the linked resource"
4032 ),
4033 data_field(
4034 "type",
4035 String,
4036 Some(D::String(AzString::from_const_str(""))),
4037 "MIME type of the linked resource"
4038 ),
4039 ],
4040 "script" => alloc::vec![
4041 data_field(
4042 "src",
4043 String,
4044 Some(D::String(AzString::from_const_str(""))),
4045 "URL of external script"
4046 ),
4047 data_field(
4048 "type",
4049 String,
4050 Some(D::String(AzString::from_const_str(""))),
4051 "MIME type or module"
4052 ),
4053 data_field(
4054 "async",
4055 Bool,
4056 Some(D::Bool(false)),
4057 "Execute asynchronously"
4058 ),
4059 data_field(
4060 "defer",
4061 Bool,
4062 Some(D::Bool(false)),
4063 "Defer execution until page load"
4064 ),
4065 ],
4066 "style" => alloc::vec![data_field(
4067 "type",
4068 String,
4069 Some(D::String(AzString::from_const_str("text/css"))),
4070 "MIME type of the style sheet"
4071 ),],
4072 "base" => alloc::vec![
4073 data_field(
4074 "href",
4075 String,
4076 Some(D::String(AzString::from_const_str(""))),
4077 "Base URL for relative URLs"
4078 ),
4079 data_field(
4080 "target",
4081 String,
4082 Some(D::String(AzString::from_const_str(""))),
4083 "Default target for hyperlinks"
4084 ),
4085 ],
4086 _ => alloc::vec![],
4087 }
4088}
4089
4090impl Default for ComponentMap {
4091 fn default() -> Self {
4097 Self {
4098 libraries: ComponentLibraryVec::from_const_slice(&[]),
4099 }
4100 }
4101}
4102
4103impl ComponentMap {
4104 #[must_use]
4105 pub fn create() -> Self {
4106 Self::default()
4107 }
4108
4109 #[must_use]
4111 pub fn with_builtin() -> Self {
4112 Self {
4113 libraries: alloc::vec![register_builtin_components()].into(),
4114 }
4115 }
4116
4117 #[must_use]
4123 pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
4124 Self {
4125 libraries: libs.clone(),
4126 }
4127 }
4128}
4129
4130fn xml_attrs_to_data_model(
4145 base_model: &ComponentDataModel,
4146 xml_attributes: &XmlAttributeMap,
4147 text_content: Option<&str>,
4148) -> ComponentDataModel {
4149 let mut model = base_model.clone();
4150
4151 let mut fields_vec = core::mem::replace(
4153 &mut model.fields,
4154 ComponentDataFieldVec::from_const_slice(&[]),
4155 )
4156 .into_library_owned_vec();
4157
4158 for field in &mut fields_vec {
4159 if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
4160 field.default_value = OptionComponentDefaultValue::Some(ComponentDefaultValue::String(
4162 attr_value.clone(),
4163 ));
4164 }
4165 }
4166
4167 model.fields = ComponentDataFieldVec::from_vec(fields_vec);
4168
4169 if let Some(text) = text_content {
4171 let prepared = prepare_string(text);
4172 if !prepared.is_empty() {
4173 model = model.with_default(
4174 "text",
4175 ComponentDefaultValue::String(AzString::from(prepared.as_str())),
4176 );
4177 }
4178 }
4179
4180 model
4181}
4182
4183fn builtin_if_component() -> ComponentDef {
4190 ComponentDef {
4191 id: ComponentId::builtin("if"),
4192 display_name: AzString::from_const_str("If"),
4193 description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
4194 css: AzString::from_const_str(""),
4195 source: ComponentSource::Builtin,
4196 data_model: ComponentDataModel {
4197 name: AzString::from_const_str("IfData"),
4198 description: AzString::from_const_str("Data for conditional rendering"),
4199 fields: alloc::vec![
4200 data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
4201 ].into(),
4202 },
4203 render_fn: builtin_if_render_fn,
4204 compile_fn: builtin_if_compile_fn,
4205 render_fn_source: None.into(),
4206 compile_fn_source: None.into(),
4207 }
4208}
4209
4210fn builtin_if_render_fn(
4211 _comp: &ComponentDef,
4212 data_model: &ComponentDataModel,
4213 _component_map: &ComponentMap,
4214) -> ResultStyledDomRenderDomError {
4215 let condition = data_model
4217 .fields
4218 .iter()
4219 .find(|f| f.name.as_str() == "condition")
4220 .and_then(|f| match &f.default_value {
4221 OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b),
4222 _ => None,
4223 })
4224 .unwrap_or(false);
4225
4226 let label = if condition {
4227 "if: true (then branch)"
4228 } else {
4229 "if: false (else branch)"
4230 };
4231 let mut dom = Dom::create_node(NodeType::Div).with_children(
4232 alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
4233 label
4234 )]
4235 .into(),
4236 );
4237 let css = Css::empty();
4238 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4239}
4240
4241fn builtin_if_compile_fn(
4242 _comp: &ComponentDef,
4243 target: &CompileTarget,
4244 _data: &ComponentDataModel,
4245 _indent: usize,
4246) -> ResultStringCompileError {
4247 match target {
4248 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4249 "if data.condition {\n // then branch\n Dom::create_div()\n} else {\n // else branch\n Dom::create_div()\n}"
4250 )),
4251 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4252 "if (data.condition) {\n // then branch\n AzDom_createDiv();\n} else {\n // else branch\n AzDom_createDiv();\n}"
4253 )),
4254 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4255 "if (data.condition) {\n // then branch\n Dom::create_div();\n} else {\n // else branch\n Dom::create_div();\n}"
4256 )),
4257 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4258 "if data.condition:\n # then branch\n Dom.create_div()\nelse:\n # else branch\n Dom.create_div()"
4259 )),
4260 }
4261}
4262
4263fn builtin_for_component() -> ComponentDef {
4266 ComponentDef {
4267 id: ComponentId::builtin("for"),
4268 display_name: AzString::from_const_str("For Loop"),
4269 description: AzString::from_const_str(
4270 "Iterative rendering: repeats children 'count' times.",
4271 ),
4272 css: AzString::from_const_str(""),
4273 source: ComponentSource::Builtin,
4274 data_model: ComponentDataModel {
4275 name: AzString::from_const_str("ForData"),
4276 description: AzString::from_const_str("Data for iterative rendering"),
4277 fields: alloc::vec![data_field(
4278 "count",
4279 ComponentFieldType::U32,
4280 Some(ComponentDefaultValue::U32(3)),
4281 "Number of iterations"
4282 ),]
4283 .into(),
4284 },
4285 render_fn: builtin_for_render_fn,
4286 compile_fn: builtin_for_compile_fn,
4287 render_fn_source: None.into(),
4288 compile_fn_source: None.into(),
4289 }
4290}
4291
4292fn builtin_for_render_fn(
4293 _comp: &ComponentDef,
4294 data_model: &ComponentDataModel,
4295 _component_map: &ComponentMap,
4296) -> ResultStyledDomRenderDomError {
4297 let count = data_model
4298 .fields
4299 .iter()
4300 .find(|f| f.name.as_str() == "count")
4301 .and_then(|f| match &f.default_value {
4302 OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n),
4303 _ => None,
4304 })
4305 .unwrap_or(3);
4306
4307 let mut items: Vec<Dom> = Vec::new();
4308 for i in 0..count {
4309 items.push(
4310 Dom::create_node(NodeType::Div).with_children(
4311 alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
4312 alloc::format!("Item {i}")
4313 )]
4314 .into(),
4315 ),
4316 );
4317 }
4318 let mut dom = Dom::create_node(NodeType::Div).with_children(items.into());
4319 let css = Css::empty();
4320 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4321}
4322
4323fn builtin_for_compile_fn(
4324 _comp: &ComponentDef,
4325 target: &CompileTarget,
4326 _data: &ComponentDataModel,
4327 _indent: usize,
4328) -> ResultStringCompileError {
4329 match target {
4330 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4331 "let mut children = Vec::new();\nfor i in 0..data.count {\n children.push(Dom::create_div());\n}\nDom::create_div().with_children(children)"
4332 )),
4333 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4334 "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n AzDom_addChild(&container, AzDom_createDiv());\n}"
4335 )),
4336 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4337 "auto container = Dom::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n container.add_child(Dom::create_div());\n}"
4338 )),
4339 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4340 "container = Dom.create_div()\nfor i in range(data.count):\n container = container.with_child(Dom.create_div())"
4341 )),
4342 }
4343}
4344
4345fn builtin_map_component() -> ComponentDef {
4348 ComponentDef {
4349 id: ComponentId::builtin("map"),
4350 display_name: AzString::from_const_str("Map"),
4351 description: AzString::from_const_str(
4352 "Map data to DOM: applies a template to each item in a collection.",
4353 ),
4354 css: AzString::from_const_str(""),
4355 source: ComponentSource::Builtin,
4356 data_model: ComponentDataModel {
4357 name: AzString::from_const_str("MapData"),
4358 description: AzString::from_const_str("Data for map rendering"),
4359 fields: alloc::vec![data_field(
4360 "data_json",
4361 ComponentFieldType::String,
4362 Some(ComponentDefaultValue::String(AzString::from_const_str(
4363 "[]"
4364 ))),
4365 "JSON array of items to map over"
4366 ),]
4367 .into(),
4368 },
4369 render_fn: builtin_map_render_fn,
4370 compile_fn: builtin_map_compile_fn,
4371 render_fn_source: None.into(),
4372 compile_fn_source: None.into(),
4373 }
4374}
4375
4376fn builtin_map_render_fn(
4377 _comp: &ComponentDef,
4378 data_model: &ComponentDataModel,
4379 _component_map: &ComponentMap,
4380) -> ResultStyledDomRenderDomError {
4381 let data_str = data_model
4383 .fields
4384 .iter()
4385 .find(|f| f.name.as_str() == "data_json")
4386 .and_then(|f| match &f.default_value {
4387 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
4388 Some(s.as_str().to_string())
4389 }
4390 _ => None,
4391 })
4392 .unwrap_or_else(|| "[]".to_string());
4393
4394 let label = alloc::format!("map: data_json={data_str}");
4395 let mut dom = Dom::create_node(NodeType::Div).with_children(
4396 alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
4397 label
4398 )]
4399 .into(),
4400 );
4401 let css = Css::empty();
4402 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
4403}
4404
4405fn builtin_map_compile_fn(
4406 _comp: &ComponentDef,
4407 target: &CompileTarget,
4408 _data: &ComponentDataModel,
4409 _indent: usize,
4410) -> ResultStringCompileError {
4411 match target {
4412 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
4413 "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)"
4414 )),
4415 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
4416 "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
4417 )),
4418 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
4419 "// Parse data.data_json and map each item\nauto container = Dom::create_div();\n// TODO: iterate parsed JSON array"
4420 )),
4421 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
4422 "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n container = container.with_child(Dom.create_div())"
4423 )),
4424 }
4425}
4426
4427#[allow(clippy::too_many_lines)] #[must_use]
4437pub extern "C" fn register_builtin_components() -> ComponentLibrary {
4438 ComponentLibrary {
4439 name: AzString::from_const_str("builtin"),
4440 version: AzString::from_const_str("1.0.0"),
4441 description: AzString::from_const_str("Built-in HTML elements"),
4442 exportable: false,
4443 modifiable: false,
4444 data_models: Vec::new().into(),
4445 enum_models: Vec::new().into(),
4446 components: alloc::vec![
4447 builtin_component_def("html", "HTML", None, ""),
4449 builtin_component_def("head", "Head", None, ""),
4450 builtin_component_def("title", "Title", Some(""), ""),
4451 builtin_component_def("body", "Body", None, ""),
4452 builtin_component_def("div", "Div", None, ""),
4454 builtin_component_def("header", "Header", None, ""),
4455 builtin_component_def("footer", "Footer", None, ""),
4456 builtin_component_def("section", "Section", None, ""),
4457 builtin_component_def("article", "Article", None, ""),
4458 builtin_component_def("aside", "Aside", None, ""),
4459 builtin_component_def("nav", "Nav", None, ""),
4460 builtin_component_def("main", "Main", None, ""),
4461 builtin_component_def("figure", "Figure", None, ""),
4462 builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
4463 builtin_component_def("address", "Address", Some(""), ""),
4464 builtin_component_def("details", "Details", None, ""),
4465 builtin_component_def("summary", "Summary", Some("Details"), ""),
4466 builtin_component_def("dialog", "Dialog", None, ""),
4467 builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
4469 builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
4470 builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
4471 builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
4472 builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
4473 builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
4474 builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
4476 builtin_component_def("span", "Span", Some(""), ""),
4477 builtin_component_def("pre", "Preformatted", Some(""), ""),
4478 builtin_component_def("code", "Code", Some(""), ""),
4479 builtin_component_def("blockquote", "Blockquote", Some(""), ""),
4480 builtin_component_def("br", "Line Break", None, ""),
4481 builtin_component_def("hr", "Horizontal Rule", None, ""),
4482 builtin_component_def("pagebreak", "Page Break", None, ""),
4483 builtin_component_def("icon", "Icon", Some(""), ""),
4484 builtin_component_def("ul", "Unordered List", None, ""),
4486 builtin_component_def("ol", "Ordered List", None, ""),
4487 builtin_component_def("li", "List Item", Some("List item"), ""),
4488 builtin_component_def("dl", "Description List", None, ""),
4489 builtin_component_def("dt", "Description Term", Some(""), ""),
4490 builtin_component_def("dd", "Description Details", Some(""), ""),
4491 builtin_component_def("menu", "Menu", None, ""),
4492 builtin_component_def("menuitem", "Menu Item", Some(""), ""),
4493 builtin_component_def("dir", "Directory List", None, ""),
4494 builtin_component_def("table", "Table", None, ""),
4496 builtin_component_def("caption", "Table Caption", Some(""), ""),
4497 builtin_component_def("thead", "Table Head", None, ""),
4498 builtin_component_def("tbody", "Table Body", None, ""),
4499 builtin_component_def("tfoot", "Table Foot", None, ""),
4500 builtin_component_def("tr", "Table Row", None, ""),
4501 builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
4502 builtin_component_def("td", "Table Data Cell", Some(""), ""),
4503 builtin_component_def("colgroup", "Column Group", None, ""),
4504 builtin_component_def("col", "Column", None, ""),
4505 builtin_component_def("a", "Link", Some("Link text"), ""),
4507 builtin_component_def("strong", "Strong", Some(""), ""),
4508 builtin_component_def("em", "Emphasis", Some(""), ""),
4509 builtin_component_def("b", "Bold", Some(""), ""),
4510 builtin_component_def("i", "Italic", Some(""), ""),
4511 builtin_component_def("u", "Underline", Some(""), ""),
4512 builtin_component_def("s", "Strikethrough", Some(""), ""),
4513 builtin_component_def("small", "Small", Some(""), ""),
4514 builtin_component_def("mark", "Mark", Some(""), ""),
4515 builtin_component_def("del", "Deleted Text", Some(""), ""),
4516 builtin_component_def("ins", "Inserted Text", Some(""), ""),
4517 builtin_component_def("sub", "Subscript", Some(""), ""),
4518 builtin_component_def("sup", "Superscript", Some(""), ""),
4519 builtin_component_def("samp", "Sample Output", Some(""), ""),
4520 builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
4521 builtin_component_def("var", "Variable", Some(""), ""),
4522 builtin_component_def("cite", "Citation", Some(""), ""),
4523 builtin_component_def("dfn", "Definition", Some(""), ""),
4524 builtin_component_def("abbr", "Abbreviation", Some(""), ""),
4525 builtin_component_def("acronym", "Acronym", Some(""), ""),
4526 builtin_component_def("q", "Inline Quote", Some(""), ""),
4527 builtin_component_def("time", "Time", Some(""), ""),
4528 builtin_component_def("big", "Big", Some(""), ""),
4529 builtin_component_def("bdo", "BiDi Override", Some(""), ""),
4530 builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
4531 builtin_component_def("wbr", "Word Break Opportunity", None, ""),
4532 builtin_component_def("ruby", "Ruby Annotation", None, ""),
4533 builtin_component_def("rt", "Ruby Text", Some(""), ""),
4534 builtin_component_def("rtc", "Ruby Text Container", None, ""),
4535 builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
4536 builtin_component_def("data", "Data", Some(""), ""),
4537 builtin_component_def("form", "Form", None, ""),
4539 builtin_component_def("fieldset", "Field Set", None, ""),
4540 builtin_component_def("legend", "Legend", Some("Legend"), ""),
4541 builtin_component_def("label", "Label", Some("Label"), ""),
4542 builtin_component_def("input", "Input", None, ""),
4543 builtin_component_def("button", "Button", Some("Button text"), ""),
4544 builtin_component_def("select", "Select", None, ""),
4545 builtin_component_def("optgroup", "Option Group", None, ""),
4546 builtin_component_def("option", "Option", Some(""), ""),
4547 builtin_component_def("textarea", "Text Area", Some(""), ""),
4548 builtin_component_def("output", "Output", Some(""), ""),
4549 builtin_component_def("progress", "Progress", None, ""),
4550 builtin_component_def("meter", "Meter", None, ""),
4551 builtin_component_def("datalist", "Data List", None, ""),
4552 builtin_component_def("canvas", "Canvas", None, ""),
4554 builtin_component_def("object", "Object", None, ""),
4555 builtin_component_def("param", "Parameter", None, ""),
4556 builtin_component_def("embed", "Embed", None, ""),
4557 builtin_component_def("audio", "Audio", None, ""),
4558 builtin_component_def("video", "Video", None, ""),
4559 builtin_component_def("source", "Source", None, ""),
4560 builtin_component_def("track", "Track", None, ""),
4561 builtin_component_def("map", "Image Map", None, ""),
4562 builtin_component_def("area", "Map Area", None, ""),
4563 builtin_component_def("svg", "SVG", None, ""),
4564 builtin_component_def("meta", "Meta", None, ""),
4566 builtin_component_def("link", "Link (Resource)", None, ""),
4567 builtin_component_def("script", "Script", Some(""), ""),
4568 builtin_component_def("style", "Style", Some(""), ""),
4569 builtin_component_def("base", "Base URL", None, ""),
4570 builtin_if_component(),
4572 builtin_for_component(),
4573 builtin_map_component(),
4574 ]
4575 .into(),
4576 }
4577}
4578
4579#[derive(Debug, Default)]
4586pub struct DomXml {
4587 pub parsed_dom: StyledDom,
4588}
4589
4590impl DomXml {
4591 #[cfg(test)]
4609 pub fn assert_eq(self, other: StyledDom) {
4610 let mut body = Dom::create_body();
4611 let mut fixed = StyledDom::create(&mut body, Css::empty());
4612 fixed.append_child(other);
4613 assert!(
4614 !(self.parsed_dom != fixed),
4615 "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
4616 ----------\r\n{}\r\n",
4617 self.parsed_dom.get_html_string("", "", true),
4618 fixed.get_html_string("", "", true)
4619 );
4620 }
4621
4622 #[must_use]
4623 pub fn into_styled_dom(self) -> StyledDom {
4624 self.into()
4625 }
4626}
4627
4628impl From<DomXml> for StyledDom {
4629 fn from(val: DomXml) -> Self {
4630 val.parsed_dom
4631 }
4632}
4633
4634#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4636#[repr(C, u8)]
4637pub enum XmlNodeChild {
4638 Text(AzString),
4640 Element(XmlNode),
4642}
4643
4644impl_option!(
4645 XmlNodeChild,
4646 OptionXmlNodeChild,
4647 copy = false,
4648 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4649);
4650
4651impl XmlNodeChild {
4652 #[must_use]
4654 pub fn as_text(&self) -> Option<&str> {
4655 match self {
4656 Self::Text(s) => Some(s.as_str()),
4657 Self::Element(_) => None,
4658 }
4659 }
4660
4661 #[must_use]
4663 pub const fn as_element(&self) -> Option<&XmlNode> {
4664 match self {
4665 Self::Text(_) => None,
4666 Self::Element(node) => Some(node),
4667 }
4668 }
4669
4670 pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
4672 match self {
4673 Self::Text(_) => None,
4674 Self::Element(node) => Some(node),
4675 }
4676 }
4677}
4678
4679impl_vec!(
4680 XmlNodeChild,
4681 XmlNodeChildVec,
4682 XmlNodeChildVecDestructor,
4683 XmlNodeChildVecDestructorType,
4684 XmlNodeChildVecSlice,
4685 OptionXmlNodeChild
4686);
4687impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
4688impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
4689impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
4690impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
4691impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
4692impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
4693impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
4694impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
4695
4696#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4698#[repr(C)]
4699pub struct XmlNode {
4700 pub node_type: XmlTagName,
4702 pub attributes: XmlAttributeMap,
4704 pub children: XmlNodeChildVec,
4706}
4707
4708impl_option!(
4709 XmlNode,
4710 OptionXmlNode,
4711 copy = false,
4712 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
4713);
4714
4715impl XmlNode {
4716 pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
4717 Self {
4718 node_type: node_type.into(),
4719 ..Default::default()
4720 }
4721 }
4722 #[must_use]
4723 pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
4724 Self {
4725 children: v.into(),
4726 ..self
4727 }
4728 }
4729
4730 #[must_use]
4732 pub fn get_text_content(&self) -> String {
4733 self.children
4734 .as_ref()
4735 .iter()
4736 .filter_map(|child| child.as_text())
4737 .collect::<Vec<_>>()
4738 .join("")
4739 }
4740
4741 #[must_use]
4743 pub fn has_only_text_children(&self) -> bool {
4744 self.children
4745 .as_ref()
4746 .iter()
4747 .all(|child| matches!(child, XmlNodeChild::Text(_)))
4748 }
4749}
4750
4751impl_vec!(
4752 XmlNode,
4753 XmlNodeVec,
4754 XmlNodeVecDestructor,
4755 XmlNodeVecDestructorType,
4756 XmlNodeVecSlice,
4757 OptionXmlNode
4758);
4759impl_vec_mut!(XmlNode, XmlNodeVec);
4760impl_vec_debug!(XmlNode, XmlNodeVec);
4761impl_vec_partialeq!(XmlNode, XmlNodeVec);
4762impl_vec_eq!(XmlNode, XmlNodeVec);
4763impl_vec_partialord!(XmlNode, XmlNodeVec);
4764impl_vec_ord!(XmlNode, XmlNodeVec);
4765impl_vec_hash!(XmlNode, XmlNodeVec);
4766impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
4767
4768#[derive(Debug, Clone, PartialEq)]
4769#[repr(C, u8)]
4770pub enum DomXmlParseError {
4771 NoHtmlNode,
4773 MultipleHtmlRootNodes,
4775 NoBodyInHtml,
4777 MultipleBodyNodes,
4779 Xml(XmlError),
4784 MalformedHierarchy(MalformedHierarchyError),
4786 RenderDom(RenderDomError),
4789 Component(ComponentParseError),
4791 Css(CssParseErrorOwned),
4793}
4794
4795impl From<XmlError> for DomXmlParseError {
4796 fn from(e: XmlError) -> Self {
4797 Self::Xml(e)
4798 }
4799}
4800
4801impl From<ComponentParseError> for DomXmlParseError {
4802 fn from(e: ComponentParseError) -> Self {
4803 Self::Component(e)
4804 }
4805}
4806
4807impl From<RenderDomError> for DomXmlParseError {
4808 fn from(e: RenderDomError) -> Self {
4809 Self::RenderDom(e)
4810 }
4811}
4812
4813impl From<CssParseErrorOwned> for DomXmlParseError {
4814 fn from(e: CssParseErrorOwned) -> Self {
4815 Self::Css(e)
4816 }
4817}
4818
4819#[derive(Debug, Clone, PartialEq)]
4822#[repr(C, u8)]
4823pub enum CompileError {
4824 Dom(RenderDomError),
4825 Xml(DomXmlParseError),
4826 Css(CssParseErrorOwned),
4827}
4828
4829impl From<ComponentError> for CompileError {
4830 fn from(e: ComponentError) -> Self {
4831 Self::Dom(RenderDomError::Component(e))
4832 }
4833}
4834
4835impl From<CssParseErrorOwned> for CompileError {
4836 fn from(e: CssParseErrorOwned) -> Self {
4837 Self::Css(e)
4838 }
4839}
4840
4841impl fmt::Display for CompileError {
4842 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4843 use self::CompileError::{Css, Dom, Xml};
4844 match self {
4845 Dom(d) => write!(f, "{d}"),
4846 Xml(s) => write!(f, "{s}"),
4847 Css(s) => write!(f, "{}", s.to_shared()),
4848 }
4849 }
4850}
4851
4852impl From<RenderDomError> for CompileError {
4853 fn from(e: RenderDomError) -> Self {
4854 Self::Dom(e)
4855 }
4856}
4857
4858impl From<DomXmlParseError> for CompileError {
4859 fn from(e: DomXmlParseError) -> Self {
4860 Self::Xml(e)
4861 }
4862}
4863
4864#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4866#[repr(C)]
4867pub struct UselessFunctionArgumentError {
4868 pub component_name: AzString,
4869 pub argument_name: AzString,
4870 pub valid_args: StringVec,
4871}
4872
4873#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4874#[repr(C, u8)]
4875pub enum ComponentError {
4876 UselessFunctionArgument(UselessFunctionArgumentError),
4879 UnknownComponent(AzString),
4884}
4885
4886#[derive(Debug, Clone, PartialEq)]
4887#[repr(C, u8)]
4888pub enum RenderDomError {
4889 Component(ComponentError),
4890 CssError(CssParseErrorOwned),
4892}
4893
4894impl From<ComponentError> for RenderDomError {
4895 fn from(e: ComponentError) -> Self {
4896 Self::Component(e)
4897 }
4898}
4899
4900impl From<CssParseErrorOwned> for RenderDomError {
4901 fn from(e: CssParseErrorOwned) -> Self {
4902 Self::CssError(e)
4903 }
4904}
4905
4906#[derive(Debug, Clone, PartialEq, Eq)]
4908#[repr(C)]
4909pub struct MissingTypeError {
4910 pub arg_pos: usize,
4911 pub arg_name: AzString,
4912}
4913
4914#[derive(Debug, Clone, PartialEq, Eq)]
4916#[repr(C)]
4917pub struct WhiteSpaceInComponentNameError {
4918 pub arg_pos: usize,
4919 pub arg_name: AzString,
4920}
4921
4922#[derive(Debug, Clone, PartialEq, Eq)]
4924#[repr(C)]
4925pub struct WhiteSpaceInComponentTypeError {
4926 pub arg_pos: usize,
4927 pub arg_name: AzString,
4928 pub arg_type: AzString,
4929}
4930
4931#[derive(Debug, Clone, PartialEq)]
4932#[repr(C, u8)]
4933pub enum ComponentParseError {
4934 NotAComponent,
4936 UnnamedComponent,
4938 MissingName(usize),
4940 MissingType(MissingTypeError),
4943 WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
4946 WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
4949 CssError(CssParseErrorOwned),
4951}
4952
4953impl fmt::Display for DomXmlParseError {
4954 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4955 use self::DomXmlParseError::{
4956 Component, Css, MalformedHierarchy, MultipleBodyNodes, MultipleHtmlRootNodes,
4957 NoBodyInHtml, NoHtmlNode, RenderDom, Xml,
4958 };
4959 match self {
4960 NoHtmlNode => write!(
4961 f,
4962 "No <html> node found as the root of the file - empty file?"
4963 ),
4964 MultipleHtmlRootNodes => write!(
4965 f,
4966 "Multiple <html> nodes found as the root of the file - only one root node allowed"
4967 ),
4968 NoBodyInHtml => write!(
4969 f,
4970 "No <body> node found as a direct child of an <html> node - malformed DOM \
4971 hierarchy?"
4972 ),
4973 MultipleBodyNodes => write!(
4974 f,
4975 "Multiple <body> nodes present, only one <body> node is allowed"
4976 ),
4977 Xml(e) => write!(f, "Error parsing XML: {e}"),
4978 MalformedHierarchy(e) => write!(
4979 f,
4980 "Invalid </{}> tag: expected </{}>",
4981 e.got.as_str(),
4982 e.expected.as_str()
4983 ),
4984 RenderDom(e) => write!(f, "Error rendering DOM: {e}"),
4985 Component(c) => write!(f, "Error parsing component in <head> node:\r\n{c}"),
4986 Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
4987 }
4988 }
4989}
4990
4991impl fmt::Display for ComponentParseError {
4992 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4993 use self::ComponentParseError::{
4994 CssError, MissingName, MissingType, NotAComponent, UnnamedComponent,
4995 WhiteSpaceInComponentName, WhiteSpaceInComponentType,
4996 };
4997 match self {
4998 NotAComponent => write!(f, "Expected <component/> node, found no such node"),
4999 UnnamedComponent => write!(
5000 f,
5001 "Found <component/> tag with out a \"name\" attribute, component must have a name"
5002 ),
5003 MissingName(arg_pos) => write!(
5004 f,
5005 "Argument at position {arg_pos} is either empty or has no name"
5006 ),
5007 MissingType(e) => write!(
5008 f,
5009 "Argument \"{}\" at position {} doesn't have a `: type`",
5010 e.arg_name, e.arg_pos
5011 ),
5012 WhiteSpaceInComponentName(e) => {
5013 write!(
5014 f,
5015 "Missing `:` between the name and the type in argument {} (around \"{}\")",
5016 e.arg_pos, e.arg_name
5017 )
5018 }
5019 WhiteSpaceInComponentType(e) => {
5020 write!(
5021 f,
5022 "Missing `,` between two arguments (in argument {}, position {}, around \
5023 \"{}\")",
5024 e.arg_name, e.arg_pos, e.arg_type
5025 )
5026 }
5027 CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
5028 }
5029 }
5030}
5031
5032impl fmt::Display for ComponentError {
5033 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5034 use self::ComponentError::{UnknownComponent, UselessFunctionArgument};
5035 match self {
5036 UselessFunctionArgument(e) => {
5037 write!(
5038 f,
5039 "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
5040 e.component_name, e.argument_name, e.valid_args
5041 )
5042 }
5043 UnknownComponent(name) => write!(f, "Unknown component: \"{name}\""),
5044 }
5045 }
5046}
5047
5048impl fmt::Display for RenderDomError {
5049 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5050 use self::RenderDomError::{Component, CssError};
5051 match self {
5052 Component(c) => write!(f, "{c}"),
5053 CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
5054 }
5055 }
5056}
5057
5058#[allow(clippy::result_large_err)] fn head_style_text(html_node: &XmlNode) -> String {
5072 let Some(head) = find_node_by_type(html_node.children.as_ref(), "head") else {
5073 return String::new();
5074 };
5075 let mut out = String::new();
5076 for child in head.children.as_ref() {
5077 let XmlNodeChild::Element(element) = child else {
5078 continue;
5079 };
5080 if !element.node_type.as_str().eq_ignore_ascii_case("style") {
5081 continue;
5082 }
5083 let text = element.get_text_content();
5084 if !text.is_empty() {
5085 if !out.is_empty() {
5086 out.push('\n');
5087 }
5088 out.push_str(&text);
5089 }
5090 }
5091 out
5092}
5093
5094pub fn get_html_node(
5095 root_nodes: &[XmlNodeChild],
5096) -> Result<alloc::borrow::Cow<'_, XmlNode>, DomXmlParseError> {
5097 use alloc::borrow::Cow;
5098
5099 let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
5100 if let XmlNodeChild::Element(node) = child {
5101 if node.node_type.as_str().eq_ignore_ascii_case("html") {
5105 Some(node)
5106 } else {
5107 None
5108 }
5109 } else {
5110 None
5111 }
5112 });
5113
5114 if let Some(html_node) = html_node_iterator.next() {
5115 return if html_node_iterator.next().is_some() {
5116 Err(DomXmlParseError::MultipleHtmlRootNodes)
5117 } else {
5118 Ok(Cow::Borrowed(html_node))
5119 };
5120 }
5121
5122 let has_structural_root = root_nodes.iter().any(|child| match child {
5135 XmlNodeChild::Element(node) => {
5136 let tag = node.node_type.as_str();
5137 tag.eq_ignore_ascii_case("body") || tag.eq_ignore_ascii_case("head")
5138 }
5139 XmlNodeChild::Text(_) => false,
5140 });
5141 if has_structural_root {
5142 return Ok(Cow::Owned(
5143 XmlNode::create("html").with_children(root_nodes.to_vec()),
5144 ));
5145 }
5146
5147 let (head_children, body_children): (Vec<_>, Vec<_>) =
5152 root_nodes.iter().cloned().partition(|child| match child {
5153 XmlNodeChild::Element(node) => {
5154 let tag = node.node_type.as_str();
5155 tag.eq_ignore_ascii_case("style")
5156 || tag.eq_ignore_ascii_case("link")
5157 || tag.eq_ignore_ascii_case("meta")
5158 || tag.eq_ignore_ascii_case("title")
5159 || tag.eq_ignore_ascii_case("base")
5160 }
5161 XmlNodeChild::Text(_) => false,
5162 });
5163
5164 let mut html_children = Vec::new();
5165 if !head_children.is_empty() {
5166 html_children.push(XmlNodeChild::Element(
5167 XmlNode::create("head").with_children(head_children),
5168 ));
5169 }
5170 html_children.push(XmlNodeChild::Element(
5171 XmlNode::create("body").with_children(body_children),
5172 ));
5173 Ok(Cow::Owned(XmlNode::create("html").with_children(html_children)))
5174}
5175
5176#[allow(clippy::result_large_err)] pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
5183 fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
5184 if depth > MAX_XML_NESTING_DEPTH {
5187 return None;
5188 }
5189 for child in nodes {
5190 if let XmlNodeChild::Element(node) = child {
5191 if node.node_type.as_str().eq_ignore_ascii_case("body") {
5193 return Some(node);
5194 }
5195 if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
5197 return Some(found);
5198 }
5199 }
5200 }
5201 None
5202 }
5203
5204 let direct_body = root_nodes.iter().find_map(|child| {
5206 if let XmlNodeChild::Element(node) = child {
5207 if node.node_type.as_str().eq_ignore_ascii_case("body") {
5209 Some(node)
5210 } else {
5211 None
5212 }
5213 } else {
5214 None
5215 }
5216 });
5217
5218 if let Some(body) = direct_body {
5219 return Ok(body);
5220 }
5221
5222 find_body_recursive(root_nodes, 0).ok_or(DomXmlParseError::NoBodyInHtml)
5225}
5226
5227fn find_node_by_type<'a>(root_nodes: &'a [XmlNodeChild], node_type: &str) -> Option<&'a XmlNode> {
5231 for child in root_nodes {
5233 if let XmlNodeChild::Element(node) = child {
5234 if node.node_type.as_str().eq_ignore_ascii_case(node_type) {
5236 return Some(node);
5237 }
5238 }
5239 }
5240
5241 for child in root_nodes {
5243 if let XmlNodeChild::Element(node) = child {
5244 if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
5245 return Some(found);
5246 }
5247 }
5248 }
5249
5250 None
5251}
5252
5253#[must_use]
5254pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
5255 node.attributes
5256 .iter()
5257 .find(|n| normalize_casing(n.key.as_str()).as_str() == attribute)
5258 .map(|s| &s.value)
5259}
5260
5261#[must_use]
5263pub fn normalize_casing(input: &str) -> String {
5264 let mut words: Vec<String> = Vec::new();
5265 let mut cur_str = Vec::new();
5266
5267 for ch in input.chars() {
5268 if ch.is_uppercase() || ch == '_' || ch == '-' {
5269 if !cur_str.is_empty() {
5270 words.push(cur_str.iter().collect());
5271 cur_str.clear();
5272 }
5273 if ch.is_uppercase() {
5274 cur_str.extend(ch.to_lowercase());
5275 }
5276 } else {
5277 cur_str.extend(ch.to_lowercase());
5278 }
5279 }
5280
5281 if !cur_str.is_empty() {
5282 words.push(cur_str.iter().collect());
5283 cur_str.clear();
5284 }
5285
5286 words.join("_")
5287}
5288
5289#[allow(trivial_casts)]
5292pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
5293 let mut hierarchy = hierarchy.to_vec();
5294 hierarchy.reverse();
5295 let Some(item) = hierarchy.pop() else {
5296 return Some(root_node);
5297 };
5298 let child = root_node.children.as_mut().get_mut(item)?;
5299 match child {
5300 XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
5301 XmlNodeChild::Text(_) => None, }
5303}
5304
5305fn get_item_internal<'a>(
5306 hierarchy: &mut Vec<usize>,
5307 root_node: &'a mut XmlNode,
5308) -> Option<&'a mut XmlNode> {
5309 if hierarchy.is_empty() {
5310 return Some(root_node);
5311 }
5312 let Some(cur_item) = hierarchy.pop() else {
5313 return Some(root_node);
5314 };
5315 let child = root_node.children.as_mut().get_mut(cur_item)?;
5316 match child {
5317 XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
5318 XmlNodeChild::Text(_) => None, }
5320}
5321
5322#[allow(clippy::result_large_err)] pub fn str_to_dom<'a>(
5329 root_nodes: &'a [XmlNodeChild],
5330 component_map: &'a ComponentMap,
5331 max_width: Option<f32>,
5332) -> Result<StyledDom, DomXmlParseError> {
5333 str_to_dom_fast(root_nodes, component_map, max_width)
5335}
5336
5337#[allow(clippy::result_large_err)] fn str_to_dom_fast<'a>(
5343 root_nodes: &'a [XmlNodeChild],
5344 component_map: &'a ComponentMap,
5345 max_width: Option<f32>,
5346) -> Result<StyledDom, DomXmlParseError> {
5347 let html_node = get_html_node(root_nodes)?;
5348 let body_node = get_body_node(html_node.children.as_ref())?;
5349
5350 let style_text = head_style_text(&html_node);
5351 let global_style = if style_text.is_empty() {
5352 None
5353 } else {
5354 Some(Css::from_string(style_text.into()))
5355 };
5356
5357 render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
5358 .map_err(Into::into)
5359}
5360
5361#[allow(clippy::result_large_err)] pub fn str_to_dom_unstyled<'a>(
5374 root_nodes: &'a [XmlNodeChild],
5375 component_map: &'a ComponentMap,
5376) -> Result<Dom, DomXmlParseError> {
5377 let html_node = get_html_node(root_nodes)?;
5378 let body_node = get_body_node(html_node.children.as_ref())?;
5379
5380 let style_text = head_style_text(&html_node);
5381 let global_style = if style_text.is_empty() {
5382 None
5383 } else {
5384 Some(Css::from_string(style_text.into()))
5385 };
5386
5387 let body_dom =
5389 xml_node_to_dom_fast(body_node, component_map, false, 0).map_err(DomXmlParseError::from)?;
5390
5391 let root_node_type = body_dom.root.node_type.clone();
5393
5394 let mut full_dom = match root_node_type {
5395 NodeType::Html => body_dom,
5396 NodeType::Body => Dom::create_html().with_child(body_dom),
5397 _ => {
5398 let body_wrapper = Dom::create_body().with_child(body_dom);
5399 Dom::create_html().with_child(body_wrapper)
5400 }
5401 };
5402
5403 if let Some(css) = global_style {
5405 full_dom.css = alloc::vec![css].into();
5406 }
5407
5408 Ok(full_dom)
5409}
5410
5411#[allow(clippy::result_large_err)] pub fn str_to_rust_code<'a>(
5418 root_nodes: &'a [XmlNodeChild],
5419 imports: &str,
5420 component_map: &'a ComponentMap,
5421) -> Result<String, CompileError> {
5422 let html_node = get_html_node(root_nodes)?;
5423 let body_node = get_body_node(html_node.children.as_ref())?;
5424 let style_text = head_style_text(&html_node);
5425 let mut global_style = if style_text.is_empty() {
5426 Css::empty()
5427 } else {
5428 azul_css::parser2::new_from_str(&style_text).0
5429 };
5430
5431 global_style.sort_by_specificity();
5432
5433 let mut css_blocks = BTreeMap::new();
5434 let mut extra_blocks = VecContents::default();
5435 let app_source = compile_body_node_to_rust_code(
5436 body_node,
5437 component_map,
5438 &mut extra_blocks,
5439 &mut css_blocks,
5440 &global_style,
5441 CssMatcher {
5442 path: Vec::new(),
5443 indices_in_parent: vec![0],
5444 children_length: vec![body_node.children.as_ref().len()],
5445 },
5446 )?;
5447
5448 let app_source = app_source
5449 .lines()
5450 .map(|l| format!(" {l}"))
5451 .collect::<Vec<String>>()
5452 .join("\r\n");
5453
5454 let _ = (&css_blocks, &extra_blocks);
5459
5460 let main_func = "
5461
5462use azul::{
5463 app::{App, AppConfig},
5464 dom::Dom,
5465 callbacks::{RefAny, LayoutCallbackInfo},
5466 window::WindowCreateOptions,
5467};
5468
5469struct Data { }
5470
5471extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
5472 crate::ui::render()
5473}
5474
5475fn main() {
5476 let config = AppConfig::create();
5477 let app = App::create(RefAny::new(Data { }), config);
5478 let window = WindowCreateOptions::create(render);
5479 app.run(window);
5480}";
5481
5482 let ui_module = format!(
5483 "#[allow(unused_imports)]\r\npub mod ui {{
5484
5485 use azul::prelude::*;
5486 use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
5487 use azul::str::String as AzString;
5488
5489 pub fn render() -> Dom {{\r\n{app_source}\r\n }}\r\n}}"
5490 );
5491 let source_code = format!(
5492 "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
5493 code\r\n{}\r\n{}\r\n\r\n{}{}",
5494 imports,
5495 compile_components(Vec::new()), ui_module,
5497 main_func,
5498 );
5499
5500 Ok(source_code)
5501}
5502
5503#[allow(clippy::needless_pass_by_value)] fn compile_components(
5506 components: Vec<(
5507 ComponentName,
5508 CompiledComponent,
5509 ComponentArguments,
5510 BTreeMap<String, String>,
5511 )>,
5512) -> String {
5513 let cs = components
5514 .iter()
5515 .map(|(name, function_body, function_args, css_blocks)| {
5516 let name = &normalize_casing(name);
5517 let f = compile_component(name, function_args, function_body)
5518 .lines()
5519 .map(|l| format!(" {l}"))
5520 .collect::<Vec<String>>()
5521 .join("\r\n");
5522
5523 format!(
5526 "#[allow(unused_imports)]\r\npub mod {name} {{\r\n use azul::dom::Dom;\r\n use \
5527 azul::str::String as AzString;\r\n{f}\r\n}}"
5528 )
5529 })
5530 .collect::<Vec<String>>()
5531 .join("\r\n\r\n");
5532
5533 let cs = cs
5534 .lines()
5535 .map(|l| format!(" {l}"))
5536 .collect::<Vec<String>>()
5537 .join("\r\n");
5538
5539 if cs.is_empty() {
5540 cs
5541 } else {
5542 format!("pub mod components {{\r\n{cs}\r\n}}")
5543 }
5544}
5545
5546fn format_component_args(component_args: &ComponentArgumentVec) -> String {
5547 let mut args = component_args
5548 .iter()
5549 .map(|a| format!("{}: {}", a.name, a.arg_type))
5550 .collect::<Vec<String>>();
5551
5552 args.sort_by(|a, b| b.cmp(a));
5553
5554 args.join(", ")
5555}
5556
5557#[must_use]
5558pub fn compile_component(
5559 component_name: &str,
5560 component_args: &ComponentArguments,
5561 component_function_body: &str,
5562) -> String {
5563 let component_name = &normalize_casing(component_name);
5564 let function_args = format_component_args(&component_args.args);
5565 let component_function_body = component_function_body
5566 .lines()
5567 .map(|l| format!(" {l}"))
5568 .collect::<Vec<String>>()
5569 .join("\r\n");
5570 let should_inline = component_function_body.lines().count() == 1;
5571 format!(
5572 "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
5573 if should_inline { "#[inline]\r\n" } else { "" },
5574 if component_args.accepts_text {
5576 "text: AzString"
5577 } else {
5578 ""
5579 },
5580 if function_args.is_empty() || !component_args.accepts_text {
5581 ""
5582 } else {
5583 ", "
5584 },
5585 function_args,
5586 component_function_body,
5587 )
5588}
5589
5590fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
5596 attr?.as_str().trim().parse::<f32>().ok()
5597}
5598
5599fn parse_svg_length(attr: Option<&AzString>) -> Option<f32> {
5607 let raw = attr?.as_str().trim();
5608 let number = raw.strip_suffix("px").unwrap_or(raw).trim();
5609 number.parse::<f32>().ok()
5610}
5611
5612fn parse_svg_view_box(value: &str) -> Option<(f32, f32, f32, f32)> {
5618 let nums: Vec<f32> = value
5619 .split(|c: char| c == ',' || c.is_ascii_whitespace())
5620 .filter(|s| !s.is_empty())
5621 .map(str::parse::<f32>)
5622 .collect::<Result<Vec<_>, _>>()
5623 .ok()?;
5624 match nums[..] {
5625 [min_x, min_y, width, height] if width > 0.0 && height > 0.0 => {
5626 Some((min_x, min_y, width, height))
5627 }
5628 _ => None,
5629 }
5630}
5631
5632fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
5634 let nums: Vec<f32> = pts
5635 .split(|c: char| c == ',' || c.is_ascii_whitespace())
5636 .filter(|s| !s.is_empty())
5637 .filter_map(|s| s.parse::<f32>().ok())
5638 .collect();
5639 if nums.len() < 4 || !nums.len().is_multiple_of(2) {
5640 return None;
5641 }
5642 let mut elements = Vec::new();
5643 let points: Vec<azul_css::props::basic::SvgPoint> = nums
5644 .chunks_exact(2)
5645 .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
5646 .collect();
5647 for w in points.windows(2) {
5648 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5649 w[0], w[1],
5650 )));
5651 }
5652 if close && points.len() >= 2 {
5653 let first = points[0];
5654 let last = *points.last().unwrap();
5655 if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
5656 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5657 last, first,
5658 )));
5659 }
5660 }
5661 Some(crate::svg::SvgMultiPolygon {
5662 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
5663 items: crate::svg::SvgPathElementVec::from_vec(elements),
5664 }]),
5665 })
5666}
5667
5668#[allow(clippy::too_many_lines)] fn apply_xml_node_attributes(
5679 node: &mut crate::dom::NodeData,
5680 xml_node: &XmlNode,
5681 component_name: &str,
5682 inside_svg: bool,
5683) {
5684 use crate::dom::{IdOrClass, NodeType, TabIndex};
5685
5686 if component_name == "img" {
5692 if let Some(src) = xml_node.attributes.get_key("src") {
5693 let width = xml_node
5694 .attributes
5695 .get_key("width")
5696 .and_then(|w| {
5697 w.as_str()
5698 .trim()
5699 .trim_end_matches("px")
5700 .trim()
5701 .parse::<usize>()
5702 .ok()
5703 })
5704 .unwrap_or(0);
5705 let height = xml_node
5706 .attributes
5707 .get_key("height")
5708 .and_then(|h| {
5709 h.as_str()
5710 .trim()
5711 .trim_end_matches("px")
5712 .trim()
5713 .parse::<usize>()
5714 .ok()
5715 })
5716 .unwrap_or(0);
5717 let image_ref = crate::resources::ImageRef::null_image(
5718 width,
5719 height,
5720 crate::resources::RawImageFormat::RGBA8,
5721 src.as_str().as_bytes().to_vec(),
5722 );
5723 node.set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
5724 }
5725 }
5726
5727 let mut ids_and_classes = Vec::new();
5729 if let Some(id_str) = xml_node.attributes.get_key("id") {
5730 for id in id_str.split_whitespace() {
5731 ids_and_classes.push(IdOrClass::Id(id.into()));
5732 }
5733 }
5734 if let Some(class_str) = xml_node.attributes.get_key("class") {
5735 for class in class_str.split_whitespace() {
5736 ids_and_classes.push(IdOrClass::Class(class.into()));
5737 }
5738 }
5739 if !ids_and_classes.is_empty() {
5740 node.set_ids_and_classes(ids_and_classes.into());
5741 }
5742
5743 if let Some(focusable) = xml_node
5745 .attributes
5746 .get_key("focusable")
5747 .and_then(|f| parse_bool(f.as_str()))
5748 {
5749 if focusable {
5750 node.set_tab_index(TabIndex::Auto);
5751 } else {
5752 node.set_tab_index(TabIndex::NoKeyboardFocus);
5753 }
5754 }
5755
5756 if let Some(tab_index) = xml_node
5758 .attributes
5759 .get_key("tabindex")
5760 .and_then(|val| val.parse::<isize>().ok())
5761 {
5762 match tab_index {
5763 0 => node.set_tab_index(TabIndex::Auto),
5764 i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(
5765 u32::try_from(i).unwrap_or(u32::MAX),
5766 )),
5767 _ => node.set_tab_index(TabIndex::NoKeyboardFocus),
5768 }
5769 }
5770
5771 apply_cell_span_attributes(node, xml_node);
5773
5774 let dir_prop = xml_node.attributes.get_key("dir").and_then(|d| {
5778 let v = d.as_str().trim();
5779 if v.eq_ignore_ascii_case("rtl") {
5780 Some(azul_css::props::style::StyleDirection::Rtl)
5781 } else if v.eq_ignore_ascii_case("ltr") {
5782 Some(azul_css::props::style::StyleDirection::Ltr)
5783 } else {
5784 None
5785 }
5786 });
5787
5788 let mut intrinsic_props: Vec<azul_css::dynamic_selector::CssPropertyWithConditions> =
5805 Vec::new();
5806 if component_name == "svg" {
5807 let view_box = xml_node
5808 .attributes
5809 .get_key("viewBox")
5810 .or_else(|| xml_node.attributes.get_key("viewbox"))
5811 .and_then(|v| parse_svg_view_box(v.as_str()));
5812 if let Some((min_x, min_y, width, height)) = view_box {
5813 node.set_svg_data(crate::dom::SvgNodeData::ViewBox {
5814 min_x,
5815 min_y,
5816 width,
5817 height,
5818 });
5819 }
5820 let stated = |key: &str| parse_svg_length(xml_node.attributes.get_key(key));
5821 let usable = |v: f32| v.is_finite() && v > 0.0;
5822 if let Some(w) = stated("width")
5823 .or(view_box.map(|(_, _, w, _)| w))
5824 .filter(|w| usable(*w))
5825 {
5826 intrinsic_props.push(
5827 azul_css::dynamic_selector::CssPropertyWithConditions::simple(
5828 azul_css::props::property::CssProperty::width(
5829 azul_css::props::layout::LayoutWidth::px(w),
5830 ),
5831 ),
5832 );
5833 }
5834 if let Some(h) = stated("height")
5835 .or(view_box.map(|(_, _, _, h)| h))
5836 .filter(|h| usable(*h))
5837 {
5838 intrinsic_props.push(
5839 azul_css::dynamic_selector::CssPropertyWithConditions::simple(
5840 azul_css::props::property::CssProperty::height(
5841 azul_css::props::layout::LayoutHeight::px(h),
5842 ),
5843 ),
5844 );
5845 }
5846 }
5847
5848 if inside_svg
5864 && matches!(
5865 component_name,
5866 "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
5867 )
5868 {
5869 use azul_css::props::{
5870 layout::{
5871 LayoutInsetBottom, LayoutLeft, LayoutPosition, LayoutRight, LayoutTop,
5872 },
5873 property::CssProperty,
5874 };
5875 let simple = azul_css::dynamic_selector::CssPropertyWithConditions::simple;
5876 intrinsic_props.push(simple(CssProperty::const_position(LayoutPosition::Absolute)));
5877 intrinsic_props.push(simple(CssProperty::const_left(LayoutLeft::const_px(0))));
5878 intrinsic_props.push(simple(CssProperty::const_top(LayoutTop::const_px(0))));
5879 intrinsic_props.push(simple(CssProperty::const_right(LayoutRight::const_px(0))));
5880 intrinsic_props.push(simple(CssProperty::const_bottom(LayoutInsetBottom::const_px(
5881 0,
5882 ))));
5883
5884 if let Some(fill) = xml_node.attributes.get_key("fill") {
5885 let fill = fill.as_str().trim();
5886 if fill != "none" {
5887 if let Ok(color) = azul_css::props::basic::color::parse_css_color(fill) {
5888 intrinsic_props.push(simple(CssProperty::const_background_content(
5889 azul_css::props::style::StyleBackgroundContentVec::from_vec(vec![
5890 azul_css::props::style::StyleBackgroundContent::Color(color),
5891 ]),
5892 )));
5893 }
5894 }
5895 }
5896
5897 if let Some(stroke) = xml_node.attributes.get_key("stroke") {
5903 let stroke = stroke.as_str().trim();
5904 if stroke != "none" {
5905 if let Ok(color) = azul_css::props::basic::color::parse_css_color(stroke) {
5906 use azul_css::props::style::{
5907 StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor,
5908 StyleBorderTopColor,
5909 };
5910 intrinsic_props.push(simple(CssProperty::const_border_top_color(
5911 StyleBorderTopColor { inner: color },
5912 )));
5913 intrinsic_props.push(simple(CssProperty::const_border_right_color(
5914 StyleBorderRightColor { inner: color },
5915 )));
5916 intrinsic_props.push(simple(CssProperty::const_border_bottom_color(
5917 StyleBorderBottomColor { inner: color },
5918 )));
5919 intrinsic_props.push(simple(CssProperty::const_border_left_color(
5920 StyleBorderLeftColor { inner: color },
5921 )));
5922 }
5923 }
5924 }
5925 if let Some(width) = parse_svg_float(xml_node.attributes.get_key("stroke-width")) {
5928 if width.is_finite() && width > 0.0 {
5929 use azul_css::props::style::{
5930 LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
5931 LayoutBorderTopWidth,
5932 };
5933 let px = azul_css::props::basic::PixelValue::px(width);
5934 intrinsic_props.push(simple(CssProperty::const_border_top_width(
5935 LayoutBorderTopWidth { inner: px },
5936 )));
5937 intrinsic_props.push(simple(CssProperty::const_border_right_width(
5938 LayoutBorderRightWidth { inner: px },
5939 )));
5940 intrinsic_props.push(simple(CssProperty::const_border_bottom_width(
5941 LayoutBorderBottomWidth { inner: px },
5942 )));
5943 intrinsic_props.push(simple(CssProperty::const_border_left_width(
5944 LayoutBorderLeftWidth { inner: px },
5945 )));
5946 }
5947 }
5948 }
5949
5950 if component_name == "svg" {
5952 intrinsic_props.push(
5953 azul_css::dynamic_selector::CssPropertyWithConditions::simple(
5954 azul_css::props::property::CssProperty::const_position(
5955 azul_css::props::layout::LayoutPosition::Relative,
5956 ),
5957 ),
5958 );
5959 }
5960
5961 let style_attr = xml_node.attributes.get_key("style");
5963 if style_attr.is_some() || dir_prop.is_some() || !intrinsic_props.is_empty() {
5964 use azul_css::dynamic_selector::CssPropertyWithConditions;
5965 let css_key_map = azul_css::props::property::get_css_key_map();
5966 let mut props: Vec<CssPropertyWithConditions> = intrinsic_props;
5967 if let Some(dir) = dir_prop {
5968 props.push(CssPropertyWithConditions::simple(
5969 azul_css::props::property::CssProperty::Direction(
5970 azul_css::css::CssPropertyValue::Exact(dir),
5971 ),
5972 ));
5973 }
5974 if let Some(style) = style_attr {
5975 let mut attributes = Vec::new();
5976 for s in style.as_str().split(';') {
5977 let mut s = s.split(':');
5978 let Some(key) = s.next() else {
5979 continue;
5980 };
5981 let Some(value) = s.next() else {
5982 continue;
5983 };
5984 drop(azul_css::parser2::parse_css_declaration(
5987 key.trim(),
5988 value.trim(),
5989 azul_css::parser2::ErrorLocationRange::default(),
5990 &css_key_map,
5991 &mut Vec::new(),
5992 &mut attributes,
5993 ));
5994 }
5995 props.extend(attributes.into_iter().filter_map(|s| match s {
5996 CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
5997 CssDeclaration::Dynamic(_) => None,
5998 }));
5999 }
6000 if !props.is_empty() {
6001 node.set_css_props(props.into());
6002 }
6003 }
6004
6005 let tag = component_name;
6007 let is_svg_shape = inside_svg
6008 && matches!(
6009 tag,
6010 "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
6011 );
6012
6013 if is_svg_shape {
6014 let clip = match tag {
6015 "path" => xml_node
6016 .attributes
6017 .get_key("d")
6018 .and_then(|d| crate::path_parser::parse_svg_path_d(d.as_str()).ok()),
6019 "circle" => {
6020 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
6021 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
6022 let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
6023 if r > 0.0 {
6024 Some(crate::svg::SvgMultiPolygon {
6025 rings: crate::svg::SvgPathVec::from_vec(vec![
6026 crate::path_parser::svg_circle_to_paths(cx, cy, r),
6027 ]),
6028 })
6029 } else {
6030 None
6031 }
6032 }
6033 "rect" => {
6034 let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
6035 let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
6036 let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
6037 let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
6038 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
6039 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
6040 if w > 0.0 && h > 0.0 {
6041 Some(crate::svg::SvgMultiPolygon {
6042 rings: crate::svg::SvgPathVec::from_vec(vec![
6043 crate::path_parser::svg_rect_to_path(x, y, w, h, rx, ry),
6044 ]),
6045 })
6046 } else {
6047 None
6048 }
6049 }
6050 "ellipse" => {
6051 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
6052 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
6053 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
6054 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
6055 if rx > 0.0 && ry > 0.0 {
6056 use azul_css::props::basic::{SvgCubicCurve, SvgPoint};
6058 const KAPPA: f32 = 0.552_284_8;
6059 let kx = rx * KAPPA;
6060 let ky = ry * KAPPA;
6061 let elements = vec![
6062 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6063 start: SvgPoint { x: cx, y: cy - ry },
6064 ctrl_1: SvgPoint {
6065 x: cx + kx,
6066 y: cy - ry,
6067 },
6068 ctrl_2: SvgPoint {
6069 x: cx + rx,
6070 y: cy - ky,
6071 },
6072 end: SvgPoint { x: cx + rx, y: cy },
6073 }),
6074 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6075 start: SvgPoint { x: cx + rx, y: cy },
6076 ctrl_1: SvgPoint {
6077 x: cx + rx,
6078 y: cy + ky,
6079 },
6080 ctrl_2: SvgPoint {
6081 x: cx + kx,
6082 y: cy + ry,
6083 },
6084 end: SvgPoint { x: cx, y: cy + ry },
6085 }),
6086 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6087 start: SvgPoint { x: cx, y: cy + ry },
6088 ctrl_1: SvgPoint {
6089 x: cx - kx,
6090 y: cy + ry,
6091 },
6092 ctrl_2: SvgPoint {
6093 x: cx - rx,
6094 y: cy + ky,
6095 },
6096 end: SvgPoint { x: cx - rx, y: cy },
6097 }),
6098 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
6099 start: SvgPoint { x: cx - rx, y: cy },
6100 ctrl_1: SvgPoint {
6101 x: cx - rx,
6102 y: cy - ky,
6103 },
6104 ctrl_2: SvgPoint {
6105 x: cx - kx,
6106 y: cy - ry,
6107 },
6108 end: SvgPoint { x: cx, y: cy - ry },
6109 }),
6110 ];
6111 Some(crate::svg::SvgMultiPolygon {
6112 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
6113 items: crate::svg::SvgPathElementVec::from_vec(elements),
6114 }]),
6115 })
6116 } else {
6117 None
6118 }
6119 }
6120 "line" => {
6121 let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
6122 let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
6123 let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
6124 let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
6125 Some(crate::svg::SvgMultiPolygon {
6126 rings: crate::svg::SvgPathVec::from_vec(vec![crate::svg::SvgPath {
6127 items: crate::svg::SvgPathElementVec::from_vec(vec![
6128 crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
6129 azul_css::props::basic::SvgPoint { x: x1, y: y1 },
6130 azul_css::props::basic::SvgPoint { x: x2, y: y2 },
6131 )),
6132 ]),
6133 }]),
6134 })
6135 }
6136 "polygon" | "polyline" => xml_node
6137 .attributes
6138 .get_key("points")
6139 .and_then(|pts| parse_svg_points(pts.as_str(), tag == "polygon")),
6140 _ => None,
6141 };
6142
6143 if let Some(mp) = clip {
6144 node.set_svg_data(crate::dom::SvgNodeData::Path(mp));
6145 }
6146 }
6147}
6148
6149fn apply_cell_span_attributes(node: &mut crate::dom::NodeData, xml_node: &XmlNode) {
6155 let mut spans = Vec::new();
6156 if let Some(n) = xml_node
6157 .attributes
6158 .get_key("colspan")
6159 .and_then(|v| v.as_str().trim().parse::<i32>().ok())
6160 {
6161 spans.push(crate::dom::AttributeType::ColSpan(n));
6162 }
6163 if let Some(n) = xml_node
6164 .attributes
6165 .get_key("rowspan")
6166 .and_then(|v| v.as_str().trim().parse::<i32>().ok())
6167 {
6168 spans.push(crate::dom::AttributeType::RowSpan(n));
6169 }
6170 if !spans.is_empty() {
6171 let mut v = node.attributes().clone().into_library_owned_vec();
6172 v.extend(spans);
6173 node.set_attributes(v.into());
6174 }
6175}
6176
6177#[allow(clippy::result_large_err)]
6178#[allow(clippy::only_used_in_recursion)]
6184fn collect_style_text(node: &XmlNode, out: &mut Vec<String>, depth: usize) {
6189 if depth >= MAX_XML_NESTING_DEPTH {
6190 return;
6191 }
6192 for child in node.children.as_ref() {
6193 let XmlNodeChild::Element(element) = child else {
6194 continue;
6195 };
6196 if normalize_casing(&element.node_type) == "style" {
6197 let text = element.get_text_content();
6198 if !text.is_empty() {
6199 out.push(text);
6200 }
6201 } else {
6202 collect_style_text(element, out, depth + 1);
6203 }
6204 }
6205}
6206
6207fn xml_node_to_dom_fast<'a>(
6208 xml_node: &'a XmlNode,
6209 component_map: &'a ComponentMap,
6210 inside_svg: bool,
6211 depth: usize,
6212) -> Result<Dom, RenderDomError> {
6213 use crate::dom::Dom;
6214
6215 let component_name = normalize_casing(&xml_node.node_type);
6216
6217 let node_type = tag_to_node_type(&component_name);
6219 let mut dom = Dom::create_node(node_type);
6220
6221 apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
6222
6223 let child_inside_svg = inside_svg || component_name == "svg";
6224
6225 if depth >= MAX_XML_NESTING_DEPTH {
6230 return Ok(dom);
6231 }
6232
6233 let mut children = Vec::new();
6235 let mut scoped_css: Vec<Css> = Vec::new();
6246 if component_name == "svg" {
6252 let mut texts = Vec::new();
6253 collect_style_text(xml_node, &mut texts, 0);
6254 for text in texts {
6255 scoped_css.push(Css::from_string(text.into()));
6256 }
6257 }
6258 for child in xml_node.children.as_ref() {
6259 match child {
6260 XmlNodeChild::Element(child_node)
6261 if normalize_casing(&child_node.node_type) == "style" =>
6262 {
6263 if component_name != "svg" {
6266 let text = child_node.get_text_content();
6267 if !text.is_empty() {
6268 scoped_css.push(Css::from_string(text.into()));
6269 }
6270 }
6271 }
6272 XmlNodeChild::Element(child_node) => {
6273 let child_dom =
6274 xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
6275 children.push(child_dom);
6276 }
6277 XmlNodeChild::Text(text) => {
6278 let text_dom = Dom::create_text_do_not_use_without_block_level_wrapper(
6279 AzString::from(text.as_str()),
6280 );
6281 children.push(text_dom);
6282 }
6283 }
6284 }
6285
6286 if !children.is_empty() {
6287 dom = dom.with_children(children.into());
6288 }
6289
6290 for css in scoped_css {
6291 dom.add_component_css(css);
6292 }
6293
6294 Ok(dom)
6295}
6296
6297#[derive(Debug)]
6300pub struct CompactDomBuilder {
6301 hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
6302 node_data: Vec<crate::dom::NodeData>,
6303 css: Vec<crate::dom::CssWithNodeId>,
6304 stack: Vec<(usize, Option<usize>)>,
6306}
6307
6308impl Default for CompactDomBuilder {
6309 fn default() -> Self {
6310 Self::new()
6311 }
6312}
6313
6314impl CompactDomBuilder {
6315 #[must_use]
6316 pub const fn new() -> Self {
6317 Self {
6318 hierarchy: Vec::new(),
6319 node_data: Vec::new(),
6320 css: Vec::new(),
6321 stack: Vec::new(),
6322 }
6323 }
6324
6325 #[must_use]
6326 pub fn with_capacity(cap: usize) -> Self {
6327 Self {
6328 hierarchy: Vec::with_capacity(cap),
6329 node_data: Vec::with_capacity(cap),
6330 css: Vec::new(),
6331 stack: Vec::new(),
6332 }
6333 }
6334
6335 pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
6337 use crate::id::NodeId;
6338 use crate::styled_dom::NodeHierarchyItem;
6339
6340 let idx = self.hierarchy.len();
6341
6342 let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
6344 NodeId::into_raw(&Some(NodeId::new(parent_idx)))
6345 } else {
6346 0 };
6348
6349 let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
6351 prev_child.map_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
6352 } else {
6353 0
6354 };
6355
6356 if let Some(&(_, Some(prev_idx))) = self.stack.last() {
6358 self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
6359 }
6360
6361 if let Some(parent) = self.stack.last_mut() {
6363 parent.1 = Some(idx);
6364 }
6365
6366 self.hierarchy.push(NodeHierarchyItem {
6368 parent: parent_raw,
6369 previous_sibling: prev_sibling_raw,
6370 next_sibling: 0, last_child: 0, });
6373 self.node_data.push(node_data);
6374
6375 self.stack.push((idx, None));
6377 }
6378
6379 pub fn close_node(&mut self) {
6381 use crate::id::NodeId;
6382
6383 if let Some((idx, last_child_idx)) = self.stack.pop() {
6384 self.hierarchy[idx].last_child =
6386 last_child_idx.map_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
6387 }
6388 }
6389
6390 pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
6392 self.open_node(node_data);
6393 self.close_node();
6394 }
6395
6396 pub fn add_css(&mut self, node_id: usize, css: Css) {
6398 self.css.push(crate::dom::CssWithNodeId { node_id, css });
6399 }
6400
6401 #[must_use]
6403 pub fn finish(self) -> crate::dom::FastDom {
6404 crate::dom::FastDom {
6405 node_hierarchy: self.hierarchy.into(),
6406 node_data: self.node_data.into(),
6407 css: self.css.into(),
6408 }
6409 }
6410}
6411
6412#[allow(clippy::result_large_err)]
6415#[allow(clippy::only_used_in_recursion)]
6418fn xml_node_to_fast_dom<'a>(
6419 xml_node: &'a XmlNode,
6420 component_map: &'a ComponentMap,
6421 inside_svg: bool,
6422 builder: &mut CompactDomBuilder,
6423 depth: usize,
6424) -> Result<(), RenderDomError> {
6425 use crate::dom::NodeData;
6426
6427 let component_name = normalize_casing(&xml_node.node_type);
6428 let node_type = tag_to_node_type(&component_name);
6429 let mut node_data = NodeData::create_node(node_type);
6430
6431 apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
6432
6433 let child_inside_svg = inside_svg || component_name == "svg";
6434
6435 builder.open_node(node_data);
6437
6438 if depth < MAX_XML_NESTING_DEPTH {
6443 for child in xml_node.children.as_ref() {
6445 match child {
6446 XmlNodeChild::Element(child_node) => {
6447 xml_node_to_fast_dom(
6448 child_node,
6449 component_map,
6450 child_inside_svg,
6451 builder,
6452 depth + 1,
6453 )?;
6454 }
6455 XmlNodeChild::Text(text) => {
6456 builder.add_leaf(
6457 NodeData::create_text_do_not_use_without_block_level_wrapper(
6458 AzString::from(text.as_str()),
6459 ),
6460 );
6461 }
6462 }
6463 }
6464 }
6465
6466 builder.close_node();
6468
6469 Ok(())
6470}
6471
6472#[allow(clippy::result_large_err)] fn render_dom_from_body_node_fast<'a>(
6476 body_node: &'a XmlNode,
6477 mut global_css: Option<Css>,
6478 component_map: &'a ComponentMap,
6479 max_width: Option<f32>,
6480) -> Result<StyledDom, RenderDomError> {
6481 use crate::dom::{NodeData, NodeType};
6482
6483 let mut builder = CompactDomBuilder::new();
6484
6485 builder.open_node(NodeData::create_node(NodeType::Html));
6488 xml_node_to_fast_dom(body_node, component_map, false, &mut builder, 0)?;
6490 builder.close_node();
6492
6493 let mut combined_rules: Vec<CssRuleBlock> = Vec::new();
6495 if let Some(max_width) = max_width {
6496 let max_width_css =
6497 Css::from_string(format!("html {{ max-width: {max_width}px; }}").into());
6498 combined_rules.extend(max_width_css.rules.into_library_owned_vec());
6499 }
6500 let mut combined_keyframes = Vec::new();
6501 if let Some(css) = global_css.take() {
6502 combined_rules.extend(css.rules.into_library_owned_vec());
6503 combined_keyframes.extend(css.keyframes.into_library_owned_vec());
6504 }
6505 let mut combined_css = Css::new(combined_rules);
6506 combined_css.keyframes = combined_keyframes.into();
6507
6508 let mut fast_dom = builder.finish();
6510 fast_dom.css = vec![crate::dom::CssWithNodeId {
6511 node_id: 0, css: combined_css,
6513 }]
6514 .into();
6515
6516 let styled = StyledDom::create_from_fast_dom(fast_dom);
6518 Ok(styled)
6519}
6520
6521fn set_stringified_attributes(
6524 dom_string: &mut String,
6525 xml_attributes: &XmlAttributeMap,
6526 filtered_xml_attributes: &ComponentArgumentVec,
6527 tabs: usize,
6528) {
6529 let t0 = String::from(" ").repeat(tabs);
6530 let t = String::from(" ").repeat(tabs + 1);
6531
6532 let _ = &t;
6535 for id in xml_attributes
6536 .get_key("id")
6537 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6538 .unwrap_or_default()
6539 {
6540 let _ = write!(
6541 dom_string,
6542 "\r\n{}.with_id(\"{}\")",
6543 t0,
6544 format_args_dynamic(id, filtered_xml_attributes)
6545 );
6546 }
6547
6548 for class in xml_attributes
6549 .get_key("class")
6550 .map(|s| s.split_whitespace().collect::<Vec<_>>())
6551 .unwrap_or_default()
6552 {
6553 let _ = write!(
6554 dom_string,
6555 "\r\n{}.with_class(\"{}\")",
6556 t0,
6557 format_args_dynamic(class, filtered_xml_attributes)
6558 );
6559 }
6560
6561 if let Some(focusable) = xml_attributes
6562 .get_key("focusable")
6563 .map(|f| format_args_dynamic(f, filtered_xml_attributes))
6564 .and_then(|f| parse_bool(&f))
6565 {
6566 if focusable {
6567 let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)");
6568 } else {
6569 let _ = write!(
6570 dom_string,
6571 "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6572 );
6573 }
6574 }
6575
6576 if let Some(tab_index) = xml_attributes
6577 .get_key("tabindex")
6578 .map(|val| format_args_dynamic(val, filtered_xml_attributes))
6579 .and_then(|val| val.parse::<isize>().ok())
6580 {
6581 match tab_index {
6582 0 => {
6583 let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)");
6584 }
6585 i if i > 0 => {
6586 let _ = write!(
6587 dom_string,
6588 "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
6589 t,
6590 usize::try_from(i).unwrap_or(0)
6591 );
6592 }
6593 _ => {
6594 let _ = write!(
6595 dom_string,
6596 "\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
6597 );
6598 }
6599 }
6600 }
6601}
6602
6603#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
6605pub enum DynamicItem {
6606 Var {
6608 name: String,
6609 format_spec: Option<String>,
6611 },
6612 Str(String),
6613}
6614
6615#[must_use]
6633pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
6634 use self::DynamicItem::{Str, Var};
6635
6636 let input: Vec<char> = input.chars().collect();
6637 let input_chars_len = input.len();
6638
6639 let mut items = Vec::new();
6640 let mut current_idx = 0;
6641 let mut last_idx = 0;
6642
6643 while current_idx < input_chars_len {
6644 let c = input[current_idx];
6645 match c {
6646 '{' if input.get(current_idx + 1).copied() != Some('{') => {
6647 let mut start_offset = 1;
6649 let mut has_found_variable = false;
6650 while let Some(c) = input.get(current_idx + start_offset) {
6651 if c.is_whitespace() {
6652 break;
6653 }
6654 if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
6655 {
6656 start_offset += 1;
6657 has_found_variable = true;
6658 break;
6659 }
6660 start_offset += 1;
6661 }
6662
6663 if has_found_variable {
6667 if last_idx != current_idx {
6668 items.push(Str(input[last_idx..current_idx].iter().collect()));
6669 }
6670
6671 let var_content: String = input
6673 [(current_idx + 1)..(current_idx + start_offset - 1)]
6674 .iter()
6675 .collect();
6676 let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
6678 let name = var_content[..colon_pos].to_string();
6679 let spec = var_content[(colon_pos + 1)..].to_string();
6680 (name, Some(spec))
6681 } else {
6682 (var_content, None)
6683 };
6684 items.push(Var {
6685 name: var_name,
6686 format_spec,
6687 });
6688 current_idx += start_offset;
6689 last_idx = current_idx;
6690 } else {
6691 current_idx += start_offset;
6692 }
6693 }
6694 _ => {
6695 current_idx += 1;
6696 }
6697 }
6698 }
6699
6700 if current_idx != last_idx {
6701 items.push(Str(input[last_idx..].iter().collect()));
6702 }
6703
6704 for item in &mut items {
6705 if let Str(s) = item {
6707 *s = s.replace("{{", "{").replace("}}", "}");
6708 }
6709 }
6710
6711 items
6712}
6713
6714fn combine_and_replace_dynamic_items(
6721 input: &[DynamicItem],
6722 variables: &ComponentArgumentVec,
6723) -> String {
6724 let mut s = String::new();
6725
6726 for item in input {
6727 match item {
6728 DynamicItem::Var { name, format_spec } => {
6729 let variable_name = normalize_casing(name.trim());
6730 if let Some(resolved_var) = variables
6731 .iter()
6732 .find(|s| s.name.as_str() == variable_name)
6733 .map(|q| &q.arg_type)
6734 {
6735 s.push_str(resolved_var);
6737 } else {
6738 s.push('{');
6739 s.push_str(name);
6740 if let Some(spec) = format_spec {
6741 s.push(':');
6742 s.push_str(spec);
6743 }
6744 s.push('}');
6745 }
6746 }
6747 DynamicItem::Str(dynamic_str) => {
6748 s.push_str(dynamic_str);
6749 }
6750 }
6751 }
6752
6753 s
6754}
6755
6756#[must_use]
6775pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
6776 let dynamic_str_items = split_dynamic_string(input);
6777 combine_and_replace_dynamic_items(&dynamic_str_items, variables)
6778}
6779
6780fn decode_numeric_entity(entity: &str) -> Option<char> {
6784 let num = entity.strip_prefix('#')?;
6785 let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
6786 u32::from_str_radix(hex, 16).ok()?
6787 } else {
6788 num.parse::<u32>().ok()?
6789 };
6790 char::from_u32(code)
6791}
6792
6793fn decode_entities(input: &str) -> String {
6800 const MAX_ENTITY_BODY: usize = 12;
6803
6804 let mut out = String::with_capacity(input.len());
6805 let bytes = input.as_bytes();
6806 let mut i = 0;
6807 while i < input.len() {
6808 if bytes[i] == b'&' {
6809 if let Some(semi_rel) = input[i + 1..].find(';') {
6810 if semi_rel <= MAX_ENTITY_BODY {
6811 let body = &input[i + 1..i + 1 + semi_rel];
6812 let end = i + 1 + semi_rel; if body.eq_ignore_ascii_case("nbsp") {
6815 out.push_str(&input[i..=end]);
6816 i = end + 1;
6817 continue;
6818 }
6819 let decoded = match body {
6820 "lt" => Some('<'),
6821 "gt" => Some('>'),
6822 "amp" => Some('&'),
6823 "quot" => Some('"'),
6824 "apos" => Some('\''),
6825 _ => decode_numeric_entity(body),
6826 };
6827 if let Some(c) = decoded {
6828 out.push(c);
6829 i = end + 1;
6830 continue;
6831 }
6832 }
6833 }
6834 out.push('&');
6836 i += 1;
6837 } else {
6838 let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
6840 out.push(ch);
6841 i += ch.len_utf8();
6842 }
6843 }
6844 out
6845}
6846
6847#[must_use]
6849pub fn prepare_string(input: &str) -> String {
6850 const SPACE: &str = " ";
6851 const RETURN: &str = "\n";
6852
6853 let input = input.trim();
6854
6855 if input.is_empty() {
6856 return String::new();
6857 }
6858
6859 let input = decode_entities(input);
6867
6868 let input_len = input.len();
6869 let mut final_lines: Vec<String> = Vec::new();
6870 let mut last_line_was_empty = false;
6871
6872 for line in input.lines() {
6873 let line = line.trim();
6874 let line = line.replace(" ", " ");
6875 let current_line_is_empty = line.is_empty();
6876
6877 if !current_line_is_empty {
6878 if last_line_was_empty {
6879 final_lines.push(format!("{RETURN}{line}"));
6880 } else {
6881 final_lines.push(line.to_string());
6882 }
6883 }
6884
6885 last_line_was_empty = current_line_is_empty;
6886 }
6887
6888 let mut target = String::with_capacity(input_len);
6889 for (line_idx, line) in final_lines.iter().enumerate() {
6890 if !(line.starts_with(RETURN) || line_idx == 0) {
6895 target.push_str(SPACE);
6896 }
6897 target.push_str(line);
6898 }
6899 target
6900}
6901
6902#[must_use]
6904pub fn parse_bool(input: &str) -> Option<bool> {
6905 match input {
6906 "true" => Some(true),
6907 "false" => Some(false),
6908 _ => None,
6909 }
6910}
6911
6912#[derive(Debug, Clone)]
6913pub struct CssMatcher {
6914 path: Vec<CssPathSelector>,
6915 indices_in_parent: Vec<usize>,
6916 children_length: Vec<usize>,
6917}
6918
6919impl CssMatcher {
6920 fn get_hash(&self) -> u64 {
6921 use core::hash::Hash;
6922
6923 use core::hash::Hasher;
6924
6925 let mut hasher = crate::hash::DefaultHasher::new();
6926 for p in &self.path {
6927 p.hash(&mut hasher);
6928 }
6929 hasher.finish()
6930 }
6931}
6932
6933impl CssMatcher {
6934 fn matches(&self, path: &CssPath) -> bool {
6935 use azul_css::css::CssPathSelector::*;
6936
6937 use crate::style::{CssGroupIterator, CssGroupSplitReason};
6938
6939 if self.path.is_empty() {
6940 return false;
6941 }
6942 if path.selectors.as_ref().is_empty() {
6943 return false;
6944 }
6945
6946 let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
6948 path_groups.reverse();
6949
6950 if path_groups.is_empty() {
6951 return false;
6952 }
6953 let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
6954 self_groups.reverse();
6955 if self_groups.is_empty() {
6956 return false;
6957 }
6958
6959 if self.indices_in_parent.len() != self_groups.len() {
6960 return false;
6961 }
6962 if self.children_length.len() != self_groups.len() {
6963 return false;
6964 }
6965
6966 let mut cur_selfgroup_scan = 0;
6980 let mut cur_pathgroup_scan = 0;
6981 let mut valid = false;
6982 let mut path_group = path_groups[cur_pathgroup_scan].clone();
6983
6984 while cur_selfgroup_scan < self_groups.len() {
6985 let mut advance = None;
6986
6987 for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
6989 let gm = group_matches(
6990 &path_group.0,
6991 &self_groups[cur_selfgroup_scan + id].0,
6992 self.indices_in_parent[cur_selfgroup_scan + id],
6993 self.children_length[cur_selfgroup_scan + id],
6994 );
6995
6996 if gm {
6997 advance = Some(id);
7000 break;
7001 }
7002 }
7003
7004 match advance {
7005 Some(n) => {
7006 if cur_pathgroup_scan == path_groups.len() - 1 {
7009 return cur_selfgroup_scan + n == self_groups.len() - 1;
7011 }
7012 cur_pathgroup_scan += 1;
7013 cur_selfgroup_scan += n;
7014 path_group = path_groups[cur_pathgroup_scan].clone();
7015 }
7016 None => return false, }
7018 }
7019
7020 cur_pathgroup_scan == path_groups.len() - 1
7022 }
7023}
7024
7025fn group_matches(
7028 a: &[&CssPathSelector],
7029 b: &[&CssPathSelector],
7030 idx_in_parent: usize,
7031 parent_children: usize,
7032) -> bool {
7033 use azul_css::css::{
7034 CssNthChildSelector, CssPathPseudoSelector,
7035 CssPathSelector::{Class, Global, Id, PseudoSelector, Type},
7036 };
7037
7038 for selector in a {
7039 match selector {
7040 Global
7042 | PseudoSelector(
7043 CssPathPseudoSelector::Hover
7044 | CssPathPseudoSelector::Active
7045 | CssPathPseudoSelector::Focus
7046 | CssPathPseudoSelector::SeatFocus,
7047 ) => {}
7048
7049 Type(tag) => {
7050 if !b.iter().any(|t| **t == Type(*tag)) {
7051 return false;
7052 }
7053 }
7054 Class(class) => {
7055 if !b.iter().any(|t| **t == Class(class.clone())) {
7056 return false;
7057 }
7058 }
7059 Id(id) => {
7060 if !b.iter().any(|t| **t == Id(id.clone())) {
7061 return false;
7062 }
7063 }
7064 PseudoSelector(CssPathPseudoSelector::First) => {
7065 if idx_in_parent != 0 {
7066 return false;
7067 }
7068 }
7069 PseudoSelector(CssPathPseudoSelector::Last) => {
7070 if idx_in_parent != parent_children.saturating_sub(1) {
7071 return false;
7072 }
7073 }
7074 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
7075 if idx_in_parent != *i as usize {
7076 return false;
7077 }
7078 }
7079 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
7080 if !idx_in_parent.is_multiple_of(2) {
7081 return false;
7082 }
7083 }
7084 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
7085 if idx_in_parent.is_multiple_of(2) {
7086 return false;
7087 }
7088 }
7089 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
7090 if !idx_in_parent
7091 .saturating_sub(p.offset as usize)
7092 .is_multiple_of(p.pattern_repeat as usize)
7093 {
7094 return false;
7095 }
7096 }
7097
7098 _ => return false, }
7100 }
7101
7102 true
7103}
7104
7105struct CssBlock {
7106 ending: Option<CssPathPseudoSelector>,
7107 block: CssRuleBlock,
7108}
7109
7110#[allow(clippy::result_large_err)] pub fn compile_body_node_to_rust_code<'a>(
7115 body_node: &'a XmlNode,
7116 component_map: &'a ComponentMap,
7117 extra_blocks: &mut VecContents,
7118 css_blocks: &mut BTreeMap<String, String>,
7119 css: &Css,
7120 mut matcher: CssMatcher,
7121) -> Result<String, CompileError> {
7122 use azul_css::css::CssDeclaration;
7123
7124 let t = "";
7125 let t2 = " ";
7126 let mut dom_string = String::from("Dom::create_body()");
7127 let node_type = CssPathSelector::Type(NodeTypeTag::Body);
7128 matcher.path.push(node_type);
7129
7130 let ids = body_node
7131 .attributes
7132 .get_key("id")
7133 .map(|s| s.split_whitespace().collect::<Vec<_>>())
7134 .unwrap_or_default();
7135 matcher.path.extend(
7136 ids.into_iter()
7137 .map(|id| CssPathSelector::Id(id.to_string().into())),
7138 );
7139 let classes = body_node
7140 .attributes
7141 .get_key("class")
7142 .map(|s| s.split_whitespace().collect::<Vec<_>>())
7143 .unwrap_or_default();
7144 matcher.path.extend(
7145 classes
7146 .into_iter()
7147 .map(|class| CssPathSelector::Class(class.to_string().into())),
7148 );
7149
7150 let matcher_hash = matcher.get_hash();
7151 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
7152 if !css_blocks_for_this_node.is_empty() {
7153 for css_block in &css_blocks_for_this_node {
7159 for declaration in css_block.block.declarations.as_ref() {
7160 let prop = match declaration {
7161 CssDeclaration::Static(s) => s,
7162 CssDeclaration::Dynamic(d) => &d.default_value,
7163 };
7164 extra_blocks.insert_from_css_property(prop);
7165 }
7166 }
7167
7168 let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
7169 if !inline_css.is_empty() {
7170 let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7171 let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
7172 }
7173 let _ = (&mut *css_blocks, matcher_hash); }
7175
7176 if !body_node.children.as_ref().is_empty() {
7177 use azul_css::codegen::format::GetHash;
7178 let children_hash = body_node.children.as_ref().get_hash();
7179 dom_string.push_str("\r\n.with_children(vec![\r\n");
7180
7181 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
7182 match child {
7183 XmlNodeChild::Element(child_node) => {
7184 let mut matcher = matcher.clone();
7185 matcher.path.push(CssPathSelector::Children);
7186 matcher.indices_in_parent.push(child_idx);
7187 matcher.children_length.push(body_node.children.len());
7188
7189 let _ = write!(
7190 dom_string,
7191 "{}{},\r\n",
7192 t,
7193 compile_node_to_rust_code_inner(
7194 child_node,
7195 component_map,
7196 1,
7197 extra_blocks,
7198 css_blocks,
7199 css,
7200 matcher,
7201 )?
7202 );
7203 }
7204 XmlNodeChild::Text(text) => {
7205 let text = text.trim();
7206 if !text.is_empty() {
7207 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
7208 let _ = write!(dom_string,
7209 "{t}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"),\r\n"
7210 );
7211 }
7212 }
7213 }
7214 }
7215 let _ = write!(dom_string, "\r\n{t}])");
7216 }
7217
7218 let dom_string = dom_string.trim();
7219 Ok(dom_string.to_string())
7220}
7221
7222fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
7228 fn decls_of(block: &CssBlock) -> Vec<String> {
7229 block
7230 .block
7231 .declarations
7232 .as_ref()
7233 .iter()
7234 .map(|d| {
7235 let prop = match d {
7236 CssDeclaration::Static(s) => s,
7237 CssDeclaration::Dynamic(dy) => &dy.default_value,
7238 };
7239 format!("{}: {};", prop.key(), prop.value())
7240 })
7241 .collect()
7242 }
7243
7244 let mut normal: Vec<String> = Vec::new();
7245 let mut pseudo: Vec<String> = Vec::new();
7246 for block in blocks {
7247 let pseudo_sel = match block.ending {
7248 Some(CssPathPseudoSelector::Hover) => Some(":hover"),
7249 Some(CssPathPseudoSelector::Active) => Some(":active"),
7250 Some(CssPathPseudoSelector::Focus) => Some(":focus"),
7251 Some(CssPathPseudoSelector::SeatFocus) => Some(":seat-focus"),
7252 _ => None,
7253 };
7254 match pseudo_sel {
7255 None => normal.extend(decls_of(block)),
7256 Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
7257 }
7258 }
7259
7260 let mut parts = normal;
7261 parts.extend(pseudo);
7262 parts.join(" ")
7263}
7264
7265fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
7266 let mut blocks = Vec::new();
7267
7268 for css_block in css.rules.as_ref() {
7269 if matcher.matches(&css_block.path) {
7270 let ending = match css_block.path.selectors.as_ref().last() {
7271 Some(CssPathSelector::PseudoSelector(p)) => Some(p.clone()),
7272 _ => None,
7273 };
7274
7275 blocks.push(CssBlock {
7276 ending,
7277 block: css_block.clone(),
7278 });
7279 }
7280 }
7281
7282 blocks
7283}
7284
7285fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
7286 use self::DynamicItem::{Str, Var};
7287 if input.is_empty() {
7288 String::from("AzString::from_const_str(\"\")")
7289 } else if input.len() == 1 {
7290 match &input[0] {
7292 Var { name, format_spec } => {
7293 let var_name = normalize_casing(name.trim());
7294 if let Some(spec) = format_spec {
7295 format!("format!(\"{{:{spec}}}\", {var_name}).into()")
7296 } else {
7297 var_name
7298 }
7299 }
7300 Str(s) => format!("AzString::from_const_str(\"{s}\")"),
7301 }
7302 } else {
7303 let mut formatted_str = String::from("format!(\"");
7305 let mut variables = Vec::new();
7306 for item in input {
7307 match item {
7308 Var { name, format_spec } => {
7309 let variable_name = normalize_casing(name.trim());
7310 if let Some(spec) = format_spec {
7311 let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
7312 } else {
7313 let _ = write!(formatted_str, "{{{variable_name}}}");
7314 }
7315 variables.push(variable_name.clone());
7316 }
7317 Str(s) => {
7318 let s = s.replace('"', "\\\"");
7319 formatted_str.push_str(&s);
7320 }
7321 }
7322 }
7323
7324 formatted_str.push('\"');
7325 if !variables.is_empty() {
7326 formatted_str.push_str(", ");
7327 }
7328
7329 formatted_str.push_str(&variables.join(", "));
7330 formatted_str.push_str(").into()");
7331 formatted_str
7332 }
7333}
7334
7335fn format_args_for_rust_code(input: &str) -> String {
7336 let dynamic_str_items = split_dynamic_string(input);
7337 compile_and_format_dynamic_items(&dynamic_str_items)
7338}
7339
7340#[allow(clippy::result_large_err)]
7341#[allow(clippy::only_used_in_recursion)]
7345#[allow(clippy::too_many_lines)] fn compile_node_to_rust_code_inner(
7347 node: &XmlNode,
7348 component_map: &ComponentMap,
7349 tabs: usize,
7350 extra_blocks: &mut VecContents,
7351 css_blocks: &mut BTreeMap<String, String>,
7352 css: &Css,
7353 mut matcher: CssMatcher,
7354) -> Result<String, CompileError> {
7355 use azul_css::css::CssDeclaration;
7356
7357 let t = String::from(" ").repeat(tabs - 1);
7358 let t2 = String::from(" ").repeat(tabs);
7359
7360 let component_name = normalize_casing(&node.node_type);
7361
7362 let node_type_tag = tag_to_node_type_tag(&component_name);
7364 let node_type = CssPathSelector::Type(node_type_tag);
7365
7366 let ctor = analyze_node_ctor(&component_name, node);
7375 let mut dom_string = ctor.render_rust().map_or_else(
7376 || {
7377 let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
7378 format!("{t2}Dom::create_node(NodeType::{tag})")
7379 },
7380 |expr| format!("{t2}{expr}"),
7381 );
7382
7383 matcher.path.push(node_type);
7384 let ids = node
7385 .attributes
7386 .get_key("id")
7387 .map(|s| s.split_whitespace().collect::<Vec<_>>())
7388 .unwrap_or_default();
7389
7390 matcher.path.extend(
7391 ids.into_iter()
7392 .map(|id| CssPathSelector::Id(id.to_string().into())),
7393 );
7394
7395 let classes = node
7396 .attributes
7397 .get_key("class")
7398 .map(|s| s.split_whitespace().collect::<Vec<_>>())
7399 .unwrap_or_default();
7400
7401 matcher.path.extend(
7402 classes
7403 .into_iter()
7404 .map(|class| CssPathSelector::Class(class.to_string().into())),
7405 );
7406
7407 let matcher_hash = matcher.get_hash();
7408 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
7409 if !css_blocks_for_this_node.is_empty() {
7410 for css_block in &css_blocks_for_this_node {
7416 for declaration in css_block.block.declarations.as_ref() {
7417 let prop = match declaration {
7418 CssDeclaration::Static(s) => s,
7419 CssDeclaration::Dynamic(d) => &d.default_value,
7420 };
7421 extra_blocks.insert_from_css_property(prop);
7422 }
7423 }
7424
7425 let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
7426 if !inline_css.is_empty() {
7427 let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
7428 let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
7429 }
7430 let _ = (&mut *css_blocks, matcher_hash); }
7432
7433 set_stringified_attributes(
7434 &mut dom_string,
7435 &node.attributes,
7436 &ComponentArgumentVec::new(),
7437 tabs,
7438 );
7439
7440 let mut caption_skipped = false;
7443 let mut children_string = node
7444 .children
7445 .as_ref()
7446 .iter()
7447 .enumerate()
7448 .filter_map(|(child_idx, c)| match c {
7449 XmlNodeChild::Element(child_node) => {
7450 if ctor.skip_caption()
7451 && !caption_skipped
7452 && child_node
7453 .node_type
7454 .as_str()
7455 .eq_ignore_ascii_case("caption")
7456 {
7457 caption_skipped = true;
7458 return None;
7459 }
7460 let mut matcher = matcher.clone();
7461 matcher.path.push(CssPathSelector::Children);
7462 matcher.indices_in_parent.push(child_idx);
7463 matcher.children_length.push(node.children.len());
7464
7465 Some(compile_node_to_rust_code_inner(
7466 child_node,
7467 component_map,
7468 tabs + 1,
7469 extra_blocks,
7470 css_blocks,
7471 css,
7472 matcher,
7473 ))
7474 }
7475 XmlNodeChild::Text(text) => {
7476 if ctor.consumes_text() {
7477 return None;
7478 }
7479 let text = text.trim();
7480 if text.is_empty() {
7481 None
7482 } else {
7483 let t2 = String::from(" ").repeat(tabs);
7484 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
7485 Some(Ok(format!(
7486 "{t2}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\")"
7487 )))
7488 }
7489 }
7490 })
7491 .collect::<Result<Vec<_>, _>>()?
7492 .join(",\r\n");
7493
7494 if !children_string.is_empty() {
7495 let _ = write!(
7496 dom_string,
7497 "\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
7498 );
7499 }
7500
7501 Ok(dom_string)
7502}
7503
7504const SAFE_CONTAINER_TAGS: &[&str] = &[
7520 "Abbr",
7525 "Acronym",
7526 "Address",
7527 "Article",
7528 "Aside",
7529 "B",
7530 "Bdi",
7531 "Bdo",
7532 "Big",
7533 "BlockQuote",
7534 "Body",
7535 "Br",
7536 "Caption",
7537 "Cite",
7538 "Code",
7539 "ColGroup",
7540 "Dd",
7541 "Del",
7542 "Dfn",
7543 "Dir",
7544 "Div",
7545 "Dl",
7546 "Dt",
7547 "Em",
7548 "Embed",
7549 "FigCaption",
7550 "Figure",
7551 "Footer",
7552 "H1",
7553 "H2",
7554 "H3",
7555 "H4",
7556 "H5",
7557 "H6",
7558 "Head",
7559 "Header",
7560 "Hr",
7561 "Html",
7562 "I",
7563 "Ins",
7564 "Kbd",
7565 "Li",
7566 "Link",
7567 "Main",
7568 "Map",
7569 "Mark",
7570 "Meta",
7571 "Nav",
7572 "Object",
7573 "Ol",
7574 "P",
7575 "Pre",
7576 "Q",
7577 "Rp",
7578 "Rt",
7579 "Rtc",
7580 "Ruby",
7581 "S",
7582 "Samp",
7583 "Script",
7584 "Section",
7585 "Small",
7586 "Span",
7587 "Strong",
7588 "Style",
7589 "Sub",
7590 "Sup",
7591 "Svg",
7592 "TBody",
7593 "Td",
7594 "TFoot",
7595 "Th",
7596 "THead",
7597 "Title",
7598 "Tr",
7599 "U",
7600 "Ul",
7601 "Var",
7602 "Wbr",
7603];
7604
7605fn safe_container_tag(tag_dbg: &str) -> &'static str {
7608 SAFE_CONTAINER_TAGS
7609 .iter()
7610 .copied()
7611 .find(|t| *t == tag_dbg)
7612 .unwrap_or("Div")
7613}
7614
7615#[derive(Debug, Clone)]
7642enum CtorArg {
7643 Str(String),
7645 Aria(String),
7647 Float(f32),
7649 OptSome(String),
7651 OptNone,
7653}
7654
7655enum NodeCtor {
7657 Plain,
7659 Semantic {
7661 suffix: String,
7664 args: Vec<CtorArg>,
7665 consumes_text: bool,
7668 skip_caption: bool,
7671 },
7672}
7673
7674fn cap_first(tag: &str) -> String {
7678 let mut c = tag.chars();
7679 c.next().map_or_else(String::new, |f| {
7680 f.to_uppercase().collect::<String>() + c.as_str()
7681 })
7682}
7683
7684fn camel_to_snake(s: &str) -> String {
7688 let chars: Vec<char> = s.chars().collect();
7689 let mut out = String::new();
7690 for (i, &ch) in chars.iter().enumerate() {
7691 if ch.is_ascii_uppercase() && i > 0 {
7692 let prev = chars[i - 1];
7693 let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
7694 if prev.is_ascii_lowercase()
7695 || prev.is_ascii_digit()
7696 || (prev.is_ascii_uppercase() && next_lower)
7697 {
7698 out.push('_');
7699 }
7700 }
7701 out.extend(ch.to_lowercase());
7702 }
7703 out
7704}
7705
7706fn esc_lit(s: &str) -> String {
7708 s.replace('\\', "\\\\").replace('"', "\\\"")
7709}
7710
7711fn fmt_f32_lit(f: f32) -> String {
7713 let s = format!("{f}");
7714 if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
7715 s
7716 } else {
7717 format!("{s}.0")
7718 }
7719}
7720
7721fn node_direct_text(node: &XmlNode) -> String {
7723 node.children
7724 .as_ref()
7725 .iter()
7726 .filter_map(|c| match c {
7727 XmlNodeChild::Text(t) => {
7728 let t = t.trim();
7729 if t.is_empty() {
7730 None
7731 } else {
7732 Some(t.to_string())
7733 }
7734 }
7735 XmlNodeChild::Element(_) => None,
7736 })
7737 .collect::<Vec<_>>()
7738 .join(" ")
7739}
7740
7741fn node_aria_label(node: &XmlNode) -> Option<String> {
7743 node.attributes.get_key("aria-label").and_then(|v| {
7744 let v = v.as_str().trim();
7745 if v.is_empty() {
7746 None
7747 } else {
7748 Some(v.to_string())
7749 }
7750 })
7751}
7752
7753fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
7755 node.attributes
7756 .get_key(key)
7757 .map_or_else(|| default.to_string(), |v| v.as_str().to_string())
7758}
7759
7760fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
7762 node.attributes
7763 .get_key(key)
7764 .and_then(|v| v.as_str().trim().parse::<f32>().ok())
7765 .unwrap_or(default)
7766}
7767
7768fn first_caption_text(node: &XmlNode) -> Option<String> {
7770 node.children.as_ref().iter().find_map(|c| match c {
7771 XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
7772 let t = e.get_text_content();
7773 let t = t.trim();
7774 if t.is_empty() {
7775 None
7776 } else {
7777 Some(t.to_string())
7778 }
7779 }
7780 _ => None,
7781 })
7782}
7783
7784const WITH_TEXT_TAGS: &[&str] = &[
7786 "acronym",
7787 "b",
7788 "bdi",
7789 "bdo",
7790 "big",
7791 "blockquote",
7792 "cite",
7793 "code",
7794 "del",
7795 "dfn",
7796 "em",
7797 "h1",
7798 "h2",
7799 "h3",
7800 "h4",
7801 "h5",
7802 "h6",
7803 "i",
7804 "ins",
7805 "kbd",
7806 "li",
7807 "mark",
7808 "p",
7809 "pre",
7810 "rp",
7811 "rt",
7812 "s",
7813 "samp",
7814 "small",
7815 "span",
7816 "strong",
7817 "style",
7818 "sub",
7819 "sup",
7820 "td",
7821 "th",
7822 "title",
7823 "u",
7824 "var",
7825];
7826
7827#[allow(clippy::too_many_lines)] fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
7830 fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
7832 NodeCtor::Semantic {
7833 suffix: suffix.into(),
7834 args,
7835 consumes_text,
7836 skip_caption: false,
7837 }
7838 }
7839
7840 let aria = node_aria_label(node);
7841 let has_aria = aria.is_some();
7842 let label = aria.unwrap_or_default();
7843 let pure_text = node.has_only_text_children();
7846 let text = node_direct_text(node);
7847 let has_text = !text.is_empty();
7848 let cap = cap_first(tag);
7849
7850 if WITH_TEXT_TAGS.contains(&tag) {
7852 if pure_text && has_text {
7853 return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
7854 }
7855 return NodeCtor::Plain;
7856 }
7857
7858 match tag {
7859 "details" | "form" | "fieldset" | "legend" | "menu" | "output" | "datalist" | "canvas"
7861 | "audio" | "video" | "area" => {
7862 if has_aria {
7863 sem(cap, vec![CtorArg::Aria(label)], false)
7864 } else {
7865 sem(format!("{cap}NoA11y"), vec![], false)
7866 }
7867 }
7868 "summary" => {
7870 if pure_text && has_text {
7871 if has_aria {
7872 sem(
7873 "SummaryWithText",
7874 vec![CtorArg::Str(text), CtorArg::Aria(label)],
7875 true,
7876 )
7877 } else {
7878 sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
7879 }
7880 } else if has_aria {
7881 sem("Summary", vec![CtorArg::Aria(label)], false)
7882 } else {
7883 sem("SummaryNoA11y", vec![], false)
7884 }
7885 }
7886
7887 "button" => {
7889 if has_aria {
7890 sem(
7891 "Button",
7892 vec![CtorArg::Str(text), CtorArg::Aria(label)],
7893 true,
7894 )
7895 } else {
7896 sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
7897 }
7898 }
7899 "a" => {
7900 let href = node_attr_or(node, "href", "");
7901 if has_aria {
7902 sem(
7903 "A",
7904 vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)],
7905 true,
7906 )
7907 } else {
7908 let lbl = if has_text {
7909 CtorArg::OptSome(text)
7910 } else {
7911 CtorArg::OptNone
7912 };
7913 sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
7914 }
7915 }
7916 "label" => {
7917 let for_id = node_attr_or(node, "for", "");
7918 if has_aria {
7919 sem(
7920 "Label",
7921 vec![
7922 CtorArg::Str(for_id),
7923 CtorArg::Str(text),
7924 CtorArg::Aria(label),
7925 ],
7926 true,
7927 )
7928 } else {
7929 sem(
7930 "LabelNoA11y",
7931 vec![CtorArg::Str(for_id), CtorArg::Str(text)],
7932 true,
7933 )
7934 }
7935 }
7936 "input" => {
7937 let ty = node_attr_or(node, "type", "text");
7938 let name = node_attr_or(node, "name", "");
7939 if has_aria {
7940 sem(
7941 "Input",
7942 vec![
7943 CtorArg::Str(ty),
7944 CtorArg::Str(name),
7945 CtorArg::Str(label.clone()),
7946 CtorArg::Aria(label),
7947 ],
7948 false,
7949 )
7950 } else {
7951 sem(
7952 "InputNoA11y",
7953 vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)],
7954 false,
7955 )
7956 }
7957 }
7958 "textarea" => {
7959 let name = node_attr_or(node, "name", "");
7960 if has_aria {
7961 sem(
7962 "Textarea",
7963 vec![
7964 CtorArg::Str(name),
7965 CtorArg::Str(label.clone()),
7966 CtorArg::Aria(label),
7967 ],
7968 false,
7969 )
7970 } else {
7971 sem(
7972 "TextareaNoA11y",
7973 vec![CtorArg::Str(name), CtorArg::Str(label)],
7974 false,
7975 )
7976 }
7977 }
7978 "select" => {
7979 let name = node_attr_or(node, "name", "");
7980 if has_aria {
7981 sem(
7982 "Select",
7983 vec![
7984 CtorArg::Str(name),
7985 CtorArg::Str(label.clone()),
7986 CtorArg::Aria(label),
7987 ],
7988 false,
7989 )
7990 } else {
7991 sem(
7992 "SelectNoA11y",
7993 vec![CtorArg::Str(name), CtorArg::Str(label)],
7994 false,
7995 )
7996 }
7997 }
7998 "option" => {
7999 let value = node_attr_or(node, "value", "");
8000 if has_aria {
8001 sem(
8002 "Option",
8003 vec![
8004 CtorArg::Str(value),
8005 CtorArg::Str(text),
8006 CtorArg::Aria(label),
8007 ],
8008 true,
8009 )
8010 } else {
8011 sem(
8012 "OptionNoA11y",
8013 vec![CtorArg::Str(value), CtorArg::Str(text)],
8014 true,
8015 )
8016 }
8017 }
8018 "optgroup" => {
8019 let lbl = node_attr_or(node, "label", "");
8020 if has_aria {
8021 sem(
8022 "Optgroup",
8023 vec![CtorArg::Str(lbl), CtorArg::Aria(label)],
8024 false,
8025 )
8026 } else {
8027 sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
8028 }
8029 }
8030 "table" => {
8031 if has_aria {
8032 let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
8035 NodeCtor::Semantic {
8036 suffix: "Table".to_string(),
8037 args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
8038 consumes_text: false,
8039 skip_caption: true,
8040 }
8041 } else {
8042 sem("TableNoA11y", vec![], false)
8043 }
8044 }
8045
8046 "progress" => sem(
8048 "ProgressNoA11y",
8049 vec![
8050 CtorArg::Float(node_attr_f32(node, "value", 0.0)),
8051 CtorArg::Float(node_attr_f32(node, "max", 1.0)),
8052 ],
8053 false,
8054 ),
8055 "meter" => sem(
8056 "MeterNoA11y",
8057 vec![
8058 CtorArg::Float(node_attr_f32(node, "value", 0.0)),
8059 CtorArg::Float(node_attr_f32(node, "min", 0.0)),
8060 CtorArg::Float(node_attr_f32(node, "max", 1.0)),
8061 ],
8062 false,
8063 ),
8064 "dialog" => sem("DialogNoA11y", vec![], false),
8065
8066 _ => NodeCtor::Plain,
8067 }
8068}
8069
8070impl CtorArg {
8071 fn render_rust(&self) -> String {
8074 match self {
8075 Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
8076 Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
8077 Self::Float(f) => fmt_f32_lit(*f),
8078 Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
8079 Self::OptNone => "OptionString::None".to_string(),
8080 }
8081 }
8082 fn render_c(&self) -> String {
8083 match self {
8084 Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
8085 Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
8086 Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
8087 Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
8088 Self::OptNone => "AzOptionString_none()".to_string(),
8089 }
8090 }
8091 fn render_cpp(&self) -> String {
8092 match self {
8093 Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
8094 Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
8095 Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
8096 Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
8097 Self::OptNone => "OptionString::none()".to_string(),
8098 }
8099 }
8100 fn render_python(&self) -> String {
8101 match self {
8102 Self::Str(s) => format!("\"{}\"", esc_lit(s)),
8103 Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
8104 Self::Float(f) => fmt_f32_lit(*f),
8105 Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
8106 Self::OptNone => "azul.OptionString.none()".to_string(),
8107 }
8108 }
8109}
8110
8111impl NodeCtor {
8112 const fn consumes_text(&self) -> bool {
8113 matches!(
8114 self,
8115 Self::Semantic {
8116 consumes_text: true,
8117 ..
8118 }
8119 )
8120 }
8121 const fn skip_caption(&self) -> bool {
8122 matches!(
8123 self,
8124 Self::Semantic {
8125 skip_caption: true,
8126 ..
8127 }
8128 )
8129 }
8130 fn render_rust(&self) -> Option<String> {
8132 match self {
8133 Self::Plain => None,
8134 Self::Semantic { suffix, args, .. } => Some(format!(
8135 "Dom::create_{}({})",
8136 camel_to_snake(suffix),
8137 args.iter()
8138 .map(CtorArg::render_rust)
8139 .collect::<Vec<_>>()
8140 .join(", ")
8141 )),
8142 }
8143 }
8144 fn render_c(&self) -> Option<String> {
8146 match self {
8147 Self::Plain => None,
8148 Self::Semantic { suffix, args, .. } => Some(format!(
8149 "AzDom_create{}({})",
8150 suffix,
8151 args.iter()
8152 .map(CtorArg::render_c)
8153 .collect::<Vec<_>>()
8154 .join(", ")
8155 )),
8156 }
8157 }
8158 fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
8160 match self {
8161 Self::Plain => None,
8162 Self::Semantic { suffix, args, .. } => {
8163 let snake = camel_to_snake(suffix);
8164 let (prefix, rendered) = match target {
8165 CompileTarget::Cpp => (
8166 format!("Dom::create_{snake}"),
8167 args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
8168 ),
8169 CompileTarget::Python => (
8170 format!("azul.Dom.create_{snake}"),
8171 args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
8172 ),
8173 _ => return None,
8174 };
8175 Some(format!("{}({})", prefix, rendered.join(", ")))
8176 }
8177 }
8178 }
8179}
8180
8181struct FluentSyntax {
8184 target: CompileTarget,
8185 create_node: fn(&str) -> String,
8187 create_text: fn(&str) -> String,
8189 with_css: fn(&str) -> String,
8191 with_class: fn(&str) -> String,
8193 with_id: fn(&str) -> String,
8195 with_child: fn(&str) -> String,
8197}
8198
8199const CPP_SYNTAX: FluentSyntax = FluentSyntax {
8200 target: CompileTarget::Cpp,
8201 create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
8205 create_text: |s| {
8206 alloc::format!("Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{s}\"))")
8207 },
8208 with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
8209 with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
8210 with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
8211 with_child: |c| alloc::format!(".with_child({c})"),
8212};
8213
8214const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
8215 target: CompileTarget::Python,
8216 create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
8218 create_text: |s| {
8219 alloc::format!("azul.Dom.create_text_do_not_use_without_block_level_wrapper(\"{s}\")")
8220 },
8221 with_css: |s| alloc::format!(".with_css(\"{s}\")"),
8222 with_class: |s| alloc::format!(".with_class(\"{s}\")"),
8223 with_id: |s| alloc::format!(".with_id(\"{s}\")"),
8224 with_child: |c| alloc::format!(".with_child({c})"),
8225};
8226
8227#[allow(clippy::result_large_err)]
8230#[allow(clippy::only_used_in_recursion)]
8233fn compile_node_fluent(
8234 node: &XmlNode,
8235 syntax: &FluentSyntax,
8236 component_map: &ComponentMap,
8237 css: &Css,
8238 mut matcher: CssMatcher,
8239) -> Result<String, CompileError> {
8240 use azul_css::css::CssDeclaration;
8241
8242 let component_name = normalize_casing(&node.node_type);
8243 let node_type_tag = tag_to_node_type_tag(&component_name);
8244 let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
8245
8246 let ctor = analyze_node_ctor(&component_name, node);
8253 let mut s = ctor.render_fluent(&syntax.target).map_or_else(
8254 || (syntax.create_node)(safe_container_tag(&tag_dbg)),
8255 |expr| expr,
8256 );
8257
8258 matcher.path.push(CssPathSelector::Type(node_type_tag));
8259 let ids: Vec<String> = node
8260 .attributes
8261 .get_key("id")
8262 .map(|v| {
8263 v.split_whitespace()
8264 .map(alloc::string::ToString::to_string)
8265 .collect()
8266 })
8267 .unwrap_or_default();
8268 matcher
8269 .path
8270 .extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
8271 let classes: Vec<String> = node
8272 .attributes
8273 .get_key("class")
8274 .map(|v| {
8275 v.split_whitespace()
8276 .map(alloc::string::ToString::to_string)
8277 .collect()
8278 })
8279 .unwrap_or_default();
8280 matcher.path.extend(
8281 classes
8282 .iter()
8283 .map(|c| CssPathSelector::Class(c.clone().into())),
8284 );
8285
8286 let blocks = get_css_blocks(css, &matcher);
8288 if !blocks.is_empty() {
8289 let inline_css = css_blocks_to_inline_string(&blocks);
8290 if !inline_css.is_empty() {
8291 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8292 s.push_str(&(syntax.with_css)(&esc));
8293 }
8294 }
8295 for id in &ids {
8296 s.push_str(&(syntax.with_id)(
8297 &id.replace('\\', "\\\\").replace('"', "\\\""),
8298 ));
8299 }
8300 for class in &classes {
8301 s.push_str(&(syntax.with_class)(
8302 &class.replace('\\', "\\\\").replace('"', "\\\""),
8303 ));
8304 }
8305
8306 let mut caption_skipped = false;
8309 for (child_idx, child) in node.children.as_ref().iter().enumerate() {
8310 match child {
8311 XmlNodeChild::Element(child_node) => {
8312 if ctor.skip_caption()
8313 && !caption_skipped
8314 && child_node
8315 .node_type
8316 .as_str()
8317 .eq_ignore_ascii_case("caption")
8318 {
8319 caption_skipped = true;
8320 continue;
8321 }
8322 let mut m = matcher.clone();
8323 m.path.push(CssPathSelector::Children);
8324 m.indices_in_parent.push(child_idx);
8325 m.children_length.push(node.children.len());
8326 let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
8327 s.push_str(&(syntax.with_child)(&child_src));
8328 }
8329 XmlNodeChild::Text(text) => {
8330 if ctor.consumes_text() {
8331 continue;
8332 }
8333 let text = text.trim();
8334 if !text.is_empty() {
8335 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8336 s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
8337 }
8338 }
8339 }
8340 }
8341
8342 Ok(s)
8343}
8344
8345#[allow(clippy::result_large_err)] fn compile_body_fluent<'a>(
8348 body_node: &'a XmlNode,
8349 syntax: &FluentSyntax,
8350 component_map: &'a ComponentMap,
8351 css: &Css,
8352 mut matcher: CssMatcher,
8353) -> Result<String, CompileError> {
8354 let mut s = (syntax.create_node)("Body");
8355 matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
8356 let classes: Vec<String> = body_node
8357 .attributes
8358 .get_key("class")
8359 .map(|v| {
8360 v.split_whitespace()
8361 .map(alloc::string::ToString::to_string)
8362 .collect()
8363 })
8364 .unwrap_or_default();
8365 matcher.path.extend(
8366 classes
8367 .iter()
8368 .map(|c| CssPathSelector::Class(c.clone().into())),
8369 );
8370
8371 let blocks = get_css_blocks(css, &matcher);
8372 if !blocks.is_empty() {
8373 let inline_css = css_blocks_to_inline_string(&blocks);
8374 if !inline_css.is_empty() {
8375 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8376 s.push_str(&(syntax.with_css)(&esc));
8377 }
8378 }
8379 for class in &classes {
8380 s.push_str(&(syntax.with_class)(
8381 &class.replace('\\', "\\\\").replace('"', "\\\""),
8382 ));
8383 }
8384
8385 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
8386 match child {
8387 XmlNodeChild::Element(child_node) => {
8388 let mut m = matcher.clone();
8389 m.path.push(CssPathSelector::Children);
8390 m.indices_in_parent.push(child_idx);
8391 m.children_length.push(body_node.children.len());
8392 let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
8393 s.push_str(&(syntax.with_child)(&child_src));
8394 }
8395 XmlNodeChild::Text(text) => {
8396 let text = text.trim();
8397 if !text.is_empty() {
8398 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8399 s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
8400 }
8401 }
8402 }
8403 }
8404 Ok(s)
8405}
8406
8407#[allow(clippy::result_large_err)] fn parse_page_style_and_body(root_nodes: &[XmlNodeChild]) -> Result<(Css, XmlNode), CompileError> {
8415 let html_node = get_html_node(root_nodes)?;
8416 let body_node = get_body_node(html_node.children.as_ref())?.clone();
8417 let mut global_style = Css::empty();
8418 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
8419 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
8420 let text = style_node.get_text_content();
8421 if !text.is_empty() {
8422 global_style = azul_css::parser2::new_from_str(&text).0;
8423 }
8424 }
8425 }
8426 global_style.sort_by_specificity();
8427 Ok((global_style, body_node))
8428}
8429
8430fn body_matcher(body_node: &XmlNode) -> CssMatcher {
8431 CssMatcher {
8432 path: Vec::new(),
8433 indices_in_parent: vec![0],
8434 children_length: vec![body_node.children.as_ref().len()],
8435 }
8436}
8437
8438#[allow(clippy::result_large_err)] pub fn str_to_cpp_code<'a>(
8444 root_nodes: &'a [XmlNodeChild],
8445 component_map: &'a ComponentMap,
8446) -> Result<String, CompileError> {
8447 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
8448 let body_node = &body_node;
8449 let render = compile_body_fluent(
8450 body_node,
8451 &CPP_SYNTAX,
8452 component_map,
8453 &global_style,
8454 body_matcher(body_node),
8455 )?;
8456 Ok(alloc::format!(
8457 "// Auto-generated UI source code (C++). Build:\n\
8458 // clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
8459 #include \"azul20.hpp\"\n\
8460 using namespace azul;\n\n\
8461 struct Data {{}};\n\n\
8462 AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n \
8463 return {render};\n}}\n\n\
8464 int main() {{\n \
8465 RefAny data = RefAny::create(Data{{}});\n \
8466 WindowCreateOptions window = WindowCreateOptions::create(render);\n \
8467 App app = App::create(std::move(data), AppConfig::default_());\n \
8468 app.run(std::move(window));\n \
8469 return 0;\n}}\n"
8470 ))
8471}
8472
8473#[allow(clippy::result_large_err)] pub fn str_to_python_code<'a>(
8479 root_nodes: &'a [XmlNodeChild],
8480 component_map: &'a ComponentMap,
8481) -> Result<String, CompileError> {
8482 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
8483 let body_node = &body_node;
8484 let render = compile_body_fluent(
8485 body_node,
8486 &PYTHON_SYNTAX,
8487 component_map,
8488 &global_style,
8489 body_matcher(body_node),
8490 )?;
8491 Ok(alloc::format!(
8492 "# Auto-generated UI source code (Python). Run: python3 main.py\n\
8493 import azul\n\n\
8494 class Data:\n pass\n\n\
8495 def render(data, info):\n return (\n {}\n )\n\n\
8496 def main():\n \
8497 app = azul.App.create(Data(), azul.AppConfig.create())\n \
8498 window = azul.WindowCreateOptions.create(render)\n \
8499 app.run(window)\n\n\
8500 if __name__ == \"__main__\":\n main()\n",
8501 render.replace("\r\n", "\n ")
8502 ))
8503}
8504
8505fn c_creator_suffix(tag_dbg: &str) -> String {
8516 let mut chars = tag_dbg.chars();
8517 chars.next().map_or_else(
8518 || "Div".to_string(),
8519 |first| {
8520 let rest: String = chars.as_str().to_lowercase();
8521 alloc::format!("{first}{rest}")
8522 },
8523 )
8524}
8525
8526#[allow(clippy::result_large_err)] #[allow(clippy::too_many_lines)] fn compile_node_c(
8529 node: &XmlNode,
8530 component_map: &ComponentMap,
8531 css: &Css,
8532 mut matcher: CssMatcher,
8533 counter: &mut usize,
8534 out: &mut String,
8535) -> Result<String, CompileError> {
8536 let _ = component_map;
8537 let component_name = normalize_casing(&node.node_type);
8538 let node_type_tag = tag_to_node_type_tag(&component_name);
8539 let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
8540
8541 let var = alloc::format!("n{}", *counter);
8542 *counter += 1;
8543 let ctor = analyze_node_ctor(&component_name, node);
8544 match ctor.render_c() {
8545 Some(expr) => {
8546 let _ = writeln!(out, " AzDom {var} = {expr};");
8547 }
8548 None => {
8549 let _ = writeln!(
8550 out,
8551 " AzDom {} = AzDom_create{}();",
8552 var,
8553 c_creator_suffix(safe_container_tag(&tag_dbg))
8554 );
8555 }
8556 }
8557
8558 matcher.path.push(CssPathSelector::Type(node_type_tag));
8559 let ids: Vec<String> = node
8560 .attributes
8561 .get_key("id")
8562 .map(|v| {
8563 v.split_whitespace()
8564 .map(alloc::string::ToString::to_string)
8565 .collect()
8566 })
8567 .unwrap_or_default();
8568 matcher
8569 .path
8570 .extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
8571 let classes: Vec<String> = node
8572 .attributes
8573 .get_key("class")
8574 .map(|v| {
8575 v.split_whitespace()
8576 .map(alloc::string::ToString::to_string)
8577 .collect()
8578 })
8579 .unwrap_or_default();
8580 matcher.path.extend(
8581 classes
8582 .iter()
8583 .map(|c| CssPathSelector::Class(c.clone().into())),
8584 );
8585
8586 let blocks = get_css_blocks(css, &matcher);
8587 if !blocks.is_empty() {
8588 let inline_css = css_blocks_to_inline_string(&blocks);
8589 if !inline_css.is_empty() {
8590 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8591 let _ = writeln!(out, " {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
8592 }
8593 }
8594 for id in &ids {
8595 let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
8596 let _ = writeln!(out, " {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
8597 }
8598 for class in &classes {
8599 let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
8600 let _ = writeln!(
8601 out,
8602 " {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));"
8603 );
8604 }
8605
8606 let mut caption_skipped = false;
8607 for (child_idx, child) in node.children.as_ref().iter().enumerate() {
8608 match child {
8609 XmlNodeChild::Element(child_node) => {
8610 if ctor.skip_caption()
8611 && !caption_skipped
8612 && child_node
8613 .node_type
8614 .as_str()
8615 .eq_ignore_ascii_case("caption")
8616 {
8617 caption_skipped = true;
8618 continue;
8619 }
8620 let mut m = matcher.clone();
8621 m.path.push(CssPathSelector::Children);
8622 m.indices_in_parent.push(child_idx);
8623 m.children_length.push(node.children.len());
8624 let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
8625 let _ = writeln!(out, " AzDom_addChild(&{var}, {child_var});");
8626 }
8627 XmlNodeChild::Text(text) => {
8628 if ctor.consumes_text() {
8629 continue;
8630 }
8631 let text = text.trim();
8632 if !text.is_empty() {
8633 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8634 let _ = writeln!(out,
8635 " AzDom_addChild(&{var}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
8636 );
8637 }
8638 }
8639 }
8640 }
8641 Ok(var)
8642}
8643
8644#[allow(clippy::result_large_err)] pub fn str_to_c_code<'a>(
8650 root_nodes: &'a [XmlNodeChild],
8651 component_map: &'a ComponentMap,
8652) -> Result<String, CompileError> {
8653 let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
8654 let body_node = &body_node;
8655 let mut body = String::new();
8656 let mut counter = 0usize;
8657
8658 let root = alloc::format!("n{counter}");
8660 counter += 1;
8661 let _ = writeln!(body, " AzDom {root} = AzDom_createBody();");
8662
8663 let mut matcher = body_matcher(body_node);
8664 matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
8665 let classes: Vec<String> = body_node
8666 .attributes
8667 .get_key("class")
8668 .map(|v| {
8669 v.split_whitespace()
8670 .map(alloc::string::ToString::to_string)
8671 .collect()
8672 })
8673 .unwrap_or_default();
8674 matcher.path.extend(
8675 classes
8676 .iter()
8677 .map(|c| CssPathSelector::Class(c.clone().into())),
8678 );
8679 let blocks = get_css_blocks(&global_style, &matcher);
8680 if !blocks.is_empty() {
8681 let inline_css = css_blocks_to_inline_string(&blocks);
8682 if !inline_css.is_empty() {
8683 let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
8684 let _ = writeln!(
8685 body,
8686 " {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));"
8687 );
8688 }
8689 }
8690 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
8691 match child {
8692 XmlNodeChild::Element(child_node) => {
8693 let mut m = matcher.clone();
8694 m.path.push(CssPathSelector::Children);
8695 m.indices_in_parent.push(child_idx);
8696 m.children_length.push(body_node.children.len());
8697 let child_var = compile_node_c(
8698 child_node,
8699 component_map,
8700 &global_style,
8701 m,
8702 &mut counter,
8703 &mut body,
8704 )?;
8705 let _ = writeln!(body, " AzDom_addChild(&{root}, {child_var});");
8706 }
8707 XmlNodeChild::Text(text) => {
8708 let text = text.trim();
8709 if !text.is_empty() {
8710 let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
8711 let _ = writeln!(body,
8712 " AzDom_addChild(&{root}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
8713 );
8714 }
8715 }
8716 }
8717 }
8718
8719 Ok(alloc::format!(
8720 "/* Auto-generated UI source code (C). Build:\n\
8721 * clang -I <azul>/target/codegen main.c -lazul\n */\n\
8722 #include \"azul.h\"\n\
8723 #include <string.h>\n\
8724 #define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
8725 AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
8726 {body} return {root};\n}}\n\n\
8727 int main(void) {{\n \
8728 AzString data_type = AZ_STR(\"Data\");\n \
8729 AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n \
8730 AzApp app = AzApp_create(data, AzAppConfig_create());\n \
8731 AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n \
8732 AzApp_run(&app, window);\n \
8733 AzApp_delete(&app);\n \
8734 return 0;\n}}\n"
8735 ))
8736}
8737
8738#[cfg(test)]
8739#[path = "xml_test.rs"]
8740mod xml_test;