1use alloc::{
22 boxed::Box,
23 collections::BTreeMap,
24 string::{String, ToString},
25 vec::Vec,
26};
27use core::{fmt, hash::Hash};
28
29use azul_css::{
30 css::{
31 Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
32 NodeTypeTag,
33 },
34 format_rust_code::VecContents,
35 parser2::{CssParseErrorOwned, ErrorLocation},
36 props::{
37 basic::{ColorU, StyleFontFamilyVec},
38 property::CssProperty,
39 style::{
40 NormalizedLinearColorStopVec, NormalizedRadialColorStopVec, StyleBackgroundContentVec,
41 StyleBackgroundPositionVec, StyleBackgroundRepeatVec, StyleBackgroundSizeVec,
42 StyleTransformVec,
43 },
44 },
45 AzString, OptionString, StringVec, U8Vec,
46};
47
48use crate::{
49 dom::{Dom, NodeType, OptionNodeType},
50 styled_dom::StyledDom,
51 window::{AzStringPair, StringPairVec},
52};
53
54pub type SyntaxError = String;
58
59#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub struct XmlTagName {
63 pub inner: AzString,
64}
65
66impl From<AzString> for XmlTagName {
67 fn from(s: AzString) -> Self {
68 Self { inner: s }
69 }
70}
71
72impl From<String> for XmlTagName {
73 fn from(s: String) -> Self {
74 Self { inner: s.into() }
75 }
76}
77
78impl From<&str> for XmlTagName {
79 fn from(s: &str) -> Self {
80 Self { inner: s.into() }
81 }
82}
83
84impl core::ops::Deref for XmlTagName {
85 type Target = AzString;
86 fn deref(&self) -> &Self::Target {
87 &self.inner
88 }
89}
90
91pub type XmlTextContent = OptionString;
93
94#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
96#[repr(C)]
97pub struct XmlAttributeMap {
98 pub inner: StringPairVec,
99}
100
101impl From<StringPairVec> for XmlAttributeMap {
102 fn from(v: StringPairVec) -> Self {
103 Self { inner: v }
104 }
105}
106
107impl core::ops::Deref for XmlAttributeMap {
108 type Target = StringPairVec;
109 fn deref(&self) -> &Self::Target {
110 &self.inner
111 }
112}
113
114impl core::ops::DerefMut for XmlAttributeMap {
115 fn deref_mut(&mut self) -> &mut Self::Target {
116 &mut self.inner
117 }
118}
119
120type ComponentArgumentName = String;
122type ComponentArgumentType = String;
124type ComponentArgumentOrder = usize;
126
127#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
129#[repr(C)]
130pub struct ComponentArgument {
131 pub name: AzString,
132 pub arg_type: AzString,
133}
134
135impl_vec!(ComponentArgument, ComponentArgumentVec, ComponentArgumentVecDestructor, ComponentArgumentVecDestructorType, ComponentArgumentVecSlice, OptionComponentArgument);
136impl_option!(ComponentArgument, OptionComponentArgument, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
137impl_vec_debug!(ComponentArgument, ComponentArgumentVec);
138impl_vec_partialeq!(ComponentArgument, ComponentArgumentVec);
139impl_vec_eq!(ComponentArgument, ComponentArgumentVec);
140impl_vec_partialord!(ComponentArgument, ComponentArgumentVec);
141impl_vec_ord!(ComponentArgument, ComponentArgumentVec);
142impl_vec_hash!(ComponentArgument, ComponentArgumentVec);
143impl_vec_clone!(ComponentArgument, ComponentArgumentVec, ComponentArgumentVecDestructor);
144impl_vec_mut!(ComponentArgument, ComponentArgumentVec);
145
146#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
149pub struct ComponentArguments {
150 pub args: ComponentArgumentVec,
151 pub accepts_text: bool,
152}
153
154type ComponentName = String;
156type CompiledComponent = String;
158
159const DEFAULT_ARGS: [&str; 8] = [
162 "id",
163 "class",
164 "tabindex",
165 "focusable",
166 "accepts_text",
167 "name",
168 "style",
169 "args",
170];
171
172#[allow(non_camel_case_types)]
175pub enum c_void {}
176
177#[repr(C)]
179pub enum XmlNodeType {
180 Root,
181 Element,
182 PI,
183 Comment,
184 Text,
185}
186
187#[repr(C)]
189pub struct XmlQualifiedName {
190 pub local_name: AzString,
191 pub namespace: OptionString,
192}
193
194#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
196#[repr(C)]
197pub enum ExternalResourceKind {
198 Image,
200 Font,
202 Stylesheet,
204 Script,
206 Icon,
208 Video,
210 Audio,
212 Unknown,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
218#[repr(C)]
219pub struct MimeTypeHint {
220 pub inner: AzString,
221}
222
223impl MimeTypeHint {
224 pub fn new(s: &str) -> Self {
225 Self { inner: AzString::from(s) }
226 }
227
228 pub fn from_extension(ext: &str) -> Self {
229 let mime = match ext.to_lowercase().as_str() {
230 "png" => "image/png",
232 "jpg" | "jpeg" => "image/jpeg",
233 "gif" => "image/gif",
234 "webp" => "image/webp",
235 "svg" => "image/svg+xml",
236 "ico" => "image/x-icon",
237 "bmp" => "image/bmp",
238 "avif" => "image/avif",
239 "ttf" => "font/ttf",
241 "otf" => "font/otf",
242 "woff" => "font/woff",
243 "woff2" => "font/woff2",
244 "eot" => "application/vnd.ms-fontobject",
245 "css" => "text/css",
247 "js" => "application/javascript",
249 "mjs" => "application/javascript",
250 "mp4" => "video/mp4",
252 "webm" => "video/webm",
253 "ogg" => "video/ogg",
254 "mp3" => "audio/mpeg",
256 "wav" => "audio/wav",
257 "flac" => "audio/flac",
258 _ => "application/octet-stream",
260 };
261 Self { inner: AzString::from(mime) }
262 }
263}
264
265impl_option!(
266 MimeTypeHint,
267 OptionMimeTypeHint,
268 copy = false,
269 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
270);
271
272#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
274#[repr(C)]
275pub struct ExternalResource {
276 pub url: AzString,
278 pub kind: ExternalResourceKind,
280 pub mime_type: OptionMimeTypeHint,
282 pub source_element: AzString,
284 pub source_attribute: AzString,
286}
287
288impl_option!(
289 ExternalResource,
290 OptionExternalResource,
291 copy = false,
292 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
293);
294
295impl_vec!(ExternalResource, ExternalResourceVec, ExternalResourceVecDestructor, ExternalResourceVecDestructorType, ExternalResourceVecSlice, OptionExternalResource);
296impl_vec_mut!(ExternalResource, ExternalResourceVec);
297impl_vec_debug!(ExternalResource, ExternalResourceVec);
298impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
299impl_vec_eq!(ExternalResource, ExternalResourceVec);
300impl_vec_partialord!(ExternalResource, ExternalResourceVec);
301impl_vec_ord!(ExternalResource, ExternalResourceVec);
302impl_vec_hash!(ExternalResource, ExternalResourceVec);
303impl_vec_clone!(ExternalResource, ExternalResourceVec, ExternalResourceVecDestructor);
304
305#[derive(Debug, PartialEq, PartialOrd, Clone)]
306#[repr(C)]
307pub struct Xml {
308 pub root: XmlNodeChildVec,
309}
310
311impl Xml {
312 pub fn scan_external_resources(&self) -> ExternalResourceVec {
324 let mut resources = Vec::new();
325
326 for child in self.root.as_ref().iter() {
327 Self::scan_node_child(child, &mut resources);
328 }
329
330 resources.into()
331 }
332
333 fn scan_node_child(child: &XmlNodeChild, resources: &mut Vec<ExternalResource>) {
334 match child {
335 XmlNodeChild::Text(text) => {
336 Self::extract_css_urls(text.as_str(), resources);
338 }
339 XmlNodeChild::Element(node) => {
340 Self::scan_node(node, resources);
341 }
342 }
343 }
344
345 fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
346 let tag_name = node.node_type.inner.as_str().to_lowercase();
347
348 let get_attr = |name: &str| -> Option<String> {
350 node.attributes.inner.as_ref().iter()
351 .find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
352 .map(|pair| pair.value.as_str().to_string())
353 };
354
355 match tag_name.as_str() {
356 "img" => {
357 if let Some(src) = get_attr("src") {
358 let mime = Self::guess_mime_from_url(&src, "image");
359 resources.push(ExternalResource {
360 url: AzString::from(src),
361 kind: ExternalResourceKind::Image,
362 mime_type: mime.into(),
363 source_element: AzString::from("img"),
364 source_attribute: AzString::from("src"),
365 });
366 }
367 if let Some(srcset) = get_attr("srcset") {
369 for src in Self::parse_srcset(&srcset) {
370 let mime = Self::guess_mime_from_url(&src, "image");
371 resources.push(ExternalResource {
372 url: AzString::from(src),
373 kind: ExternalResourceKind::Image,
374 mime_type: mime.into(),
375 source_element: AzString::from("img"),
376 source_attribute: AzString::from("srcset"),
377 });
378 }
379 }
380 }
381 "link" => {
382 if let Some(href) = get_attr("href") {
383 let rel = get_attr("rel").unwrap_or_default().to_lowercase();
384 let type_attr = get_attr("type");
385 let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
386
387 let (kind, category) = if rel.contains("stylesheet") {
388 (ExternalResourceKind::Stylesheet, "stylesheet")
389 } else if rel.contains("icon") || rel.contains("apple-touch-icon") {
390 (ExternalResourceKind::Icon, "image")
391 } else if as_attr == "font" {
392 (ExternalResourceKind::Font, "font")
393 } else if as_attr == "script" {
394 (ExternalResourceKind::Script, "script")
395 } else if as_attr == "image" {
396 (ExternalResourceKind::Image, "image")
397 } else {
398 (ExternalResourceKind::Unknown, "")
399 };
400
401 let mime = type_attr.map(|t| MimeTypeHint::new(&t))
402 .or_else(|| Self::guess_mime_from_url(&href, category));
403
404 resources.push(ExternalResource {
405 url: AzString::from(href),
406 kind,
407 mime_type: mime.into(),
408 source_element: AzString::from("link"),
409 source_attribute: AzString::from("href"),
410 });
411 }
412 }
413 "script" => {
414 if let Some(src) = get_attr("src") {
415 let type_attr = get_attr("type");
416 let mime = type_attr.map(|t| MimeTypeHint::new(&t))
417 .or_else(|| Some(MimeTypeHint::new("application/javascript")));
418
419 resources.push(ExternalResource {
420 url: AzString::from(src),
421 kind: ExternalResourceKind::Script,
422 mime_type: mime.into(),
423 source_element: AzString::from("script"),
424 source_attribute: AzString::from("src"),
425 });
426 }
427 }
428 "video" => {
429 if let Some(src) = get_attr("src") {
430 let mime = Self::guess_mime_from_url(&src, "video");
431 resources.push(ExternalResource {
432 url: AzString::from(src),
433 kind: ExternalResourceKind::Video,
434 mime_type: mime.into(),
435 source_element: AzString::from("video"),
436 source_attribute: AzString::from("src"),
437 });
438 }
439 if let Some(poster) = get_attr("poster") {
440 let mime = Self::guess_mime_from_url(&poster, "image");
441 resources.push(ExternalResource {
442 url: AzString::from(poster),
443 kind: ExternalResourceKind::Image,
444 mime_type: mime.into(),
445 source_element: AzString::from("video"),
446 source_attribute: AzString::from("poster"),
447 });
448 }
449 }
450 "audio" => {
451 if let Some(src) = get_attr("src") {
452 let mime = Self::guess_mime_from_url(&src, "audio");
453 resources.push(ExternalResource {
454 url: AzString::from(src),
455 kind: ExternalResourceKind::Audio,
456 mime_type: mime.into(),
457 source_element: AzString::from("audio"),
458 source_attribute: AzString::from("src"),
459 });
460 }
461 }
462 "source" => {
463 if let Some(src) = get_attr("src") {
464 let type_attr = get_attr("type");
465 let kind = if type_attr.as_ref().map(|t| t.starts_with("audio")).unwrap_or(false) {
467 ExternalResourceKind::Audio
468 } else {
469 ExternalResourceKind::Video
470 };
471 let mime = type_attr.map(|t| MimeTypeHint::new(&t))
472 .or_else(|| Self::guess_mime_from_url(&src, if kind == ExternalResourceKind::Audio { "audio" } else { "video" }));
473
474 resources.push(ExternalResource {
475 url: AzString::from(src),
476 kind,
477 mime_type: mime.into(),
478 source_element: AzString::from("source"),
479 source_attribute: AzString::from("src"),
480 });
481 }
482 if let Some(srcset) = get_attr("srcset") {
484 for src in Self::parse_srcset(&srcset) {
485 let mime = Self::guess_mime_from_url(&src, "image");
486 resources.push(ExternalResource {
487 url: AzString::from(src),
488 kind: ExternalResourceKind::Image,
489 mime_type: mime.into(),
490 source_element: AzString::from("source"),
491 source_attribute: AzString::from("srcset"),
492 });
493 }
494 }
495 }
496 "a" => {
497 if let Some(href) = get_attr("href") {
498 if Self::looks_like_resource(&href) {
500 let mime = Self::guess_mime_from_url(&href, "");
501 resources.push(ExternalResource {
502 url: AzString::from(href),
503 kind: ExternalResourceKind::Unknown,
504 mime_type: mime.into(),
505 source_element: AzString::from("a"),
506 source_attribute: AzString::from("href"),
507 });
508 }
509 }
510 }
511 "virtualized-view" | "embed" | "object" => {
512 let src_attr = if tag_name == "object" { "data" } else { "src" };
513 if let Some(src) = get_attr(src_attr) {
514 resources.push(ExternalResource {
515 url: AzString::from(src),
516 kind: ExternalResourceKind::Unknown,
517 mime_type: OptionMimeTypeHint::None,
518 source_element: AzString::from(tag_name.clone()),
519 source_attribute: AzString::from(src_attr),
520 });
521 }
522 }
523 "style" => {
524 for child in node.children.as_ref().iter() {
526 if let XmlNodeChild::Text(text) = child {
527 Self::extract_css_urls(text.as_str(), resources);
528 }
529 }
530 }
531 _ => {}
532 }
533
534 if let Some(style) = get_attr("style") {
536 Self::extract_css_urls(&style, resources);
537 }
538
539 if let Some(bg) = get_attr("background") {
541 let mime = Self::guess_mime_from_url(&bg, "image");
542 resources.push(ExternalResource {
543 url: AzString::from(bg),
544 kind: ExternalResourceKind::Image,
545 mime_type: mime.into(),
546 source_element: AzString::from(tag_name),
547 source_attribute: AzString::from("background"),
548 });
549 }
550
551 for child in node.children.as_ref().iter() {
553 Self::scan_node_child(child, resources);
554 }
555 }
556
557 fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
559 let mut remaining = css;
561
562 while let Some(pos) = remaining.find("url(") {
563 let after_url = &remaining[pos + 4..];
564 if let Some(url) = Self::extract_url_value(after_url) {
565 let mime = Self::guess_mime_from_url(&url, "");
566 let kind = Self::guess_kind_from_url(&url);
567 resources.push(ExternalResource {
568 url: AzString::from(url),
569 kind,
570 mime_type: mime.into(),
571 source_element: AzString::from("style"),
572 source_attribute: AzString::from("url()"),
573 });
574 }
575 remaining = after_url;
576 }
577
578 remaining = css;
580 while let Some(pos) = remaining.to_lowercase().find("@import") {
581 let after_import = &remaining[pos + 7..];
582 let trimmed = after_import.trim_start();
583
584 if trimmed.starts_with("url(") {
585 if let Some(url) = Self::extract_url_value(&trimmed[4..]) {
586 resources.push(ExternalResource {
587 url: AzString::from(url),
588 kind: ExternalResourceKind::Stylesheet,
589 mime_type: Some(MimeTypeHint::new("text/css")).into(),
590 source_element: AzString::from("style"),
591 source_attribute: AzString::from("@import"),
592 });
593 }
594 } else if let Some(url) = Self::extract_quoted_string(trimmed) {
595 resources.push(ExternalResource {
596 url: AzString::from(url),
597 kind: ExternalResourceKind::Stylesheet,
598 mime_type: Some(MimeTypeHint::new("text/css")).into(),
599 source_element: AzString::from("style"),
600 source_attribute: AzString::from("@import"),
601 });
602 }
603
604 remaining = after_import;
605 }
606 }
607
608 fn extract_url_value(s: &str) -> Option<String> {
610 let trimmed = s.trim_start();
611 if trimmed.starts_with('"') {
612 Self::extract_quoted_string(trimmed)
613 } else if trimmed.starts_with('\'') {
614 let end = trimmed[1..].find('\'')?;
615 Some(trimmed[1..1+end].to_string())
616 } else {
617 let end = trimmed.find(')')?;
618 Some(trimmed[..end].trim().to_string())
619 }
620 }
621
622 fn extract_quoted_string(s: &str) -> Option<String> {
624 if s.starts_with('"') {
625 let end = s[1..].find('"')?;
626 Some(s[1..1+end].to_string())
627 } else if s.starts_with('\'') {
628 let end = s[1..].find('\'')?;
629 Some(s[1..1+end].to_string())
630 } else {
631 None
632 }
633 }
634
635 fn parse_srcset(srcset: &str) -> Vec<String> {
637 srcset.split(',')
638 .filter_map(|entry| {
639 let trimmed = entry.trim();
640 trimmed.split_whitespace().next().map(|s| s.to_string())
642 })
643 .filter(|url| !url.is_empty())
644 .collect()
645 }
646
647 fn looks_like_resource(url: &str) -> bool {
649 let lower = url.to_lowercase();
650 let resource_exts = [
652 ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp",
653 ".ttf", ".otf", ".woff", ".woff2", ".eot",
654 ".css", ".js",
655 ".mp4", ".webm", ".ogg", ".mp3", ".wav",
656 ".pdf", ".zip", ".tar", ".gz",
657 ];
658 resource_exts.iter().any(|ext| lower.ends_with(ext))
659 }
660
661 fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
663 let lower = url.to_lowercase();
664 let path = lower.split('?').next().unwrap_or(&lower);
666 if path.ends_with(".png") || path.ends_with(".jpg") || path.ends_with(".jpeg")
667 || path.ends_with(".gif") || path.ends_with(".webp") || path.ends_with(".svg")
668 || path.ends_with(".bmp") || path.ends_with(".avif") {
669 ExternalResourceKind::Image
670 } else if path.ends_with(".ttf") || path.ends_with(".otf") || path.ends_with(".woff")
671 || path.ends_with(".woff2") || path.ends_with(".eot") {
672 ExternalResourceKind::Font
673 } else if path.ends_with(".css") {
674 ExternalResourceKind::Stylesheet
675 } else if path.ends_with(".js") || path.ends_with(".mjs") {
676 ExternalResourceKind::Script
677 } else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
678 ExternalResourceKind::Video
679 } else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
680 ExternalResourceKind::Audio
681 } else if path.ends_with(".ico") {
682 ExternalResourceKind::Icon
683 } else {
684 ExternalResourceKind::Unknown
685 }
686 }
687
688 fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
690 let lower = url.to_lowercase();
691 let ext = lower.rsplit('.').next()?;
693 let ext = ext.split('?').next()?;
695
696 let valid_exts = [
698 "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif",
699 "ttf", "otf", "woff", "woff2", "eot",
700 "css", "js", "mjs",
701 "mp4", "webm", "ogg", "mp3", "wav", "flac",
702 ];
703
704 if valid_exts.contains(&ext) {
705 Some(MimeTypeHint::from_extension(ext))
706 } else if !category.is_empty() {
707 match category {
709 "image" => Some(MimeTypeHint::new("image/*")),
710 "font" => Some(MimeTypeHint::new("font/*")),
711 "stylesheet" => Some(MimeTypeHint::new("text/css")),
712 "script" => Some(MimeTypeHint::new("application/javascript")),
713 "video" => Some(MimeTypeHint::new("video/*")),
714 "audio" => Some(MimeTypeHint::new("audio/*")),
715 _ => None,
716 }
717 } else {
718 None
719 }
720 }
721}
722
723#[derive(Debug, PartialEq, PartialOrd, Clone)]
724#[repr(C)]
725pub struct NonXmlCharError {
726 pub ch: u32, pub pos: XmlTextPos,
728}
729
730#[derive(Debug, PartialEq, PartialOrd, Clone)]
731#[repr(C)]
732pub struct InvalidCharError {
733 pub expected: u8,
734 pub got: u8,
735 pub pos: XmlTextPos,
736}
737
738#[derive(Debug, PartialEq, PartialOrd, Clone)]
739#[repr(C)]
740pub struct InvalidCharMultipleError {
741 pub expected: u8,
742 pub got: U8Vec,
743 pub pos: XmlTextPos,
744}
745
746#[derive(Debug, PartialEq, PartialOrd, Clone)]
747#[repr(C)]
748pub struct InvalidQuoteError {
749 pub got: u8,
750 pub pos: XmlTextPos,
751}
752
753#[derive(Debug, PartialEq, PartialOrd, Clone)]
754#[repr(C)]
755pub struct InvalidSpaceError {
756 pub got: u8,
757 pub pos: XmlTextPos,
758}
759
760#[derive(Debug, PartialEq, PartialOrd, Clone)]
761#[repr(C)]
762pub struct InvalidStringError {
763 pub got: AzString,
764 pub pos: XmlTextPos,
765}
766
767#[derive(Debug, PartialEq, PartialOrd, Clone)]
768#[repr(C, u8)]
769pub enum XmlStreamError {
770 UnexpectedEndOfStream,
771 InvalidName,
772 NonXmlChar(NonXmlCharError),
773 InvalidChar(InvalidCharError),
774 InvalidCharMultiple(InvalidCharMultipleError),
775 InvalidQuote(InvalidQuoteError),
776 InvalidSpace(InvalidSpaceError),
777 InvalidString(InvalidStringError),
778 InvalidReference,
779 InvalidExternalID,
780 InvalidCommentData,
781 InvalidCommentEnd,
782 InvalidCharacterData,
783}
784
785impl fmt::Display for XmlStreamError {
786 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
787 use self::XmlStreamError::*;
788 match self {
789 UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
790 InvalidName => write!(f, "Invalid name"),
791 NonXmlChar(nx) => write!(
792 f,
793 "Non-XML character: {:?} at {}",
794 core::char::from_u32(nx.ch),
795 nx.pos
796 ),
797 InvalidChar(ic) => write!(
798 f,
799 "Invalid character: expected: {}, got: {} at {}",
800 ic.expected as char, ic.got as char, ic.pos
801 ),
802 InvalidCharMultiple(imc) => write!(
803 f,
804 "Multiple invalid characters: expected: {}, got: {:?} at {}",
805 imc.expected,
806 imc.got.as_ref(),
807 imc.pos
808 ),
809 InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
810 InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
811 InvalidString(ise) => write!(
812 f,
813 "Invalid string: got \"{}\" at {}",
814 ise.got.as_str(),
815 ise.pos
816 ),
817 InvalidReference => write!(f, "Invalid reference"),
818 InvalidExternalID => write!(f, "Invalid external ID"),
819 InvalidCommentData => write!(f, "Invalid comment data"),
820 InvalidCommentEnd => write!(f, "Invalid comment end"),
821 InvalidCharacterData => write!(f, "Invalid character data"),
822 }
823 }
824}
825
826#[derive(Debug, PartialEq, PartialOrd, Clone, Ord, Hash, Eq)]
827#[repr(C)]
828pub struct XmlTextPos {
829 pub row: u32,
830 pub col: u32,
831}
832
833impl fmt::Display for XmlTextPos {
834 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
835 write!(f, "line {}:{}", self.row, self.col)
836 }
837}
838
839#[derive(Debug, PartialEq, PartialOrd, Clone)]
840#[repr(C)]
841pub struct XmlTextError {
842 pub stream_error: XmlStreamError,
843 pub pos: XmlTextPos,
844}
845
846#[derive(Debug, PartialEq, PartialOrd, Clone)]
847#[repr(C, u8)]
848pub enum XmlParseError {
849 InvalidDeclaration(XmlTextError),
850 InvalidComment(XmlTextError),
851 InvalidPI(XmlTextError),
852 InvalidDoctype(XmlTextError),
853 InvalidEntity(XmlTextError),
854 InvalidElement(XmlTextError),
855 InvalidAttribute(XmlTextError),
856 InvalidCdata(XmlTextError),
857 InvalidCharData(XmlTextError),
858 UnknownToken(XmlTextPos),
859}
860
861impl fmt::Display for XmlParseError {
862 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
863 use self::XmlParseError::*;
864 match self {
865 InvalidDeclaration(e) => {
866 write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
867 }
868 InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
869 InvalidPI(e) => write!(
870 f,
871 "Invalid processing instruction: {} at {}",
872 e.stream_error, e.pos
873 ),
874 InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
875 InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
876 InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
877 InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
878 InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
879 InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
880 UnknownToken(e) => write!(f, "Unknown token at {}", e),
881 }
882 }
883}
884
885impl_result!(
886 Xml,
887 XmlError,
888 ResultXmlXmlError,
889 copy = false,
890 [Debug, PartialEq, PartialOrd, Clone]
891);
892
893#[derive(Debug, PartialEq, PartialOrd, Clone)]
894#[repr(C)]
895pub struct DuplicatedNamespaceError {
896 pub ns: AzString,
897 pub pos: XmlTextPos,
898}
899
900#[derive(Debug, PartialEq, PartialOrd, Clone)]
901#[repr(C)]
902pub struct UnknownNamespaceError {
903 pub ns: AzString,
904 pub pos: XmlTextPos,
905}
906
907#[derive(Debug, PartialEq, PartialOrd, Clone)]
908#[repr(C)]
909pub struct UnexpectedCloseTagError {
910 pub expected: AzString,
911 pub actual: AzString,
912 pub pos: XmlTextPos,
913}
914
915#[derive(Debug, PartialEq, PartialOrd, Clone)]
916#[repr(C)]
917pub struct UnknownEntityReferenceError {
918 pub entity: AzString,
919 pub pos: XmlTextPos,
920}
921
922#[derive(Debug, PartialEq, PartialOrd, Clone)]
923#[repr(C)]
924pub struct DuplicatedAttributeError {
925 pub attribute: AzString,
926 pub pos: XmlTextPos,
927}
928
929#[derive(Debug, PartialEq, PartialOrd, Clone)]
931#[repr(C)]
932pub struct MalformedHierarchyError {
933 pub expected: AzString,
935 pub got: AzString,
937}
938
939#[derive(Debug, PartialEq, PartialOrd, Clone)]
940#[repr(C, u8)]
941pub enum XmlError {
942 NoParserAvailable,
943 InvalidXmlPrefixUri(XmlTextPos),
944 UnexpectedXmlUri(XmlTextPos),
945 UnexpectedXmlnsUri(XmlTextPos),
946 InvalidElementNamePrefix(XmlTextPos),
947 DuplicatedNamespace(DuplicatedNamespaceError),
948 UnknownNamespace(UnknownNamespaceError),
949 UnexpectedCloseTag(UnexpectedCloseTagError),
950 UnexpectedEntityCloseTag(XmlTextPos),
951 UnknownEntityReference(UnknownEntityReferenceError),
952 MalformedEntityReference(XmlTextPos),
953 EntityReferenceLoop(XmlTextPos),
954 InvalidAttributeValue(XmlTextPos),
955 DuplicatedAttribute(DuplicatedAttributeError),
956 NoRootNode,
957 SizeLimit,
958 DtdDetected,
959 MalformedHierarchy(MalformedHierarchyError),
961 ParserError(XmlParseError),
962 UnclosedRootNode,
963 UnexpectedDeclaration(XmlTextPos),
964 NodesLimitReached,
965 AttributesLimitReached,
966 NamespacesLimitReached,
967 InvalidName(XmlTextPos),
968 NonXmlChar(XmlTextPos),
969 InvalidChar(XmlTextPos),
970 InvalidChar2(XmlTextPos),
971 InvalidString(XmlTextPos),
972 InvalidExternalID(XmlTextPos),
973 InvalidComment(XmlTextPos),
974 InvalidCharacterData(XmlTextPos),
975 UnknownToken(XmlTextPos),
976 UnexpectedEndOfStream,
977}
978
979impl fmt::Display for XmlError {
980 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
981 use self::XmlError::*;
982 match self {
983 NoParserAvailable => write!(
984 f,
985 "Library was compiled without XML parser (XML parser not available)"
986 ),
987 InvalidXmlPrefixUri(pos) => {
988 write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
989 }
990 UnexpectedXmlUri(pos) => {
991 write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
992 }
993 UnexpectedXmlnsUri(pos) => write!(
994 f,
995 "Unexpected XML namespace URI at line {}:{}",
996 pos.row, pos.col
997 ),
998 InvalidElementNamePrefix(pos) => write!(
999 f,
1000 "Invalid element name prefix at line {}:{}",
1001 pos.row, pos.col
1002 ),
1003 DuplicatedNamespace(ns) => write!(
1004 f,
1005 "Duplicated namespace: \"{}\" at {}",
1006 ns.ns.as_str(),
1007 ns.pos
1008 ),
1009 UnknownNamespace(uns) => write!(
1010 f,
1011 "Unknown namespace: \"{}\" at {}",
1012 uns.ns.as_str(),
1013 uns.pos
1014 ),
1015 UnexpectedCloseTag(ct) => write!(
1016 f,
1017 "Unexpected close tag: expected \"{}\", got \"{}\" at {}",
1018 ct.expected.as_str(),
1019 ct.actual.as_str(),
1020 ct.pos
1021 ),
1022 UnexpectedEntityCloseTag(pos) => write!(
1023 f,
1024 "Unexpected entity close tag at line {}:{}",
1025 pos.row, pos.col
1026 ),
1027 UnknownEntityReference(uer) => write!(
1028 f,
1029 "Unexpected entity reference: \"{}\" at {}",
1030 uer.entity, uer.pos
1031 ),
1032 MalformedEntityReference(pos) => write!(
1033 f,
1034 "Malformed entity reference at line {}:{}",
1035 pos.row, pos.col
1036 ),
1037 EntityReferenceLoop(pos) => write!(
1038 f,
1039 "Entity reference loop (recursive entity reference) at line {}:{}",
1040 pos.row, pos.col
1041 ),
1042 InvalidAttributeValue(pos) => {
1043 write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
1044 }
1045 DuplicatedAttribute(ae) => write!(
1046 f,
1047 "Duplicated attribute \"{}\" at line {}:{}",
1048 ae.attribute.as_str(),
1049 ae.pos.row,
1050 ae.pos.col
1051 ),
1052 NoRootNode => write!(f, "No root node found"),
1053 SizeLimit => write!(f, "XML file too large (size limit reached)"),
1054 DtdDetected => write!(f, "Document type descriptor detected"),
1055 MalformedHierarchy(e) => write!(
1056 f,
1057 "Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
1058 e.expected.as_str(),
1059 e.got.as_str()
1060 ),
1061 ParserError(p) => write!(f, "{}", p),
1062 UnclosedRootNode => write!(f, "unclosed root node"),
1063 UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
1064 NodesLimitReached => write!(f, "nodes limit reached"),
1065 AttributesLimitReached => write!(f, "attributes limit reached"),
1066 NamespacesLimitReached => write!(f, "namespaces limit reached"),
1067 InvalidName(tp) => write!(f, "invalid name at {tp}"),
1068 NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
1069 InvalidChar(tp) => write!(f, "invalid char at {tp}"),
1070 InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
1071 InvalidString(tp) => write!(f, "invalid string at {tp}"),
1072 InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
1073 InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
1074 InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
1075 UnknownToken(tp) => write!(f, "unknown token at {tp}"),
1076 UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
1077 }
1078 }
1079}
1080
1081#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1089#[repr(C)]
1090pub struct ComponentId {
1091 pub collection: AzString,
1093 pub name: AzString,
1095}
1096
1097impl ComponentId {
1098 pub fn builtin(name: &str) -> Self {
1099 Self {
1100 collection: AzString::from_const_str("builtin"),
1101 name: AzString::from(name),
1102 }
1103 }
1104
1105 pub fn new(collection: &str, name: &str) -> Self {
1106 Self {
1107 collection: AzString::from(collection),
1108 name: AzString::from(name),
1109 }
1110 }
1111
1112 pub fn qualified_name(&self) -> String {
1114 format!("{}:{}", self.collection.as_str(), self.name.as_str())
1115 }
1116}
1117
1118#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1124#[repr(C)]
1125pub struct ComponentCallbackArg {
1126 pub name: AzString,
1128 pub arg_type: ComponentFieldType,
1130}
1131
1132impl_vec!(ComponentCallbackArg, ComponentCallbackArgVec, ComponentCallbackArgVecDestructor, ComponentCallbackArgVecDestructorType, ComponentCallbackArgVecSlice, OptionComponentCallbackArg);
1133impl_option!(ComponentCallbackArg, OptionComponentCallbackArg, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
1134impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
1135impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
1136impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
1137impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
1138impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
1139impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
1140impl_vec_clone!(ComponentCallbackArg, ComponentCallbackArgVec, ComponentCallbackArgVecDestructor);
1141
1142#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1144#[repr(C)]
1145pub struct ComponentCallbackSignature {
1146 pub return_type: AzString,
1148 pub args: ComponentCallbackArgVec,
1150}
1151
1152#[repr(C)]
1155pub struct ComponentFieldTypeBox {
1156 pub ptr: *mut ComponentFieldType,
1157}
1158
1159impl ComponentFieldTypeBox {
1160 pub fn new(t: ComponentFieldType) -> Self {
1161 Self { ptr: Box::into_raw(Box::new(t)) }
1162 }
1163
1164 pub fn as_ref(&self) -> &ComponentFieldType {
1165 unsafe { &*self.ptr }
1166 }
1167}
1168
1169impl Clone for ComponentFieldTypeBox {
1170 fn clone(&self) -> Self {
1171 Self::new(unsafe { (*self.ptr).clone() })
1172 }
1173}
1174
1175impl Drop for ComponentFieldTypeBox {
1176 fn drop(&mut self) {
1177 if !self.ptr.is_null() {
1178 unsafe { let _ = Box::from_raw(self.ptr); }
1179 }
1180 }
1181}
1182
1183impl fmt::Debug for ComponentFieldTypeBox {
1184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1185 if self.ptr.is_null() {
1186 write!(f, "ComponentFieldTypeBox(null)")
1187 } else {
1188 write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
1189 }
1190 }
1191}
1192
1193impl PartialEq for ComponentFieldTypeBox {
1194 fn eq(&self, other: &Self) -> bool {
1195 if self.ptr.is_null() && other.ptr.is_null() { return true; }
1196 if self.ptr.is_null() || other.ptr.is_null() { return false; }
1197 unsafe { *self.ptr == *other.ptr }
1198 }
1199}
1200
1201impl Eq for ComponentFieldTypeBox {}
1202
1203impl PartialOrd for ComponentFieldTypeBox {
1204 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1205 Some(self.cmp(other))
1206 }
1207}
1208
1209impl Ord for ComponentFieldTypeBox {
1210 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1211 match (self.ptr.is_null(), other.ptr.is_null()) {
1212 (true, true) => core::cmp::Ordering::Equal,
1213 (true, false) => core::cmp::Ordering::Less,
1214 (false, true) => core::cmp::Ordering::Greater,
1215 (false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
1216 }
1217 }
1218}
1219
1220impl core::hash::Hash for ComponentFieldTypeBox {
1221 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1222 if !self.ptr.is_null() {
1223 unsafe { (*self.ptr).hash(state); }
1224 }
1225 }
1226}
1227
1228#[repr(C)]
1231pub struct ComponentFieldValueBox {
1232 pub ptr: *mut ComponentFieldValue,
1233}
1234
1235impl ComponentFieldValueBox {
1236 pub fn new(v: ComponentFieldValue) -> Self {
1237 Self { ptr: Box::into_raw(Box::new(v)) }
1238 }
1239
1240 pub fn as_ref(&self) -> &ComponentFieldValue {
1241 unsafe { &*self.ptr }
1242 }
1243}
1244
1245impl Clone for ComponentFieldValueBox {
1246 fn clone(&self) -> Self {
1247 Self::new(unsafe { (*self.ptr).clone() })
1248 }
1249}
1250
1251impl Drop for ComponentFieldValueBox {
1252 fn drop(&mut self) {
1253 if !self.ptr.is_null() {
1254 unsafe { let _ = Box::from_raw(self.ptr); }
1255 }
1256 }
1257}
1258
1259impl fmt::Debug for ComponentFieldValueBox {
1260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1261 if self.ptr.is_null() {
1262 write!(f, "ComponentFieldValueBox(null)")
1263 } else {
1264 write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
1265 }
1266 }
1267}
1268
1269impl PartialEq for ComponentFieldValueBox {
1270 fn eq(&self, other: &Self) -> bool {
1271 if self.ptr.is_null() && other.ptr.is_null() { return true; }
1272 if self.ptr.is_null() || other.ptr.is_null() { return false; }
1273 unsafe { *self.ptr == *other.ptr }
1274 }
1275}
1276
1277#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1281#[repr(C, u8)]
1282pub enum ComponentFieldType {
1283 String,
1284 Bool,
1285 I32,
1286 I64,
1287 U32,
1288 U64,
1289 Usize,
1290 F32,
1291 F64,
1292 ColorU,
1293 CssProperty,
1294 ImageRef,
1295 FontRef,
1296 StyledDom,
1298 Callback(ComponentCallbackSignature),
1300 RefAny(AzString),
1302 OptionType(ComponentFieldTypeBox),
1304 VecType(ComponentFieldTypeBox),
1306 StructRef(AzString),
1308 EnumRef(AzString),
1310}
1311
1312impl ComponentFieldType {
1313 pub fn parse(s: &str) -> Option<Self> {
1317 let s = s.trim();
1318 match s {
1319 "String" | "string" => return Some(ComponentFieldType::String),
1320 "Bool" | "bool" => return Some(ComponentFieldType::Bool),
1321 "I32" | "i32" => return Some(ComponentFieldType::I32),
1322 "I64" | "i64" => return Some(ComponentFieldType::I64),
1323 "U32" | "u32" => return Some(ComponentFieldType::U32),
1324 "U64" | "u64" => return Some(ComponentFieldType::U64),
1325 "Usize" | "usize" => return Some(ComponentFieldType::Usize),
1326 "F32" | "f32" => return Some(ComponentFieldType::F32),
1327 "F64" | "f64" => return Some(ComponentFieldType::F64),
1328 "ColorU" => return Some(ComponentFieldType::ColorU),
1329 "CssProperty" => return Some(ComponentFieldType::CssProperty),
1330 "ImageRef" => return Some(ComponentFieldType::ImageRef),
1331 "FontRef" => return Some(ComponentFieldType::FontRef),
1332 "StyledDom" => return Some(ComponentFieldType::StyledDom),
1333 "RefAny" => return Some(ComponentFieldType::RefAny(AzString::from(""))),
1334 _ => {}
1335 }
1336
1337 if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
1339 let inner_type = ComponentFieldType::parse(inner)?;
1340 return Some(ComponentFieldType::OptionType(ComponentFieldTypeBox::new(inner_type)));
1341 }
1342
1343 if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
1345 let inner_type = ComponentFieldType::parse(inner)?;
1346 return Some(ComponentFieldType::VecType(ComponentFieldTypeBox::new(inner_type)));
1347 }
1348
1349 if let Some(sig) = s.strip_prefix("Callback(").and_then(|r| r.strip_suffix(')')) {
1351 return Some(ComponentFieldType::Callback(ComponentCallbackSignature {
1352 return_type: AzString::from(sig),
1353 args: Vec::new().into(),
1354 }));
1355 }
1356
1357 if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
1359 return Some(ComponentFieldType::RefAny(AzString::from(hint)));
1360 }
1361
1362 if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
1364 return Some(ComponentFieldType::EnumRef(AzString::from(name)));
1365 }
1366
1367 if let Some(name) = s.strip_prefix("StructRef(").and_then(|r| r.strip_suffix(')')) {
1369 return Some(ComponentFieldType::StructRef(AzString::from(name)));
1370 }
1371
1372 if s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
1374 return Some(ComponentFieldType::StructRef(AzString::from(s)));
1375 }
1376
1377 None
1378 }
1379
1380 pub fn format(&self) -> String {
1383 match self {
1384 ComponentFieldType::String => "String".to_string(),
1385 ComponentFieldType::Bool => "Bool".to_string(),
1386 ComponentFieldType::I32 => "I32".to_string(),
1387 ComponentFieldType::I64 => "I64".to_string(),
1388 ComponentFieldType::U32 => "U32".to_string(),
1389 ComponentFieldType::U64 => "U64".to_string(),
1390 ComponentFieldType::Usize => "Usize".to_string(),
1391 ComponentFieldType::F32 => "F32".to_string(),
1392 ComponentFieldType::F64 => "F64".to_string(),
1393 ComponentFieldType::ColorU => "ColorU".to_string(),
1394 ComponentFieldType::CssProperty => "CssProperty".to_string(),
1395 ComponentFieldType::ImageRef => "ImageRef".to_string(),
1396 ComponentFieldType::FontRef => "FontRef".to_string(),
1397 ComponentFieldType::StyledDom => "StyledDom".to_string(),
1398 ComponentFieldType::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
1399 ComponentFieldType::RefAny(hint) => {
1400 if hint.as_str().is_empty() {
1401 "RefAny".to_string()
1402 } else {
1403 format!("RefAny({})", hint.as_str())
1404 }
1405 }
1406 ComponentFieldType::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
1407 ComponentFieldType::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
1408 ComponentFieldType::StructRef(name) => name.as_str().to_string(),
1409 ComponentFieldType::EnumRef(name) => name.as_str().to_string(),
1410 }
1411 }
1412}
1413
1414impl core::fmt::Display for ComponentFieldType {
1415 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1416 f.write_str(&self.format())
1417 }
1418}
1419
1420#[derive(Debug, Clone, PartialEq)]
1422#[repr(C)]
1423pub struct ComponentEnumVariant {
1424 pub name: AzString,
1426 pub description: AzString,
1428 pub fields: ComponentDataFieldVec,
1430}
1431
1432impl_vec!(ComponentEnumVariant, ComponentEnumVariantVec, ComponentEnumVariantVecDestructor, ComponentEnumVariantVecDestructorType, ComponentEnumVariantVecSlice, OptionComponentEnumVariant);
1433impl_option!(ComponentEnumVariant, OptionComponentEnumVariant, copy = false, [Debug, Clone, PartialEq]);
1434impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
1435impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
1436impl_vec_clone!(ComponentEnumVariant, ComponentEnumVariantVec, ComponentEnumVariantVecDestructor);
1437
1438#[derive(Debug, Clone, PartialEq)]
1441#[repr(C)]
1442pub struct ComponentEnumModel {
1443 pub name: AzString,
1445 pub description: AzString,
1447 pub variants: ComponentEnumVariantVec,
1449}
1450
1451impl_vec!(ComponentEnumModel, ComponentEnumModelVec, ComponentEnumModelVecDestructor, ComponentEnumModelVecDestructorType, ComponentEnumModelVecSlice, OptionComponentEnumModel);
1452impl_option!(ComponentEnumModel, OptionComponentEnumModel, copy = false, [Debug, Clone, PartialEq]);
1453impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
1454impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
1455impl_vec_clone!(ComponentEnumModel, ComponentEnumModelVec, ComponentEnumModelVecDestructor);
1456
1457#[derive(Debug, Clone, PartialEq)]
1459#[repr(C, u8)]
1460pub enum ComponentDefaultValue {
1461 None,
1463 String(AzString),
1465 Bool(bool),
1467 I32(i32),
1469 I64(i64),
1471 U32(u32),
1473 U64(u64),
1475 Usize(usize),
1477 F32(f32),
1479 F64(f64),
1481 ColorU(ColorU),
1483 ComponentInstance(ComponentInstanceDefault),
1485 CallbackFnPointer(AzString),
1487 Json(AzString),
1489}
1490
1491impl_option!(ComponentDefaultValue, OptionComponentDefaultValue, copy = false, [Debug, Clone, PartialEq]);
1492
1493#[derive(Debug, Clone, PartialEq)]
1495#[repr(C)]
1496pub struct ComponentInstanceDefault {
1497 pub library: AzString,
1499 pub component: AzString,
1501 pub field_overrides: ComponentFieldOverrideVec,
1503}
1504
1505#[derive(Debug, Clone, PartialEq)]
1507#[repr(C)]
1508pub struct ComponentFieldOverride {
1509 pub field_name: AzString,
1511 pub source: ComponentFieldValueSource,
1513}
1514
1515impl_vec!(ComponentFieldOverride, ComponentFieldOverrideVec, ComponentFieldOverrideVecDestructor, ComponentFieldOverrideVecDestructorType, ComponentFieldOverrideVecSlice, OptionComponentFieldOverride);
1516impl_option!(ComponentFieldOverride, OptionComponentFieldOverride, copy = false, [Debug, Clone, PartialEq]);
1517impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
1518impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
1519impl_vec_clone!(ComponentFieldOverride, ComponentFieldOverrideVec, ComponentFieldOverrideVecDestructor);
1520
1521#[derive(Debug, Clone, PartialEq)]
1523#[repr(C, u8)]
1524pub enum ComponentFieldValueSource {
1525 Default,
1527 Literal(AzString),
1529 Binding(AzString),
1531}
1532
1533#[derive(Debug, Clone, PartialEq)]
1536#[repr(C, u8)]
1537pub enum ComponentFieldValue {
1538 String(AzString),
1539 Bool(bool),
1540 I32(i32),
1541 I64(i64),
1542 U32(u32),
1543 U64(u64),
1544 Usize(usize),
1545 F32(f32),
1546 F64(f64),
1547 ColorU(ColorU),
1548 None,
1550 Some(ComponentFieldValueBox),
1552 Vec(ComponentFieldValueVec),
1554 StyledDom(StyledDom),
1556 Struct(ComponentFieldNamedValueVec),
1558 Enum { variant: AzString, fields: ComponentFieldNamedValueVec },
1560 Callback(AzString),
1562 RefAny(crate::refany::RefAny),
1564}
1565
1566#[derive(Debug, Clone, PartialEq)]
1568#[repr(C)]
1569pub struct ComponentFieldNamedValue {
1570 pub name: AzString,
1571 pub value: ComponentFieldValue,
1572}
1573
1574impl_vec!(ComponentFieldNamedValue, ComponentFieldNamedValueVec, ComponentFieldNamedValueVecDestructor, ComponentFieldNamedValueVecDestructorType, ComponentFieldNamedValueVecSlice, OptionComponentFieldNamedValue);
1575impl_option!(ComponentFieldNamedValue, OptionComponentFieldNamedValue, copy = false, [Debug, Clone, PartialEq]);
1576impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1577impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
1578impl_vec_clone!(ComponentFieldNamedValue, ComponentFieldNamedValueVec, ComponentFieldNamedValueVecDestructor);
1579
1580impl ComponentFieldNamedValueVec {
1581 pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
1583 self.as_ref().iter().find_map(|v| {
1584 if v.name.as_str() == name { Some(&v.value) } else { None }
1585 })
1586 }
1587
1588 pub fn get_string(&self, name: &str) -> Option<&AzString> {
1590 match self.get_field(name) {
1591 Some(ComponentFieldValue::String(s)) => Some(s),
1592 _ => None,
1593 }
1594 }
1595}
1596
1597impl_vec!(ComponentFieldValue, ComponentFieldValueVec, ComponentFieldValueVecDestructor, ComponentFieldValueVecDestructorType, ComponentFieldValueVecSlice, OptionComponentFieldValue);
1598impl_option!(ComponentFieldValue, OptionComponentFieldValue, copy = false, [Debug, Clone, PartialEq]);
1599impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
1600impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
1601impl_vec_clone!(ComponentFieldValue, ComponentFieldValueVec, ComponentFieldValueVecDestructor);
1602
1603#[derive(Debug, Clone, PartialEq)]
1605#[repr(C)]
1606pub struct ComponentDataField {
1607 pub name: AzString,
1609 pub field_type: ComponentFieldType,
1611 pub default_value: OptionComponentDefaultValue,
1613 pub required: bool,
1615 pub description: AzString,
1617}
1618
1619impl_vec!(ComponentDataField, ComponentDataFieldVec, ComponentDataFieldVecDestructor, ComponentDataFieldVecDestructorType, ComponentDataFieldVecSlice, OptionComponentDataField);
1620impl_option!(ComponentDataField, OptionComponentDataField, copy = false, [Debug, Clone, PartialEq]);
1621impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
1622impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
1623impl_vec_clone!(ComponentDataField, ComponentDataFieldVec, ComponentDataFieldVecDestructor);
1624
1625#[derive(Debug, Clone)]
1632#[repr(C)]
1633pub struct ComponentDataModel {
1634 pub name: AzString,
1636 pub description: AzString,
1638 pub fields: ComponentDataFieldVec,
1640}
1641
1642impl ComponentDataModel {
1643 pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
1645 self.fields.as_ref().iter().find(|f| f.name.as_str() == name)
1646 }
1647
1648 pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
1650 self.get_field(name).and_then(|f| {
1651 match &f.default_value {
1652 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
1653 _ => None,
1654 }
1655 })
1656 }
1657
1658 pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
1661 let mut fields_vec = core::mem::replace(
1662 &mut self.fields,
1663 ComponentDataFieldVec::from_const_slice(&[]),
1664 ).into_library_owned_vec();
1665 for f in fields_vec.iter_mut() {
1666 if f.name.as_str() == name {
1667 f.default_value = OptionComponentDefaultValue::Some(value);
1668 break;
1669 }
1670 }
1671 self.fields = ComponentDataFieldVec::from_vec(fields_vec);
1672 self
1673 }
1674}
1675
1676impl_vec!(ComponentDataModel, ComponentDataModelVec, ComponentDataModelVecDestructor, ComponentDataModelVecDestructorType, ComponentDataModelVecSlice, OptionComponentDataModel);
1677impl_option!(ComponentDataModel, OptionComponentDataModel, copy = false, [Debug, Clone]);
1678impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
1679impl_vec_clone!(ComponentDataModel, ComponentDataModelVec, ComponentDataModelVecDestructor);
1680impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
1681
1682#[cfg(feature = "serde-json")]
1687mod serde_impl {
1688 use super::*;
1689 use serde::{Serialize, Serializer, Deserialize, Deserializer};
1690 use serde::ser::SerializeStruct;
1691
1692 fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
1695 serializer.serialize_str(s.as_str())
1696 }
1697
1698 fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
1699 let s = alloc::string::String::deserialize(deserializer)?;
1700 Ok(AzString::from(s.as_str()))
1701 }
1702
1703 impl Serialize for ComponentFieldType {
1706 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1707 serializer.serialize_str(&field_type_to_string(self))
1708 }
1709 }
1710
1711 impl<'de> Deserialize<'de> for ComponentFieldType {
1712 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1713 let s = alloc::string::String::deserialize(deserializer)?;
1714 Ok(string_to_field_type(&s))
1715 }
1716 }
1717
1718 fn field_type_to_string(ft: &ComponentFieldType) -> alloc::string::String {
1719 match ft {
1720 ComponentFieldType::String => "String".into(),
1721 ComponentFieldType::Bool => "bool".into(),
1722 ComponentFieldType::I32 => "i32".into(),
1723 ComponentFieldType::I64 => "i64".into(),
1724 ComponentFieldType::U32 => "u32".into(),
1725 ComponentFieldType::U64 => "u64".into(),
1726 ComponentFieldType::Usize => "usize".into(),
1727 ComponentFieldType::F32 => "f32".into(),
1728 ComponentFieldType::F64 => "f64".into(),
1729 ComponentFieldType::ColorU => "ColorU".into(),
1730 ComponentFieldType::CssProperty => "CssProperty".into(),
1731 ComponentFieldType::ImageRef => "ImageRef".into(),
1732 ComponentFieldType::FontRef => "FontRef".into(),
1733 ComponentFieldType::StyledDom => "Dom".into(),
1734 ComponentFieldType::Callback(sig) => alloc::format!(
1735 "Callback({})",
1736 sig.return_type.as_str()
1737 ),
1738 ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
1739 ComponentFieldType::OptionType(inner) => alloc::format!(
1740 "Option<{}>",
1741 field_type_to_string(inner.as_ref())
1742 ),
1743 ComponentFieldType::VecType(inner) => alloc::format!(
1744 "Vec<{}>",
1745 field_type_to_string(inner.as_ref())
1746 ),
1747 ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
1748 ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
1749 }
1750 }
1751
1752 fn string_to_field_type(s: &str) -> ComponentFieldType {
1753 match s {
1754 "String" | "string" => ComponentFieldType::String,
1755 "bool" | "Bool" => ComponentFieldType::Bool,
1756 "i32" | "I32" => ComponentFieldType::I32,
1757 "i64" | "I64" => ComponentFieldType::I64,
1758 "u32" | "U32" => ComponentFieldType::U32,
1759 "u64" | "U64" => ComponentFieldType::U64,
1760 "usize" | "Usize" => ComponentFieldType::Usize,
1761 "f32" | "F32" => ComponentFieldType::F32,
1762 "f64" | "F64" => ComponentFieldType::F64,
1763 "ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
1764 "CssProperty" => ComponentFieldType::CssProperty,
1765 "ImageRef" | "Image" => ComponentFieldType::ImageRef,
1766 "FontRef" | "Font" => ComponentFieldType::FontRef,
1767 "Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
1768 other => {
1769 if let Some(inner) = other.strip_prefix("Option<").and_then(|s| s.strip_suffix('>')) {
1770 ComponentFieldType::OptionType(ComponentFieldTypeBox::new(string_to_field_type(inner)))
1771 } else if let Some(inner) = other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
1772 ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(inner)))
1773 } else if let Some(name) = other.strip_prefix("struct:") {
1774 ComponentFieldType::StructRef(AzString::from(name))
1775 } else if let Some(name) = other.strip_prefix("enum:") {
1776 ComponentFieldType::EnumRef(AzString::from(name))
1777 } else if other.starts_with("Callback") {
1778 let ret = other.strip_prefix("Callback(")
1779 .and_then(|s| s.strip_suffix(')'))
1780 .unwrap_or("()");
1781 ComponentFieldType::Callback(ComponentCallbackSignature {
1782 return_type: AzString::from(ret),
1783 args: ComponentCallbackArgVec::from_const_slice(&[]),
1784 })
1785 } else if other.starts_with("RefAny") {
1786 let hint = other.strip_prefix("RefAny(")
1787 .and_then(|s| s.strip_suffix(')'))
1788 .unwrap_or("");
1789 ComponentFieldType::RefAny(AzString::from(hint))
1790 } else {
1791 ComponentFieldType::String }
1793 }
1794 }
1795 }
1796
1797 impl Serialize for ComponentDefaultValue {
1800 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1801 use serde::ser::SerializeMap;
1802 match self {
1803 ComponentDefaultValue::None => serializer.serialize_none(),
1804 ComponentDefaultValue::String(s) => serializer.serialize_str(s.as_str()),
1805 ComponentDefaultValue::Bool(b) => serializer.serialize_bool(*b),
1806 ComponentDefaultValue::I32(v) => serializer.serialize_i32(*v),
1807 ComponentDefaultValue::I64(v) => serializer.serialize_i64(*v),
1808 ComponentDefaultValue::U32(v) => serializer.serialize_u32(*v),
1809 ComponentDefaultValue::U64(v) => serializer.serialize_u64(*v),
1810 ComponentDefaultValue::Usize(v) => serializer.serialize_u64(*v as u64),
1811 ComponentDefaultValue::F32(v) => serializer.serialize_f32(*v),
1812 ComponentDefaultValue::F64(v) => serializer.serialize_f64(*v),
1813 ComponentDefaultValue::ColorU(c) => {
1814 serializer.serialize_str(&alloc::format!("#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a))
1815 }
1816 ComponentDefaultValue::ComponentInstance(ci) => {
1817 let mut map = serializer.serialize_map(Some(2))?;
1818 map.serialize_entry("library", ci.library.as_str())?;
1819 map.serialize_entry("component", ci.component.as_str())?;
1820 map.end()
1821 }
1822 ComponentDefaultValue::CallbackFnPointer(name) => {
1823 serializer.serialize_str(name.as_str())
1824 }
1825 ComponentDefaultValue::Json(json_str) => {
1826 match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
1828 Ok(v) => v.serialize(serializer),
1829 Err(_) => serializer.serialize_str(json_str.as_str()),
1830 }
1831 }
1832 }
1833 }
1834 }
1835
1836 impl<'de> Deserialize<'de> for ComponentDefaultValue {
1837 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1838 let val = serde_json::Value::deserialize(deserializer)?;
1839 Ok(match val {
1840 serde_json::Value::Null => ComponentDefaultValue::None,
1841 serde_json::Value::Bool(b) => ComponentDefaultValue::Bool(b),
1842 serde_json::Value::Number(n) => {
1843 if let Some(i) = n.as_i64() {
1844 if let Ok(v) = i32::try_from(i) {
1845 ComponentDefaultValue::I32(v)
1846 } else {
1847 ComponentDefaultValue::I64(i)
1848 }
1849 } else if let Some(f) = n.as_f64() {
1850 ComponentDefaultValue::F64(f)
1851 } else {
1852 ComponentDefaultValue::None
1853 }
1854 }
1855 serde_json::Value::String(s) => {
1856 ComponentDefaultValue::String(AzString::from(s.as_str()))
1857 }
1858 _ => ComponentDefaultValue::None,
1859 })
1860 }
1861 }
1862
1863 impl Serialize for OptionComponentDefaultValue {
1866 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1867 match self {
1868 OptionComponentDefaultValue::Some(v) => v.serialize(serializer),
1869 OptionComponentDefaultValue::None => serializer.serialize_none(),
1870 }
1871 }
1872 }
1873
1874 impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
1875 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1876 let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
1877 Ok(match val {
1878 Some(v) => OptionComponentDefaultValue::Some(v),
1879 None => OptionComponentDefaultValue::None,
1880 })
1881 }
1882 }
1883
1884 impl Serialize for ComponentDataField {
1887 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1888 let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
1889 s.serialize_field("name", self.name.as_str())?;
1890 s.serialize_field("type", &self.field_type)?;
1891 s.serialize_field("default", &self.default_value)?;
1892 s.serialize_field("required", &self.required)?;
1893 s.serialize_field("description", self.description.as_str())?;
1894 s.end()
1895 }
1896 }
1897
1898 impl<'de> Deserialize<'de> for ComponentDataField {
1899 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1900 #[derive(Deserialize)]
1901 struct Helper {
1902 name: alloc::string::String,
1903 #[serde(rename = "type", default = "default_type")]
1904 field_type: ComponentFieldType,
1905 #[serde(default)]
1906 default: OptionComponentDefaultValue,
1907 #[serde(default)]
1908 required: bool,
1909 #[serde(default)]
1910 description: alloc::string::String,
1911 }
1912 fn default_type() -> ComponentFieldType { ComponentFieldType::String }
1913
1914 let h = Helper::deserialize(deserializer)?;
1915 Ok(ComponentDataField {
1916 name: AzString::from(h.name.as_str()),
1917 field_type: h.field_type,
1918 default_value: h.default,
1919 required: h.required,
1920 description: AzString::from(h.description.as_str()),
1921 })
1922 }
1923 }
1924
1925 impl Serialize for ComponentDataModel {
1928 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1929 let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
1930 s.serialize_field("name", self.name.as_str())?;
1931 s.serialize_field("description", self.description.as_str())?;
1932 let fields: alloc::vec::Vec<&ComponentDataField> = self.fields.as_ref().iter().collect();
1933 s.serialize_field("fields", &fields)?;
1934 s.end()
1935 }
1936 }
1937
1938 impl<'de> Deserialize<'de> for ComponentDataModel {
1939 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1940 #[derive(Deserialize)]
1941 struct Helper {
1942 #[serde(default)]
1943 name: alloc::string::String,
1944 #[serde(default)]
1945 description: alloc::string::String,
1946 #[serde(default)]
1947 fields: alloc::vec::Vec<ComponentDataField>,
1948 }
1949
1950 let h = Helper::deserialize(deserializer)?;
1951 Ok(ComponentDataModel {
1952 name: AzString::from(h.name.as_str()),
1953 description: AzString::from(h.description.as_str()),
1954 fields: ComponentDataFieldVec::from_vec(h.fields),
1955 })
1956 }
1957 }
1958}
1959
1960#[cfg(feature = "serde-json")]
1962pub use serde_impl::*;
1963
1964#[cfg(feature = "serde-json")]
1965impl ComponentDataModel {
1966 pub fn to_json(&self) -> Result<alloc::string::String, alloc::string::String> {
1968 serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{}", e))
1969 }
1970
1971 pub fn from_json(json: &str) -> Result<Self, alloc::string::String> {
1973 serde_json::from_str(json).map_err(|e| alloc::format!("{}", e))
1974 }
1975}
1976
1977#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1979#[repr(C)]
1980pub enum ComponentSource {
1981 Builtin,
1983 Compiled,
1985 UserDefined,
1987}
1988
1989impl Default for ComponentSource {
1990 fn default() -> Self {
1991 ComponentSource::UserDefined
1992 }
1993}
1994
1995impl ComponentSource {
1996 pub fn create() -> Self {
1997 Self::default()
1998 }
1999}
2000
2001#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2003#[repr(C)]
2004pub enum CompileTarget {
2005 Rust,
2006 C,
2007 Cpp,
2008 Python,
2009}
2010
2011impl_result!(
2012 StyledDom,
2013 RenderDomError,
2014 ResultStyledDomRenderDomError,
2015 copy = false,
2016 [Debug, Clone, PartialEq]
2017);
2018
2019impl_result!(
2020 AzString,
2021 CompileError,
2022 ResultStringCompileError,
2023 copy = false,
2024 [Debug, Clone, PartialEq]
2025);
2026
2027pub type ComponentRenderFn = fn(
2034 &ComponentDef,
2035 &ComponentDataModel,
2036 &ComponentMap,
2037) -> ResultStyledDomRenderDomError;
2038
2039pub type ComponentCompileFn = fn(
2041 &ComponentDef,
2042 &CompileTarget,
2043 &ComponentDataModel,
2044 indent: usize,
2045) -> ResultStringCompileError;
2046
2047pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
2050
2051#[repr(C)]
2059pub struct RegisterComponentFn {
2060 pub cb: RegisterComponentFnType,
2061 pub ctx: crate::refany::OptionRefAny,
2064}
2065
2066impl_callback!(RegisterComponentFn, RegisterComponentFnType);
2067
2068pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
2071
2072#[repr(C)]
2080pub struct RegisterComponentLibraryFn {
2081 pub cb: RegisterComponentLibraryFnType,
2082 pub ctx: crate::refany::OptionRefAny,
2085}
2086
2087impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
2088
2089#[derive(Clone)]
2093#[repr(C)]
2094pub struct ComponentDef {
2095 pub id: ComponentId,
2097 pub display_name: AzString,
2099 pub description: AzString,
2101 pub css: AzString,
2103 pub source: ComponentSource,
2105 pub data_model: ComponentDataModel,
2111 pub render_fn: ComponentRenderFn,
2113 pub compile_fn: ComponentCompileFn,
2115 pub render_fn_source: OptionString,
2117 pub compile_fn_source: OptionString,
2119}
2120
2121impl fmt::Debug for ComponentDef {
2122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2123 f.debug_struct("ComponentDef")
2124 .field("id", &self.id)
2125 .field("display_name", &self.display_name)
2126 .field("source", &self.source)
2127 .field("data_model", &self.data_model.name)
2128 .finish()
2129 }
2130}
2131
2132impl_vec!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor, ComponentDefVecDestructorType, ComponentDefVecSlice, OptionComponentDef);
2133impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
2134impl_vec_debug!(ComponentDef, ComponentDefVec);
2135impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
2136impl_vec_mut!(ComponentDef, ComponentDefVec);
2137
2138#[derive(Debug, Clone)]
2140#[repr(C)]
2141pub struct ComponentLibrary {
2142 pub name: AzString,
2144 pub version: AzString,
2146 pub description: AzString,
2148 pub components: ComponentDefVec,
2150 pub exportable: bool,
2152 pub modifiable: bool,
2155 pub data_models: ComponentDataModelVec,
2158 pub enum_models: ComponentEnumModelVec,
2161}
2162
2163impl_vec!(ComponentLibrary, ComponentLibraryVec, ComponentLibraryVecDestructor, ComponentLibraryVecDestructorType, ComponentLibraryVecSlice, OptionComponentLibrary);
2164impl_option!(ComponentLibrary, OptionComponentLibrary, copy = false, [Debug, Clone]);
2165impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
2166impl_vec_clone!(ComponentLibrary, ComponentLibraryVec, ComponentLibraryVecDestructor);
2167impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
2168
2169#[derive(Debug, Clone)]
2171#[repr(C)]
2172pub struct ComponentMap {
2173 pub libraries: ComponentLibraryVec,
2175}
2176
2177impl ComponentMap {
2178 pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
2180 self.libraries
2181 .iter()
2182 .find(|lib| lib.name.as_str() == collection)
2183 .and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
2184 }
2185
2186 pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
2188 self.get("builtin", name)
2189 }
2190
2191 pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
2193 if let Some((collection, name)) = qualified.split_once(':') {
2194 self.get(collection, name)
2195 } else {
2196 self.get_unqualified(qualified)
2197 }
2198 }
2199
2200 pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
2202 self.libraries.iter().filter(|lib| lib.exportable).collect()
2203 }
2204
2205 pub fn all_components(&self) -> Vec<&ComponentDef> {
2207 self.libraries.iter().flat_map(|lib| lib.components.iter()).collect()
2208 }
2209
2210}
2211
2212pub fn tag_to_node_type(tag: &str) -> NodeType {
2219 match tag {
2220 "html" => NodeType::Html,
2222 "head" => NodeType::Head,
2223 "title" => NodeType::Title,
2224 "body" => NodeType::Body,
2225 "div" => NodeType::Div,
2227 "header" => NodeType::Header,
2228 "footer" => NodeType::Footer,
2229 "section" => NodeType::Section,
2230 "article" => NodeType::Article,
2231 "aside" => NodeType::Aside,
2232 "nav" => NodeType::Nav,
2233 "main" => NodeType::Main,
2234 "figure" => NodeType::Figure,
2235 "figcaption" => NodeType::FigCaption,
2236 "address" => NodeType::Address,
2237 "details" => NodeType::Details,
2238 "summary" => NodeType::Summary,
2239 "dialog" => NodeType::Dialog,
2240 "h1" => NodeType::H1,
2242 "h2" => NodeType::H2,
2243 "h3" => NodeType::H3,
2244 "h4" => NodeType::H4,
2245 "h5" => NodeType::H5,
2246 "h6" => NodeType::H6,
2247 "p" => NodeType::P,
2249 "span" => NodeType::Span,
2250 "pre" => NodeType::Pre,
2251 "code" => NodeType::Code,
2252 "blockquote" => NodeType::BlockQuote,
2253 "br" => NodeType::Br,
2254 "hr" => NodeType::Hr,
2255 "ul" => NodeType::Ul,
2257 "ol" => NodeType::Ol,
2258 "li" => NodeType::Li,
2259 "dl" => NodeType::Dl,
2260 "dt" => NodeType::Dt,
2261 "dd" => NodeType::Dd,
2262 "menu" => NodeType::Menu,
2263 "menuitem" => NodeType::MenuItem,
2264 "dir" => NodeType::Dir,
2265 "table" => NodeType::Table,
2267 "caption" => NodeType::Caption,
2268 "thead" => NodeType::THead,
2269 "tbody" => NodeType::TBody,
2270 "tfoot" => NodeType::TFoot,
2271 "tr" => NodeType::Tr,
2272 "th" => NodeType::Th,
2273 "td" => NodeType::Td,
2274 "colgroup" => NodeType::ColGroup,
2275 "col" => NodeType::Col,
2276 "form" => NodeType::Form,
2278 "fieldset" => NodeType::FieldSet,
2279 "legend" => NodeType::Legend,
2280 "label" => NodeType::Label,
2281 "input" => NodeType::Input,
2282 "button" => NodeType::Button,
2283 "select" => NodeType::Select,
2284 "optgroup" => NodeType::OptGroup,
2285 "option" => NodeType::SelectOption,
2286 "textarea" => NodeType::TextArea,
2287 "output" => NodeType::Output,
2288 "progress" => NodeType::Progress,
2289 "meter" => NodeType::Meter,
2290 "datalist" => NodeType::DataList,
2291 "a" => NodeType::A,
2293 "strong" => NodeType::Strong,
2294 "em" => NodeType::Em,
2295 "b" => NodeType::B,
2296 "i" => NodeType::I,
2297 "u" => NodeType::U,
2298 "s" => NodeType::S,
2299 "small" => NodeType::Small,
2300 "mark" => NodeType::Mark,
2301 "del" => NodeType::Del,
2302 "ins" => NodeType::Ins,
2303 "samp" => NodeType::Samp,
2304 "kbd" => NodeType::Kbd,
2305 "var" => NodeType::Var,
2306 "cite" => NodeType::Cite,
2307 "dfn" => NodeType::Dfn,
2308 "abbr" => NodeType::Abbr,
2309 "acronym" => NodeType::Acronym,
2310 "q" => NodeType::Q,
2311 "time" => NodeType::Time,
2312 "sub" => NodeType::Sub,
2313 "sup" => NodeType::Sup,
2314 "big" => NodeType::Big,
2315 "bdo" => NodeType::Bdo,
2316 "bdi" => NodeType::Bdi,
2317 "wbr" => NodeType::Wbr,
2318 "ruby" => NodeType::Ruby,
2319 "rt" => NodeType::Rt,
2320 "rtc" => NodeType::Rtc,
2321 "rp" => NodeType::Rp,
2322 "data" => NodeType::Data,
2323 "canvas" => NodeType::Canvas,
2325 "object" => NodeType::Object,
2326 "param" => NodeType::Param,
2327 "embed" => NodeType::Embed,
2328 "audio" => NodeType::Audio,
2329 "video" => NodeType::Video,
2330 "source" => NodeType::Source,
2331 "track" => NodeType::Track,
2332 "map" => NodeType::Map,
2333 "area" => NodeType::Area,
2334 "svg" => NodeType::Svg,
2336 "g" => NodeType::SvgG,
2337 "defs" => NodeType::SvgDefs,
2338 "symbol" => NodeType::SvgSymbol,
2339 "use" => NodeType::SvgUse,
2340 "switch" => NodeType::SvgSwitch,
2341 "path" => NodeType::SvgPath,
2342 "circle" => NodeType::SvgCircle,
2343 "rect" => NodeType::SvgRect,
2344 "ellipse" => NodeType::SvgEllipse,
2345 "line" => NodeType::SvgLine,
2346 "polygon" => NodeType::SvgPolygon,
2347 "polyline" => NodeType::SvgPolyline,
2348 "tspan" => NodeType::SvgTspan,
2349 "textpath" => NodeType::SvgTextPath,
2350 "lineargradient" => NodeType::SvgLinearGradient,
2351 "radialgradient" => NodeType::SvgRadialGradient,
2352 "stop" => NodeType::SvgStop,
2353 "pattern" => NodeType::SvgPattern,
2354 "clippath" => NodeType::SvgClipPathElement,
2355 "mask" => NodeType::SvgMask,
2356 "filter" => NodeType::SvgFilter,
2357 "feblend" => NodeType::SvgFeBlend,
2358 "fecolormatrix" => NodeType::SvgFeColorMatrix,
2359 "fecomponenttransfer" => NodeType::SvgFeComponentTransfer,
2360 "fecomposite" => NodeType::SvgFeComposite,
2361 "feconvolvematrix" => NodeType::SvgFeConvolveMatrix,
2362 "fediffuselighting" => NodeType::SvgFeDiffuseLighting,
2363 "fedisplacementmap" => NodeType::SvgFeDisplacementMap,
2364 "fedistantlight" => NodeType::SvgFeDistantLight,
2365 "fedropshadow" => NodeType::SvgFeDropShadow,
2366 "feflood" => NodeType::SvgFeFlood,
2367 "fefuncr" => NodeType::SvgFeFuncR,
2368 "fefuncg" => NodeType::SvgFeFuncG,
2369 "fefuncb" => NodeType::SvgFeFuncB,
2370 "fefunca" => NodeType::SvgFeFuncA,
2371 "fegaussianblur" => NodeType::SvgFeGaussianBlur,
2372 "feimage" => NodeType::SvgFeImage,
2373 "femerge" => NodeType::SvgFeMerge,
2374 "femergenode" => NodeType::SvgFeMergeNode,
2375 "femorphology" => NodeType::SvgFeMorphology,
2376 "feoffset" => NodeType::SvgFeOffset,
2377 "fepointlight" => NodeType::SvgFePointLight,
2378 "fespecularlighting" => NodeType::SvgFeSpecularLighting,
2379 "fespotlight" => NodeType::SvgFeSpotLight,
2380 "fetile" => NodeType::SvgFeTile,
2381 "feturbulence" => NodeType::SvgFeTurbulence,
2382 "foreignobject" => NodeType::SvgForeignObject,
2383 "desc" => NodeType::SvgDesc,
2384 "view" => NodeType::SvgView,
2385 "animate" => NodeType::SvgAnimate,
2386 "animatemotion" => NodeType::SvgAnimateMotion,
2387 "animatetransform" => NodeType::SvgAnimateTransform,
2388 "set" => NodeType::SvgSet,
2389 "mpath" => NodeType::SvgMpath,
2390 "meta" => NodeType::Meta,
2392 "link" => NodeType::Link,
2393 "script" => NodeType::Script,
2394 "style" => NodeType::Style,
2395 "base" => NodeType::Base,
2396 _ => NodeType::Div,
2397 }
2398}
2399
2400fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
2403 match tag {
2404 "html" => NodeTypeTag::Html,
2406 "head" => NodeTypeTag::Head,
2407 "title" => NodeTypeTag::Title,
2408 "body" => NodeTypeTag::Body,
2409 "div" => NodeTypeTag::Div,
2411 "header" => NodeTypeTag::Header,
2412 "footer" => NodeTypeTag::Footer,
2413 "section" => NodeTypeTag::Section,
2414 "article" => NodeTypeTag::Article,
2415 "aside" => NodeTypeTag::Aside,
2416 "nav" => NodeTypeTag::Nav,
2417 "main" => NodeTypeTag::Main,
2418 "figure" => NodeTypeTag::Figure,
2419 "figcaption" => NodeTypeTag::FigCaption,
2420 "address" => NodeTypeTag::Address,
2421 "details" => NodeTypeTag::Details,
2422 "summary" => NodeTypeTag::Summary,
2423 "dialog" => NodeTypeTag::Dialog,
2424 "h1" => NodeTypeTag::H1,
2426 "h2" => NodeTypeTag::H2,
2427 "h3" => NodeTypeTag::H3,
2428 "h4" => NodeTypeTag::H4,
2429 "h5" => NodeTypeTag::H5,
2430 "h6" => NodeTypeTag::H6,
2431 "p" => NodeTypeTag::P,
2433 "span" => NodeTypeTag::Span,
2434 "pre" => NodeTypeTag::Pre,
2435 "code" => NodeTypeTag::Code,
2436 "blockquote" => NodeTypeTag::BlockQuote,
2437 "br" => NodeTypeTag::Br,
2438 "hr" => NodeTypeTag::Hr,
2439 "ul" => NodeTypeTag::Ul,
2441 "ol" => NodeTypeTag::Ol,
2442 "li" => NodeTypeTag::Li,
2443 "dl" => NodeTypeTag::Dl,
2444 "dt" => NodeTypeTag::Dt,
2445 "dd" => NodeTypeTag::Dd,
2446 "menu" => NodeTypeTag::Menu,
2447 "menuitem" => NodeTypeTag::MenuItem,
2448 "dir" => NodeTypeTag::Dir,
2449 "table" => NodeTypeTag::Table,
2451 "caption" => NodeTypeTag::Caption,
2452 "thead" => NodeTypeTag::THead,
2453 "tbody" => NodeTypeTag::TBody,
2454 "tfoot" => NodeTypeTag::TFoot,
2455 "tr" => NodeTypeTag::Tr,
2456 "th" => NodeTypeTag::Th,
2457 "td" => NodeTypeTag::Td,
2458 "colgroup" => NodeTypeTag::ColGroup,
2459 "col" => NodeTypeTag::Col,
2460 "form" => NodeTypeTag::Form,
2462 "fieldset" => NodeTypeTag::FieldSet,
2463 "legend" => NodeTypeTag::Legend,
2464 "label" => NodeTypeTag::Label,
2465 "input" => NodeTypeTag::Input,
2466 "button" => NodeTypeTag::Button,
2467 "select" => NodeTypeTag::Select,
2468 "optgroup" => NodeTypeTag::OptGroup,
2469 "option" => NodeTypeTag::SelectOption,
2470 "textarea" => NodeTypeTag::TextArea,
2471 "output" => NodeTypeTag::Output,
2472 "progress" => NodeTypeTag::Progress,
2473 "meter" => NodeTypeTag::Meter,
2474 "datalist" => NodeTypeTag::DataList,
2475 "a" => NodeTypeTag::A,
2477 "strong" => NodeTypeTag::Strong,
2478 "em" => NodeTypeTag::Em,
2479 "b" => NodeTypeTag::B,
2480 "i" => NodeTypeTag::I,
2481 "u" => NodeTypeTag::U,
2482 "s" => NodeTypeTag::S,
2483 "small" => NodeTypeTag::Small,
2484 "mark" => NodeTypeTag::Mark,
2485 "del" => NodeTypeTag::Del,
2486 "ins" => NodeTypeTag::Ins,
2487 "samp" => NodeTypeTag::Samp,
2488 "kbd" => NodeTypeTag::Kbd,
2489 "var" => NodeTypeTag::Var,
2490 "cite" => NodeTypeTag::Cite,
2491 "dfn" => NodeTypeTag::Dfn,
2492 "abbr" => NodeTypeTag::Abbr,
2493 "acronym" => NodeTypeTag::Acronym,
2494 "q" => NodeTypeTag::Q,
2495 "time" => NodeTypeTag::Time,
2496 "sub" => NodeTypeTag::Sub,
2497 "sup" => NodeTypeTag::Sup,
2498 "big" => NodeTypeTag::Big,
2499 "bdo" => NodeTypeTag::Bdo,
2500 "bdi" => NodeTypeTag::Bdi,
2501 "wbr" => NodeTypeTag::Wbr,
2502 "ruby" => NodeTypeTag::Ruby,
2503 "rt" => NodeTypeTag::Rt,
2504 "rtc" => NodeTypeTag::Rtc,
2505 "rp" => NodeTypeTag::Rp,
2506 "data" => NodeTypeTag::Data,
2507 "canvas" => NodeTypeTag::Canvas,
2509 "object" => NodeTypeTag::Object,
2510 "param" => NodeTypeTag::Param,
2511 "embed" => NodeTypeTag::Embed,
2512 "audio" => NodeTypeTag::Audio,
2513 "video" => NodeTypeTag::Video,
2514 "source" => NodeTypeTag::Source,
2515 "track" => NodeTypeTag::Track,
2516 "map" => NodeTypeTag::Map,
2517 "area" => NodeTypeTag::Area,
2518 "svg" => NodeTypeTag::Svg,
2519 "g" => NodeTypeTag::SvgG,
2520 "defs" => NodeTypeTag::SvgDefs,
2521 "symbol" => NodeTypeTag::SvgSymbol,
2522 "use" => NodeTypeTag::SvgUse,
2523 "switch" => NodeTypeTag::SvgSwitch,
2524 "path" => NodeTypeTag::SvgPath,
2525 "circle" => NodeTypeTag::SvgCircle,
2526 "rect" => NodeTypeTag::SvgRect,
2527 "ellipse" => NodeTypeTag::SvgEllipse,
2528 "line" => NodeTypeTag::SvgLine,
2529 "polygon" => NodeTypeTag::SvgPolygon,
2530 "polyline" => NodeTypeTag::SvgPolyline,
2531 "tspan" => NodeTypeTag::SvgTspan,
2532 "textpath" => NodeTypeTag::SvgTextPath,
2533 "lineargradient" => NodeTypeTag::SvgLinearGradient,
2534 "radialgradient" => NodeTypeTag::SvgRadialGradient,
2535 "stop" => NodeTypeTag::SvgStop,
2536 "pattern" => NodeTypeTag::SvgPattern,
2537 "clippath" => NodeTypeTag::SvgClipPathElement,
2538 "mask" => NodeTypeTag::SvgMask,
2539 "filter" => NodeTypeTag::SvgFilter,
2540 "feblend" => NodeTypeTag::SvgFeBlend,
2541 "fecolormatrix" => NodeTypeTag::SvgFeColorMatrix,
2542 "fecomponenttransfer" => NodeTypeTag::SvgFeComponentTransfer,
2543 "fecomposite" => NodeTypeTag::SvgFeComposite,
2544 "feconvolvematrix" => NodeTypeTag::SvgFeConvolveMatrix,
2545 "fediffuselighting" => NodeTypeTag::SvgFeDiffuseLighting,
2546 "fedisplacementmap" => NodeTypeTag::SvgFeDisplacementMap,
2547 "fedistantlight" => NodeTypeTag::SvgFeDistantLight,
2548 "fedropshadow" => NodeTypeTag::SvgFeDropShadow,
2549 "feflood" => NodeTypeTag::SvgFeFlood,
2550 "fefuncr" => NodeTypeTag::SvgFeFuncR,
2551 "fefuncg" => NodeTypeTag::SvgFeFuncG,
2552 "fefuncb" => NodeTypeTag::SvgFeFuncB,
2553 "fefunca" => NodeTypeTag::SvgFeFuncA,
2554 "fegaussianblur" => NodeTypeTag::SvgFeGaussianBlur,
2555 "feimage" => NodeTypeTag::SvgFeImage,
2556 "femerge" => NodeTypeTag::SvgFeMerge,
2557 "femergenode" => NodeTypeTag::SvgFeMergeNode,
2558 "femorphology" => NodeTypeTag::SvgFeMorphology,
2559 "feoffset" => NodeTypeTag::SvgFeOffset,
2560 "fepointlight" => NodeTypeTag::SvgFePointLight,
2561 "fespecularlighting" => NodeTypeTag::SvgFeSpecularLighting,
2562 "fespotlight" => NodeTypeTag::SvgFeSpotLight,
2563 "fetile" => NodeTypeTag::SvgFeTile,
2564 "feturbulence" => NodeTypeTag::SvgFeTurbulence,
2565 "foreignobject" => NodeTypeTag::SvgForeignObject,
2566 "desc" => NodeTypeTag::SvgDesc,
2567 "view" => NodeTypeTag::SvgView,
2568 "animate" => NodeTypeTag::SvgAnimate,
2569 "animatemotion" => NodeTypeTag::SvgAnimateMotion,
2570 "animatetransform" => NodeTypeTag::SvgAnimateTransform,
2571 "set" => NodeTypeTag::SvgSet,
2572 "mpath" => NodeTypeTag::SvgMpath,
2573 "meta" => NodeTypeTag::Meta,
2575 "link" => NodeTypeTag::Link,
2576 "script" => NodeTypeTag::Script,
2577 "style" => NodeTypeTag::Style,
2578 "base" => NodeTypeTag::Base,
2579 "img" | "image" => NodeTypeTag::Img,
2581 "icon" => NodeTypeTag::Icon,
2582 _ => NodeTypeTag::Div,
2583 }
2584}
2585
2586fn builtin_render_fn(
2589 def: &ComponentDef,
2590 data: &ComponentDataModel,
2591 _component_map: &ComponentMap,
2592) -> ResultStyledDomRenderDomError {
2593 let node_type = tag_to_node_type(def.id.name.as_str());
2594 let mut dom = Dom::create_node(node_type);
2595 if let Some(text_str) = data.get_default_string("text") {
2596 let prepared = prepare_string(text_str);
2597 if !prepared.is_empty() {
2598 dom = dom.with_children(alloc::vec![Dom::create_text(prepared)].into());
2599 }
2600 }
2601 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
2602 r.into()
2603}
2604
2605fn builtin_compile_fn(
2608 def: &ComponentDef,
2609 target: &CompileTarget,
2610 data: &ComponentDataModel,
2611 indent: usize,
2612) -> ResultStringCompileError {
2613 let node_type = tag_to_node_type(def.id.name.as_str());
2614 let type_name = format!("{:?}", node_type); let text = data.get_default_string("text");
2616
2617 let r: Result<AzString, CompileError> = match target {
2618 CompileTarget::Rust => {
2619 if let Some(text_str) = text {
2620 Ok(format!(
2621 "Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text(AzString::from_const_str(\"{}\"))].into())",
2622 type_name,
2623 text_str.as_str().replace("\\", "\\\\").replace("\"", "\\\"")
2624 ).into())
2625 } else {
2626 Ok(format!("Dom::create_node(NodeType::{})", type_name).into())
2627 }
2628 }
2629 CompileTarget::C => {
2630 if let Some(text_str) = text {
2631 Ok(format!(
2632 "AzDom_createText(AzString_fromConstStr(\"{}\"))",
2633 text_str.as_str().replace("\\", "\\\\").replace("\"", "\\\"")
2634 ).into())
2635 } else {
2636 Ok(format!("AzDom_create{}()", type_name).into())
2637 }
2638 }
2639 CompileTarget::Cpp => {
2640 Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into())
2641 }
2642 CompileTarget::Python => {
2643 Ok(format!("Dom.{}()", type_name.to_lowercase()).into())
2644 }
2645 };
2646 r.into()
2647}
2648
2649fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn core::fmt::Display) {
2651 use crate::dom::{Dom, NodeType};
2652 let text = alloc::format!("{}: {}", field_name, value);
2653 children.push(
2654 Dom::create_node(NodeType::Div)
2655 .with_children(alloc::vec![Dom::create_text(text)].into()),
2656 );
2657}
2658
2659pub fn user_defined_render_fn(
2671 def: &ComponentDef,
2672 data: &ComponentDataModel,
2673 component_map: &ComponentMap,
2674) -> ResultStyledDomRenderDomError {
2675 use crate::dom::{Dom, NodeType};
2676 use azul_css::css::Css;
2677
2678 let mut children: Vec<Dom> = Vec::new();
2679
2680 for field in data.fields.as_ref().iter() {
2681 let field_name = field.name.as_str();
2682
2683 match &field.default_value {
2685 OptionComponentDefaultValue::None => {
2686 continue;
2688 }
2689 OptionComponentDefaultValue::Some(default_val) => {
2690 match default_val {
2691 ComponentDefaultValue::String(s) => {
2692 let text = s.as_str().trim();
2693 if !text.is_empty() {
2694 let label_dom = Dom::create_node(NodeType::Div)
2695 .with_children(alloc::vec![Dom::create_text(text.to_string())].into());
2696 children.push(label_dom);
2697 }
2698 }
2699 ComponentDefaultValue::Bool(v) => {
2700 push_scalar_field(&mut children, field_name, v);
2701 }
2702 ComponentDefaultValue::I32(v) => {
2703 push_scalar_field(&mut children, field_name, v);
2704 }
2705 ComponentDefaultValue::I64(v) => {
2706 push_scalar_field(&mut children, field_name, v);
2707 }
2708 ComponentDefaultValue::U32(v) => {
2709 push_scalar_field(&mut children, field_name, v);
2710 }
2711 ComponentDefaultValue::U64(v) => {
2712 push_scalar_field(&mut children, field_name, v);
2713 }
2714 ComponentDefaultValue::Usize(v) => {
2715 push_scalar_field(&mut children, field_name, v);
2716 }
2717 ComponentDefaultValue::F32(v) => {
2718 push_scalar_field(&mut children, field_name, v);
2719 }
2720 ComponentDefaultValue::F64(v) => {
2721 push_scalar_field(&mut children, field_name, v);
2722 }
2723 ComponentDefaultValue::ColorU(c) => {
2724 let text = alloc::format!("{}: #{:02x}{:02x}{:02x}{:02x}", field_name, c.r, c.g, c.b, c.a);
2725 children.push(Dom::create_node(NodeType::Div)
2726 .with_children(alloc::vec![Dom::create_text(text)].into()));
2727 }
2728 ComponentDefaultValue::ComponentInstance(ci) => {
2729 if let Some(sub_comp) = component_map.get(ci.library.as_str(), ci.component.as_str()) {
2731 let sub_data = sub_comp.data_model.clone();
2732 match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
2733 ResultStyledDomRenderDomError::Ok(_styled_dom) => {
2734 let text = alloc::format!("[{}:{}]", ci.library.as_str(), ci.component.as_str());
2737 children.push(Dom::create_node(NodeType::Div)
2738 .with_children(alloc::vec![Dom::create_text(text)].into()));
2739 }
2740 ResultStyledDomRenderDomError::Err(_) => {
2741 let text = alloc::format!("[Error rendering {}:{}]", ci.library.as_str(), ci.component.as_str());
2743 children.push(Dom::create_node(NodeType::Div)
2744 .with_children(alloc::vec![Dom::create_text(text)].into()));
2745 }
2746 }
2747 } else {
2748 let text = alloc::format!("[Unknown component {}:{}]", ci.library.as_str(), ci.component.as_str());
2749 children.push(Dom::create_node(NodeType::Div)
2750 .with_children(alloc::vec![Dom::create_text(text)].into()));
2751 }
2752 }
2753 ComponentDefaultValue::CallbackFnPointer(name) => {
2754 let text = alloc::format!("{}: fn({})", field_name, name.as_str());
2756 children.push(Dom::create_node(NodeType::Div)
2757 .with_children(alloc::vec![Dom::create_text(text)].into()));
2758 }
2759 ComponentDefaultValue::Json(json_str) => {
2760 let text = alloc::format!("{}: {}", field_name, json_str.as_str());
2761 children.push(Dom::create_node(NodeType::Div)
2762 .with_children(alloc::vec![Dom::create_text(text)].into()));
2763 }
2764 ComponentDefaultValue::None => {
2765 continue;
2767 }
2768 }
2769 }
2770 }
2771 }
2772
2773 let mut wrapper = Dom::create_node(NodeType::Div);
2774 if !children.is_empty() {
2775 wrapper = wrapper.with_children(children.into());
2776 }
2777
2778 let css = if !def.css.as_str().is_empty() {
2780 Css::from_string(def.css.clone())
2781 } else {
2782 Css::empty()
2783 };
2784
2785 let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
2786 r.into()
2787}
2788
2789pub fn user_defined_compile_fn(
2798 def: &ComponentDef,
2799 target: &CompileTarget,
2800 data: &ComponentDataModel,
2801 indent: usize,
2802) -> ResultStringCompileError {
2803 let tag = def.id.name.as_str();
2804 let indent_str = " ".repeat(indent * 4);
2805 let inner_indent = " ".repeat((indent + 1) * 4);
2806
2807 let r: Result<AzString, CompileError> = match target {
2808 CompileTarget::Rust => {
2809 let mut lines = Vec::new();
2810 lines.push(alloc::format!("{}// Component: {}", indent_str, tag));
2811 lines.push(alloc::format!("{}let mut children: Vec<Dom> = Vec::new();", indent_str));
2812
2813 for field in data.fields.as_ref().iter() {
2814 let fname = field.name.as_str();
2815 match &field.default_value {
2816 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
2817 let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"");
2818 lines.push(alloc::format!(
2819 "{}children.push(Dom::create_text(AzString::from_const_str(\"{}\")));",
2820 inner_indent, escaped
2821 ));
2822 }
2823 OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
2824 lines.push(alloc::format!(
2825 "{}children.push(Dom::create_text(AzString::from(format!(\"{{}}: {{}}\", \"{}\", {}).as_str())));",
2826 inner_indent, fname, b
2827 ));
2828 }
2829 OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
2830 let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
2831 lines.push(alloc::format!(
2832 "{}children.push({}()); // sub-component {}:{}",
2833 inner_indent, fn_name, ci.library.as_str(), ci.component.as_str()
2834 ));
2835 }
2836 _ => {
2837 lines.push(alloc::format!(
2839 "{}// field '{}': {:?}",
2840 inner_indent, fname, field.field_type
2841 ));
2842 }
2843 }
2844 }
2845
2846 lines.push(alloc::format!(
2847 "{}Dom::create_node(NodeType::Div).with_children(children.into())",
2848 indent_str
2849 ));
2850 Ok(lines.join("\n").into())
2851 }
2852 CompileTarget::C => {
2853 let mut lines = Vec::new();
2854 lines.push(alloc::format!("{}/* Component: {} */", indent_str, tag));
2855 lines.push(alloc::format!("{}AzDom root = AzDom_createDiv();", indent_str));
2856
2857 for field in data.fields.as_ref().iter() {
2858 let fname = field.name.as_str();
2859 match &field.default_value {
2860 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
2861 let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"");
2862 lines.push(alloc::format!(
2863 "{}AzDom_addChild(&root, AzDom_createText(AzString_fromConstStr(\"{}\")));",
2864 inner_indent, escaped
2865 ));
2866 }
2867 OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
2868 let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
2869 lines.push(alloc::format!(
2870 "{}AzDom_addChild(&root, {}());",
2871 inner_indent, fn_name
2872 ));
2873 }
2874 _ => {
2875 lines.push(alloc::format!(
2876 "{}/* field '{}' */",
2877 inner_indent, fname
2878 ));
2879 }
2880 }
2881 }
2882
2883 lines.push(alloc::format!("{}return root;", indent_str));
2884 Ok(lines.join("\n").into())
2885 }
2886 CompileTarget::Cpp => {
2887 let mut lines = Vec::new();
2888 lines.push(alloc::format!("{}// Component: {}", indent_str, tag));
2889 lines.push(alloc::format!("{}auto root = Dom::create_div();", indent_str));
2890
2891 for field in data.fields.as_ref().iter() {
2892 let fname = field.name.as_str();
2893 match &field.default_value {
2894 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
2895 let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"");
2896 lines.push(alloc::format!(
2897 "{}root.add_child(Dom::create_text(\"{}\"));",
2898 inner_indent, escaped
2899 ));
2900 }
2901 OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
2902 let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
2903 lines.push(alloc::format!(
2904 "{}root.add_child({}());",
2905 inner_indent, fn_name
2906 ));
2907 }
2908 _ => {
2909 lines.push(alloc::format!(
2910 "{}// field '{}'",
2911 inner_indent, fname
2912 ));
2913 }
2914 }
2915 }
2916
2917 lines.push(alloc::format!("{}return root;", indent_str));
2918 Ok(lines.join("\n").into())
2919 }
2920 CompileTarget::Python => {
2921 let mut lines = Vec::new();
2922 lines.push(alloc::format!("{}# Component: {}", indent_str, tag));
2923 lines.push(alloc::format!("{}root = Dom.div()", indent_str));
2924
2925 for field in data.fields.as_ref().iter() {
2926 let fname = field.name.as_str();
2927 match &field.default_value {
2928 OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
2929 let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "\\'");
2930 lines.push(alloc::format!(
2931 "{}root.add_child(Dom.text('{}'))",
2932 inner_indent, escaped
2933 ));
2934 }
2935 OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
2936 let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
2937 lines.push(alloc::format!(
2938 "{}root.add_child({}())",
2939 inner_indent, fn_name
2940 ));
2941 }
2942 _ => {
2943 lines.push(alloc::format!(
2944 "{}# field '{}'",
2945 inner_indent, fname
2946 ));
2947 }
2948 }
2949 }
2950
2951 lines.push(alloc::format!("{}return root", indent_str));
2952 Ok(lines.join("\n").into())
2953 }
2954 };
2955 r.into()
2956}
2957
2958fn builtin_component_def(tag: &str, display_name: &str, default_text: Option<&str>, css: &str) -> ComponentDef {
2970 let mut fields = builtin_data_model(tag);
2971 if let Some(text) = default_text {
2973 fields.push(data_field("text", ComponentFieldType::String,
2974 Some(ComponentDefaultValue::String(AzString::from(text))),
2975 "Text content of the element"));
2976 }
2977 let model_name = format!("{}Data", display_name);
2978 ComponentDef {
2979 id: ComponentId::builtin(tag),
2980 display_name: AzString::from(display_name),
2981 description: AzString::from(format!("HTML <{}> element", tag).as_str()),
2982 css: AzString::from(css),
2983 source: ComponentSource::Builtin,
2984 data_model: ComponentDataModel {
2985 name: AzString::from(model_name.as_str()),
2986 description: AzString::from(format!("Data model for <{}>", tag).as_str()),
2987 fields: fields.into(),
2988 },
2989 render_fn: builtin_render_fn,
2990 compile_fn: builtin_compile_fn,
2991 render_fn_source: None.into(),
2992 compile_fn_source: None.into(),
2993 }
2994}
2995
2996fn data_field(name: &str, ft: ComponentFieldType, default: Option<ComponentDefaultValue>, description: &str) -> ComponentDataField {
2998 let required = default.is_none();
2999 ComponentDataField {
3000 name: AzString::from(name),
3001 field_type: ft,
3002 default_value: match default {
3003 Some(d) => OptionComponentDefaultValue::Some(d),
3004 None => OptionComponentDefaultValue::None,
3005 },
3006 required,
3007 description: AzString::from(description),
3008 }
3009}
3010
3011fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
3017 use ComponentFieldType::*;
3018 use ComponentDefaultValue as D;
3019 match tag {
3020 "a" => alloc::vec![
3021 data_field("href", String, Some(D::String(AzString::from_const_str(""))), "URL the link points to"),
3022 data_field("target", String, Some(D::String(AzString::from_const_str(""))), "Where to open the linked document (_blank, _self, _parent, _top)"),
3023 data_field("rel", String, Some(D::String(AzString::from_const_str(""))), "Relationship between current and linked document"),
3024 ],
3025 "img" | "image" => alloc::vec![
3026 data_field("src", String, None, "URL of the image"),
3027 data_field("alt", String, Some(D::String(AzString::from_const_str(""))), "Alternative text for the image"),
3028 data_field("width", String, Some(D::String(AzString::from_const_str(""))), "Width of the image"),
3029 data_field("height", String, Some(D::String(AzString::from_const_str(""))), "Height of the image"),
3030 ],
3031 "form" => alloc::vec![
3032 data_field("action", String, Some(D::String(AzString::from_const_str(""))), "URL where form data is submitted"),
3033 data_field("method", String, Some(D::String(AzString::from_const_str("GET"))), "HTTP method for form submission (GET or POST)"),
3034 ],
3035 "label" => alloc::vec![
3036 data_field("for", String, Some(D::String(AzString::from_const_str(""))), "ID of the form element this label is for"),
3037 ],
3038 "button" => alloc::vec![
3039 data_field("type", String, Some(D::String(AzString::from_const_str("button"))), "Button type (button, submit, reset)"),
3040 data_field("disabled", Bool, Some(D::Bool(false)), "Whether the button is disabled"),
3041 ],
3042 "td" | "th" => alloc::vec![
3043 data_field("colspan", I32, Some(D::I32(1)), "Number of columns the cell spans"),
3044 data_field("rowspan", I32, Some(D::I32(1)), "Number of rows the cell spans"),
3045 ],
3046 "icon" => alloc::vec![
3047 data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Icon name"),
3048 ],
3049 "ol" => alloc::vec![
3050 data_field("start", I32, Some(D::I32(1)), "Start value for the ordered list"),
3051 data_field("type", String, Some(D::String(AzString::from_const_str("1"))), "Numbering type (1, A, a, I, i)"),
3052 ],
3053 "input" => alloc::vec![
3055 data_field("type", String, Some(D::String(AzString::from_const_str("text"))), "Input type (text, password, email, number, checkbox, radio, etc.)"),
3056 data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name of the input for form submission"),
3057 data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Current value of the input"),
3058 data_field("placeholder", String, Some(D::String(AzString::from_const_str(""))), "Placeholder text"),
3059 data_field("disabled", Bool, Some(D::Bool(false)), "Whether the input is disabled"),
3060 data_field("required", Bool, Some(D::Bool(false)), "Whether the input is required"),
3061 data_field("readonly", Bool, Some(D::Bool(false)), "Whether the input is read-only"),
3062 data_field("checked", Bool, Some(D::Bool(false)), "Whether the checkbox/radio is checked"),
3063 data_field("min", String, Some(D::String(AzString::from_const_str(""))), "Minimum value (for number, range, date)"),
3064 data_field("max", String, Some(D::String(AzString::from_const_str(""))), "Maximum value (for number, range, date)"),
3065 data_field("step", String, Some(D::String(AzString::from_const_str(""))), "Step increment (for number, range)"),
3066 data_field("pattern", String, Some(D::String(AzString::from_const_str(""))), "Regex pattern for validation"),
3067 data_field("maxlength", String, Some(D::String(AzString::from_const_str(""))), "Maximum number of characters"),
3068 ],
3069 "select" => alloc::vec![
3070 data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name for form submission"),
3071 data_field("multiple", Bool, Some(D::Bool(false)), "Whether multiple options can be selected"),
3072 data_field("disabled", Bool, Some(D::Bool(false)), "Whether the select is disabled"),
3073 data_field("required", Bool, Some(D::Bool(false)), "Whether selection is required"),
3074 data_field("size", String, Some(D::String(AzString::from_const_str(""))), "Number of visible options"),
3075 ],
3076 "option" => alloc::vec![
3077 data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Value submitted with the form"),
3078 data_field("selected", Bool, Some(D::Bool(false)), "Whether this option is selected"),
3079 data_field("disabled", Bool, Some(D::Bool(false)), "Whether this option is disabled"),
3080 ],
3081 "optgroup" => alloc::vec![
3082 data_field("label", String, Some(D::String(AzString::from_const_str(""))), "Label for the option group"),
3083 data_field("disabled", Bool, Some(D::Bool(false)), "Whether the group is disabled"),
3084 ],
3085 "textarea" => alloc::vec![
3086 data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name for form submission"),
3087 data_field("placeholder", String, Some(D::String(AzString::from_const_str(""))), "Placeholder text"),
3088 data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
3089 data_field("cols", I32, Some(D::I32(20)), "Visible width in average character widths"),
3090 data_field("disabled", Bool, Some(D::Bool(false)), "Whether the textarea is disabled"),
3091 data_field("required", Bool, Some(D::Bool(false)), "Whether content is required"),
3092 data_field("readonly", Bool, Some(D::Bool(false)), "Whether the textarea is read-only"),
3093 data_field("maxlength", String, Some(D::String(AzString::from_const_str(""))), "Maximum number of characters"),
3094 ],
3095 "fieldset" => alloc::vec![
3096 data_field("disabled", Bool, Some(D::Bool(false)), "Whether all controls in the fieldset are disabled"),
3097 ],
3098 "output" => alloc::vec![
3099 data_field("for", String, Some(D::String(AzString::from_const_str(""))), "IDs of elements that contributed to the output"),
3100 data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name for form submission"),
3101 ],
3102 "progress" => alloc::vec![
3103 data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Current progress value"),
3104 data_field("max", String, Some(D::String(AzString::from_const_str("1"))), "Maximum value"),
3105 ],
3106 "meter" => alloc::vec![
3107 data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Current value"),
3108 data_field("min", String, Some(D::String(AzString::from_const_str("0"))), "Minimum value"),
3109 data_field("max", String, Some(D::String(AzString::from_const_str("1"))), "Maximum value"),
3110 data_field("low", String, Some(D::String(AzString::from_const_str(""))), "Low threshold"),
3111 data_field("high", String, Some(D::String(AzString::from_const_str(""))), "High threshold"),
3112 data_field("optimum", String, Some(D::String(AzString::from_const_str(""))), "Optimum value"),
3113 ],
3114 "details" => alloc::vec![
3116 data_field("open", Bool, Some(D::Bool(false)), "Whether the details are visible"),
3117 ],
3118 "dialog" => alloc::vec![
3119 data_field("open", Bool, Some(D::Bool(false)), "Whether the dialog is active and can be interacted with"),
3120 ],
3121 "audio" | "video" => alloc::vec![
3123 data_field("src", String, Some(D::String(AzString::from_const_str(""))), "URL of the media resource"),
3124 data_field("controls", Bool, Some(D::Bool(false)), "Whether to show playback controls"),
3125 data_field("autoplay", Bool, Some(D::Bool(false)), "Whether to start playing automatically"),
3126 data_field("loop", Bool, Some(D::Bool(false)), "Whether to loop playback"),
3127 data_field("muted", Bool, Some(D::Bool(false)), "Whether audio is muted"),
3128 data_field("preload", String, Some(D::String(AzString::from_const_str("auto"))), "Preload hint (none, metadata, auto)"),
3129 ],
3130 "source" => alloc::vec![
3131 data_field("src", String, None, "URL of the media resource"),
3132 data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the resource"),
3133 ],
3134 "track" => alloc::vec![
3135 data_field("src", String, None, "URL of the track file"),
3136 data_field("kind", String, Some(D::String(AzString::from_const_str("subtitles"))), "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"),
3137 data_field("srclang", String, Some(D::String(AzString::from_const_str(""))), "Language of the track text"),
3138 data_field("label", String, Some(D::String(AzString::from_const_str(""))), "User-readable title for the track"),
3139 data_field("default", Bool, Some(D::Bool(false)), "Whether this is the default track"),
3140 ],
3141 "canvas" => alloc::vec![
3142 data_field("width", String, Some(D::String(AzString::from_const_str("300"))), "Width of the canvas in pixels"),
3143 data_field("height", String, Some(D::String(AzString::from_const_str("150"))), "Height of the canvas in pixels"),
3144 ],
3145 "embed" => alloc::vec![
3146 data_field("src", String, None, "URL of the resource to embed"),
3147 data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the embedded content"),
3148 data_field("width", String, Some(D::String(AzString::from_const_str(""))), "Width"),
3149 data_field("height", String, Some(D::String(AzString::from_const_str(""))), "Height"),
3150 ],
3151 "object" => alloc::vec![
3152 data_field("data", String, Some(D::String(AzString::from_const_str(""))), "URL of the resource"),
3153 data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the resource"),
3154 data_field("width", String, Some(D::String(AzString::from_const_str(""))), "Width"),
3155 data_field("height", String, Some(D::String(AzString::from_const_str(""))), "Height"),
3156 ],
3157 "param" => alloc::vec![
3158 data_field("name", String, None, "Name of the parameter"),
3159 data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Value of the parameter"),
3160 ],
3161 "area" => alloc::vec![
3162 data_field("shape", String, Some(D::String(AzString::from_const_str("default"))), "Shape of the area (default, rect, circle, poly)"),
3163 data_field("coords", String, Some(D::String(AzString::from_const_str(""))), "Coordinates of the area"),
3164 data_field("href", String, Some(D::String(AzString::from_const_str(""))), "URL for the area link"),
3165 data_field("alt", String, Some(D::String(AzString::from_const_str(""))), "Alternative text"),
3166 data_field("target", String, Some(D::String(AzString::from_const_str(""))), "Where to open the linked document"),
3167 ],
3168 "map" => alloc::vec![
3169 data_field("name", String, None, "Name of the image map (referenced by usemap)"),
3170 ],
3171 "time" => alloc::vec![
3173 data_field("datetime", String, Some(D::String(AzString::from_const_str(""))), "Machine-readable date/time value"),
3174 ],
3175 "data" => alloc::vec![
3176 data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Machine-readable value"),
3177 ],
3178 "abbr" | "acronym" | "dfn" => alloc::vec![
3179 data_field("title", String, Some(D::String(AzString::from_const_str(""))), "Full expansion or definition"),
3180 ],
3181 "q" | "blockquote" => alloc::vec![
3182 data_field("cite", String, Some(D::String(AzString::from_const_str(""))), "URL of the source of the quotation"),
3183 ],
3184 "del" | "ins" => alloc::vec![
3185 data_field("cite", String, Some(D::String(AzString::from_const_str(""))), "URL explaining the change"),
3186 data_field("datetime", String, Some(D::String(AzString::from_const_str(""))), "Date/time of the change"),
3187 ],
3188 "bdo" => alloc::vec![
3189 data_field("dir", String, Some(D::String(AzString::from_const_str("ltr"))), "Text direction (ltr, rtl)"),
3190 ],
3191 "col" | "colgroup" => alloc::vec![
3192 data_field("span", I32, Some(D::I32(1)), "Number of columns the element spans"),
3193 ],
3194 "meta" => alloc::vec![
3196 data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Metadata name"),
3197 data_field("content", String, Some(D::String(AzString::from_const_str(""))), "Metadata value"),
3198 data_field("charset", String, Some(D::String(AzString::from_const_str(""))), "Character encoding"),
3199 data_field("http-equiv", String, Some(D::String(AzString::from_const_str(""))), "HTTP header equivalent"),
3200 ],
3201 "link" => alloc::vec![
3202 data_field("rel", String, None, "Relationship type"),
3203 data_field("href", String, Some(D::String(AzString::from_const_str(""))), "URL of the linked resource"),
3204 data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the linked resource"),
3205 ],
3206 "script" => alloc::vec![
3207 data_field("src", String, Some(D::String(AzString::from_const_str(""))), "URL of external script"),
3208 data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type or module"),
3209 data_field("async", Bool, Some(D::Bool(false)), "Execute asynchronously"),
3210 data_field("defer", Bool, Some(D::Bool(false)), "Defer execution until page load"),
3211 ],
3212 "style" => alloc::vec![
3213 data_field("type", String, Some(D::String(AzString::from_const_str("text/css"))), "MIME type of the style sheet"),
3214 ],
3215 "base" => alloc::vec![
3216 data_field("href", String, Some(D::String(AzString::from_const_str(""))), "Base URL for relative URLs"),
3217 data_field("target", String, Some(D::String(AzString::from_const_str(""))), "Default target for hyperlinks"),
3218 ],
3219 _ => alloc::vec![],
3220 }
3221}
3222
3223impl Default for ComponentMap {
3224 fn default() -> Self {
3230 ComponentMap {
3231 libraries: ComponentLibraryVec::from_const_slice(&[]),
3232 }
3233 }
3234}
3235
3236impl ComponentMap {
3237 pub fn create() -> Self {
3238 Self::default()
3239 }
3240
3241 pub fn with_builtin() -> Self {
3243 ComponentMap {
3244 libraries: alloc::vec![register_builtin_components()].into(),
3245 }
3246 }
3247
3248 pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
3254 ComponentMap {
3255 libraries: libs.clone(),
3256 }
3257 }
3258}
3259
3260fn xml_attrs_to_data_model(
3275 base_model: &ComponentDataModel,
3276 xml_attributes: &XmlAttributeMap,
3277 text_content: Option<&str>,
3278) -> ComponentDataModel {
3279 let mut model = base_model.clone();
3280
3281 let mut fields_vec = core::mem::replace(
3283 &mut model.fields,
3284 ComponentDataFieldVec::from_const_slice(&[]),
3285 ).into_library_owned_vec();
3286
3287 for field in fields_vec.iter_mut() {
3288 if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
3289 field.default_value = OptionComponentDefaultValue::Some(
3291 ComponentDefaultValue::String(attr_value.clone()),
3292 );
3293 }
3294 }
3295
3296 model.fields = ComponentDataFieldVec::from_vec(fields_vec);
3297
3298 if let Some(text) = text_content {
3300 let prepared = prepare_string(text);
3301 if !prepared.is_empty() {
3302 model = model.with_default("text", ComponentDefaultValue::String(AzString::from(prepared.as_str())));
3303 }
3304 }
3305
3306 model
3307}
3308
3309fn builtin_if_component() -> ComponentDef {
3316 ComponentDef {
3317 id: ComponentId::builtin("if"),
3318 display_name: AzString::from_const_str("If"),
3319 description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
3320 css: AzString::from_const_str(""),
3321 source: ComponentSource::Builtin,
3322 data_model: ComponentDataModel {
3323 name: AzString::from_const_str("IfData"),
3324 description: AzString::from_const_str("Data for conditional rendering"),
3325 fields: alloc::vec![
3326 data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
3327 ].into(),
3328 },
3329 render_fn: builtin_if_render_fn,
3330 compile_fn: builtin_if_compile_fn,
3331 render_fn_source: None.into(),
3332 compile_fn_source: None.into(),
3333 }
3334}
3335
3336fn builtin_if_render_fn(
3337 _comp: &ComponentDef,
3338 data_model: &ComponentDataModel,
3339 _component_map: &ComponentMap,
3340) -> ResultStyledDomRenderDomError {
3341 let condition = data_model.fields.iter().find(|f| f.name.as_str() == "condition")
3343 .and_then(|f| match &f.default_value { OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b), _ => None })
3344 .unwrap_or(false);
3345
3346 let label = if condition { "if: true (then branch)" } else { "if: false (else branch)" };
3347 let mut dom = Dom::create_node(NodeType::Div)
3348 .with_children(alloc::vec![Dom::create_text(label)].into());
3349 let css = Css::empty();
3350 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
3351}
3352
3353fn builtin_if_compile_fn(
3354 _comp: &ComponentDef,
3355 target: &CompileTarget,
3356 _data: &ComponentDataModel,
3357 _indent: usize,
3358) -> ResultStringCompileError {
3359 match target {
3360 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
3361 "if data.condition {\n // then branch\n Dom::div()\n} else {\n // else branch\n Dom::div()\n}"
3362 )),
3363 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
3364 "if (data.condition) {\n // then branch\n AzDom_createDiv();\n} else {\n // else branch\n AzDom_createDiv();\n}"
3365 )),
3366 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
3367 "if (data.condition) {\n // then branch\n Dom::div();\n} else {\n // else branch\n Dom::div();\n}"
3368 )),
3369 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
3370 "if data.condition:\n # then branch\n Dom.div()\nelse:\n # else branch\n Dom.div()"
3371 )),
3372 }
3373}
3374
3375fn builtin_for_component() -> ComponentDef {
3378 ComponentDef {
3379 id: ComponentId::builtin("for"),
3380 display_name: AzString::from_const_str("For Loop"),
3381 description: AzString::from_const_str("Iterative rendering: repeats children 'count' times."),
3382 css: AzString::from_const_str(""),
3383 source: ComponentSource::Builtin,
3384 data_model: ComponentDataModel {
3385 name: AzString::from_const_str("ForData"),
3386 description: AzString::from_const_str("Data for iterative rendering"),
3387 fields: alloc::vec![
3388 data_field("count", ComponentFieldType::U32, Some(ComponentDefaultValue::U32(3)), "Number of iterations"),
3389 ].into(),
3390 },
3391 render_fn: builtin_for_render_fn,
3392 compile_fn: builtin_for_compile_fn,
3393 render_fn_source: None.into(),
3394 compile_fn_source: None.into(),
3395 }
3396}
3397
3398fn builtin_for_render_fn(
3399 _comp: &ComponentDef,
3400 data_model: &ComponentDataModel,
3401 _component_map: &ComponentMap,
3402) -> ResultStyledDomRenderDomError {
3403 let count = data_model.fields.iter().find(|f| f.name.as_str() == "count")
3404 .and_then(|f| match &f.default_value { OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n), _ => None })
3405 .unwrap_or(3);
3406
3407 let mut items: alloc::vec::Vec<Dom> = alloc::vec::Vec::new();
3408 for i in 0..count {
3409 items.push(Dom::create_node(NodeType::Div)
3410 .with_children(alloc::vec![Dom::create_text(alloc::format!("Item {}", i))].into()));
3411 }
3412 let mut dom = Dom::create_node(NodeType::Div)
3413 .with_children(items.into());
3414 let css = Css::empty();
3415 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
3416}
3417
3418fn builtin_for_compile_fn(
3419 _comp: &ComponentDef,
3420 target: &CompileTarget,
3421 _data: &ComponentDataModel,
3422 _indent: usize,
3423) -> ResultStringCompileError {
3424 match target {
3425 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
3426 "let mut children = Vec::new();\nfor i in 0..data.count {\n children.push(Dom::div());\n}\nDom::div().with_children(children)"
3427 )),
3428 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
3429 "AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n AzDom_addChild(&container, AzDom_createDiv());\n}"
3430 )),
3431 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
3432 "auto container = Dom::div();\nfor (uint32_t i = 0; i < data.count; i++) {\n container.add_child(Dom::div());\n}"
3433 )),
3434 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
3435 "container = Dom.div()\nfor i in range(data.count):\n container.add_child(Dom.div())"
3436 )),
3437 }
3438}
3439
3440fn builtin_map_component() -> ComponentDef {
3443 ComponentDef {
3444 id: ComponentId::builtin("map"),
3445 display_name: AzString::from_const_str("Map"),
3446 description: AzString::from_const_str("Map data to DOM: applies a template to each item in a collection."),
3447 css: AzString::from_const_str(""),
3448 source: ComponentSource::Builtin,
3449 data_model: ComponentDataModel {
3450 name: AzString::from_const_str("MapData"),
3451 description: AzString::from_const_str("Data for map rendering"),
3452 fields: alloc::vec![
3453 data_field("data_json", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from_const_str("[]"))), "JSON array of items to map over"),
3454 ].into(),
3455 },
3456 render_fn: builtin_map_render_fn,
3457 compile_fn: builtin_map_compile_fn,
3458 render_fn_source: None.into(),
3459 compile_fn_source: None.into(),
3460 }
3461}
3462
3463fn builtin_map_render_fn(
3464 _comp: &ComponentDef,
3465 data_model: &ComponentDataModel,
3466 _component_map: &ComponentMap,
3467) -> ResultStyledDomRenderDomError {
3468 let data_str = data_model.fields.iter().find(|f| f.name.as_str() == "data_json")
3470 .and_then(|f| match &f.default_value { OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s.as_str().to_string()), _ => None })
3471 .unwrap_or_else(|| "[]".to_string());
3472
3473 let label = alloc::format!("map: data_json={}", data_str);
3474 let mut dom = Dom::create_node(NodeType::Div)
3475 .with_children(alloc::vec![Dom::create_text(label)].into());
3476 let css = Css::empty();
3477 ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
3478}
3479
3480fn builtin_map_compile_fn(
3481 _comp: &ComponentDef,
3482 target: &CompileTarget,
3483 _data: &ComponentDataModel,
3484 _indent: usize,
3485) -> ResultStringCompileError {
3486 match target {
3487 CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
3488 "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::div() // map template\n}).collect();\nDom::div().with_children(children)"
3489 )),
3490 CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
3491 "// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
3492 )),
3493 CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
3494 "// Parse data.data_json and map each item\nauto container = Dom::div();\n// TODO: iterate parsed JSON array"
3495 )),
3496 CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
3497 "import json\nitems = json.loads(data.data_json)\ncontainer = Dom.div()\nfor item in items:\n container.add_child(Dom.div())"
3498 )),
3499 }
3500}
3501
3502pub extern "C" fn register_builtin_components() -> ComponentLibrary {
3511 ComponentLibrary {
3512 name: AzString::from_const_str("builtin"),
3513 version: AzString::from_const_str("1.0.0"),
3514 description: AzString::from_const_str("Built-in HTML elements"),
3515 exportable: false,
3516 modifiable: false,
3517 data_models: Vec::new().into(),
3518 enum_models: Vec::new().into(),
3519 components: alloc::vec![
3520 builtin_component_def("html", "HTML", None, ""),
3522 builtin_component_def("head", "Head", None, ""),
3523 builtin_component_def("title", "Title", Some(""), ""),
3524 builtin_component_def("body", "Body", None, ""),
3525 builtin_component_def("div", "Div", None, ""),
3527 builtin_component_def("header", "Header", None, ""),
3528 builtin_component_def("footer", "Footer", None, ""),
3529 builtin_component_def("section", "Section", None, ""),
3530 builtin_component_def("article", "Article", None, ""),
3531 builtin_component_def("aside", "Aside", None, ""),
3532 builtin_component_def("nav", "Nav", None, ""),
3533 builtin_component_def("main", "Main", None, ""),
3534 builtin_component_def("figure", "Figure", None, ""),
3535 builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
3536 builtin_component_def("address", "Address", Some(""), ""),
3537 builtin_component_def("details", "Details", None, ""),
3538 builtin_component_def("summary", "Summary", Some("Details"), ""),
3539 builtin_component_def("dialog", "Dialog", None, ""),
3540 builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
3542 builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
3543 builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
3544 builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
3545 builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
3546 builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
3547 builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
3549 builtin_component_def("span", "Span", Some(""), ""),
3550 builtin_component_def("pre", "Preformatted", Some(""), ""),
3551 builtin_component_def("code", "Code", Some(""), ""),
3552 builtin_component_def("blockquote", "Blockquote", Some(""), ""),
3553 builtin_component_def("br", "Line Break", None, ""),
3554 builtin_component_def("hr", "Horizontal Rule", None, ""),
3555 builtin_component_def("icon", "Icon", Some(""), ""),
3556 builtin_component_def("ul", "Unordered List", None, ""),
3558 builtin_component_def("ol", "Ordered List", None, ""),
3559 builtin_component_def("li", "List Item", Some("List item"), ""),
3560 builtin_component_def("dl", "Description List", None, ""),
3561 builtin_component_def("dt", "Description Term", Some(""), ""),
3562 builtin_component_def("dd", "Description Details", Some(""), ""),
3563 builtin_component_def("menu", "Menu", None, ""),
3564 builtin_component_def("menuitem", "Menu Item", Some(""), ""),
3565 builtin_component_def("dir", "Directory List", None, ""),
3566 builtin_component_def("table", "Table", None, ""),
3568 builtin_component_def("caption", "Table Caption", Some(""), ""),
3569 builtin_component_def("thead", "Table Head", None, ""),
3570 builtin_component_def("tbody", "Table Body", None, ""),
3571 builtin_component_def("tfoot", "Table Foot", None, ""),
3572 builtin_component_def("tr", "Table Row", None, ""),
3573 builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
3574 builtin_component_def("td", "Table Data Cell", Some(""), ""),
3575 builtin_component_def("colgroup", "Column Group", None, ""),
3576 builtin_component_def("col", "Column", None, ""),
3577 builtin_component_def("a", "Link", Some("Link text"), ""),
3579 builtin_component_def("strong", "Strong", Some(""), ""),
3580 builtin_component_def("em", "Emphasis", Some(""), ""),
3581 builtin_component_def("b", "Bold", Some(""), ""),
3582 builtin_component_def("i", "Italic", Some(""), ""),
3583 builtin_component_def("u", "Underline", Some(""), ""),
3584 builtin_component_def("s", "Strikethrough", Some(""), ""),
3585 builtin_component_def("small", "Small", Some(""), ""),
3586 builtin_component_def("mark", "Mark", Some(""), ""),
3587 builtin_component_def("del", "Deleted Text", Some(""), ""),
3588 builtin_component_def("ins", "Inserted Text", Some(""), ""),
3589 builtin_component_def("sub", "Subscript", Some(""), ""),
3590 builtin_component_def("sup", "Superscript", Some(""), ""),
3591 builtin_component_def("samp", "Sample Output", Some(""), ""),
3592 builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
3593 builtin_component_def("var", "Variable", Some(""), ""),
3594 builtin_component_def("cite", "Citation", Some(""), ""),
3595 builtin_component_def("dfn", "Definition", Some(""), ""),
3596 builtin_component_def("abbr", "Abbreviation", Some(""), ""),
3597 builtin_component_def("acronym", "Acronym", Some(""), ""),
3598 builtin_component_def("q", "Inline Quote", Some(""), ""),
3599 builtin_component_def("time", "Time", Some(""), ""),
3600 builtin_component_def("big", "Big", Some(""), ""),
3601 builtin_component_def("bdo", "BiDi Override", Some(""), ""),
3602 builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
3603 builtin_component_def("wbr", "Word Break Opportunity", None, ""),
3604 builtin_component_def("ruby", "Ruby Annotation", None, ""),
3605 builtin_component_def("rt", "Ruby Text", Some(""), ""),
3606 builtin_component_def("rtc", "Ruby Text Container", None, ""),
3607 builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
3608 builtin_component_def("data", "Data", Some(""), ""),
3609 builtin_component_def("form", "Form", None, ""),
3611 builtin_component_def("fieldset", "Field Set", None, ""),
3612 builtin_component_def("legend", "Legend", Some("Legend"), ""),
3613 builtin_component_def("label", "Label", Some("Label"), ""),
3614 builtin_component_def("input", "Input", None, ""),
3615 builtin_component_def("button", "Button", Some("Button text"), ""),
3616 builtin_component_def("select", "Select", None, ""),
3617 builtin_component_def("optgroup", "Option Group", None, ""),
3618 builtin_component_def("option", "Option", Some(""), ""),
3619 builtin_component_def("textarea", "Text Area", Some(""), ""),
3620 builtin_component_def("output", "Output", Some(""), ""),
3621 builtin_component_def("progress", "Progress", None, ""),
3622 builtin_component_def("meter", "Meter", None, ""),
3623 builtin_component_def("datalist", "Data List", None, ""),
3624 builtin_component_def("canvas", "Canvas", None, ""),
3626 builtin_component_def("object", "Object", None, ""),
3627 builtin_component_def("param", "Parameter", None, ""),
3628 builtin_component_def("embed", "Embed", None, ""),
3629 builtin_component_def("audio", "Audio", None, ""),
3630 builtin_component_def("video", "Video", None, ""),
3631 builtin_component_def("source", "Source", None, ""),
3632 builtin_component_def("track", "Track", None, ""),
3633 builtin_component_def("map", "Image Map", None, ""),
3634 builtin_component_def("area", "Map Area", None, ""),
3635 builtin_component_def("svg", "SVG", None, ""),
3636 builtin_component_def("meta", "Meta", None, ""),
3638 builtin_component_def("link", "Link (Resource)", None, ""),
3639 builtin_component_def("script", "Script", Some(""), ""),
3640 builtin_component_def("style", "Style", Some(""), ""),
3641 builtin_component_def("base", "Base URL", None, ""),
3642 builtin_if_component(),
3644 builtin_for_component(),
3645 builtin_map_component(),
3646 ].into(),
3647 }
3648}
3649
3650#[derive(Default)]
3658pub struct DomXml {
3659 pub parsed_dom: StyledDom,
3660}
3661
3662impl DomXml {
3663 #[cfg(test)]
3676 pub fn assert_eq(self, other: StyledDom) {
3677 let mut body = Dom::create_body();
3678 let mut fixed = StyledDom::create(&mut body, Css::empty());
3679 fixed.append_child(other);
3680 if self.parsed_dom != fixed {
3681 panic!(
3682 "\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
3683 ----------\r\n{}\r\n",
3684 self.parsed_dom.get_html_string("", "", true),
3685 fixed.get_html_string("", "", true)
3686 );
3687 }
3688 }
3689
3690 pub fn into_styled_dom(self) -> StyledDom {
3691 self.into()
3692 }
3693}
3694
3695impl From<DomXml> for StyledDom {
3696 fn from(val: DomXml) -> Self {
3697 val.parsed_dom
3698 }
3699}
3700
3701#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3703#[repr(C, u8)]
3704pub enum XmlNodeChild {
3705 Text(AzString),
3707 Element(XmlNode),
3709}
3710
3711impl_option!(
3712 XmlNodeChild,
3713 OptionXmlNodeChild,
3714 copy = false,
3715 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3716);
3717
3718impl XmlNodeChild {
3719 pub fn as_text(&self) -> Option<&str> {
3721 match self {
3722 XmlNodeChild::Text(s) => Some(s.as_str()),
3723 XmlNodeChild::Element(_) => None,
3724 }
3725 }
3726
3727 pub fn as_element(&self) -> Option<&XmlNode> {
3729 match self {
3730 XmlNodeChild::Text(_) => None,
3731 XmlNodeChild::Element(node) => Some(node),
3732 }
3733 }
3734
3735 pub fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
3737 match self {
3738 XmlNodeChild::Text(_) => None,
3739 XmlNodeChild::Element(node) => Some(node),
3740 }
3741 }
3742}
3743
3744impl_vec!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor, XmlNodeChildVecDestructorType, XmlNodeChildVecSlice, OptionXmlNodeChild);
3745impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
3746impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
3747impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
3748impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
3749impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
3750impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
3751impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
3752impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
3753
3754#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3756#[repr(C)]
3757pub struct XmlNode {
3758 pub node_type: XmlTagName,
3760 pub attributes: XmlAttributeMap,
3762 pub children: XmlNodeChildVec,
3764}
3765
3766impl_option!(
3767 XmlNode,
3768 OptionXmlNode,
3769 copy = false,
3770 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3771);
3772
3773impl XmlNode {
3774 pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
3775 XmlNode {
3776 node_type: node_type.into(),
3777 ..Default::default()
3778 }
3779 }
3780 pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
3781 Self {
3782 children: v.into(),
3783 ..self
3784 }
3785 }
3786
3787 pub fn get_text_content(&self) -> String {
3789 self.children
3790 .as_ref()
3791 .iter()
3792 .filter_map(|child| child.as_text())
3793 .collect::<Vec<_>>()
3794 .join("")
3795 }
3796
3797 pub fn has_only_text_children(&self) -> bool {
3799 self.children
3800 .as_ref()
3801 .iter()
3802 .all(|child| matches!(child, XmlNodeChild::Text(_)))
3803 }
3804}
3805
3806impl_vec!(XmlNode, XmlNodeVec, XmlNodeVecDestructor, XmlNodeVecDestructorType, XmlNodeVecSlice, OptionXmlNode);
3807impl_vec_mut!(XmlNode, XmlNodeVec);
3808impl_vec_debug!(XmlNode, XmlNodeVec);
3809impl_vec_partialeq!(XmlNode, XmlNodeVec);
3810impl_vec_eq!(XmlNode, XmlNodeVec);
3811impl_vec_partialord!(XmlNode, XmlNodeVec);
3812impl_vec_ord!(XmlNode, XmlNodeVec);
3813impl_vec_hash!(XmlNode, XmlNodeVec);
3814impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
3815
3816#[derive(Debug, Clone, PartialEq)]
3817#[repr(C, u8)]
3818pub enum DomXmlParseError {
3819 NoHtmlNode,
3821 MultipleHtmlRootNodes,
3823 NoBodyInHtml,
3825 MultipleBodyNodes,
3827 Xml(XmlError),
3832 MalformedHierarchy(MalformedHierarchyError),
3834 RenderDom(RenderDomError),
3837 Component(ComponentParseError),
3839 Css(CssParseErrorOwned),
3841}
3842
3843impl From<XmlError> for DomXmlParseError {
3844 fn from(e: XmlError) -> Self {
3845 Self::Xml(e)
3846 }
3847}
3848
3849impl From<ComponentParseError> for DomXmlParseError {
3850 fn from(e: ComponentParseError) -> Self {
3851 Self::Component(e)
3852 }
3853}
3854
3855impl From<RenderDomError> for DomXmlParseError {
3856 fn from(e: RenderDomError) -> Self {
3857 Self::RenderDom(e)
3858 }
3859}
3860
3861impl From<CssParseErrorOwned> for DomXmlParseError {
3862 fn from(e: CssParseErrorOwned) -> Self {
3863 Self::Css(e)
3864 }
3865}
3866
3867#[derive(Debug, Clone, PartialEq)]
3870#[repr(C, u8)]
3871pub enum CompileError {
3872 Dom(RenderDomError),
3873 Xml(DomXmlParseError),
3874 Css(CssParseErrorOwned),
3875}
3876
3877impl From<ComponentError> for CompileError {
3878 fn from(e: ComponentError) -> Self {
3879 CompileError::Dom(RenderDomError::Component(e))
3880 }
3881}
3882
3883impl From<CssParseErrorOwned> for CompileError {
3884 fn from(e: CssParseErrorOwned) -> Self {
3885 CompileError::Css(e)
3886 }
3887}
3888
3889impl<'a> fmt::Display for CompileError {
3890 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3891 use self::CompileError::*;
3892 match self {
3893 Dom(d) => write!(f, "{}", d),
3894 Xml(s) => write!(f, "{}", s),
3895 Css(s) => write!(f, "{}", s.to_shared()),
3896 }
3897 }
3898}
3899
3900impl From<RenderDomError> for CompileError {
3901 fn from(e: RenderDomError) -> Self {
3902 CompileError::Dom(e)
3903 }
3904}
3905
3906impl From<DomXmlParseError> for CompileError {
3907 fn from(e: DomXmlParseError) -> Self {
3908 CompileError::Xml(e)
3909 }
3910}
3911
3912#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3914#[repr(C)]
3915pub struct UselessFunctionArgumentError {
3916 pub component_name: AzString,
3917 pub argument_name: AzString,
3918 pub valid_args: StringVec,
3919}
3920
3921#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3922#[repr(C, u8)]
3923pub enum ComponentError {
3924 UselessFunctionArgument(UselessFunctionArgumentError),
3927 UnknownComponent(AzString),
3932}
3933
3934#[derive(Debug, Clone, PartialEq)]
3935#[repr(C, u8)]
3936pub enum RenderDomError {
3937 Component(ComponentError),
3938 CssError(CssParseErrorOwned),
3940}
3941
3942impl From<ComponentError> for RenderDomError {
3943 fn from(e: ComponentError) -> Self {
3944 Self::Component(e)
3945 }
3946}
3947
3948impl From<CssParseErrorOwned> for RenderDomError {
3949 fn from(e: CssParseErrorOwned) -> Self {
3950 Self::CssError(e)
3951 }
3952}
3953
3954#[derive(Debug, Clone, PartialEq)]
3956#[repr(C)]
3957pub struct MissingTypeError {
3958 pub arg_pos: usize,
3959 pub arg_name: AzString,
3960}
3961
3962#[derive(Debug, Clone, PartialEq)]
3964#[repr(C)]
3965pub struct WhiteSpaceInComponentNameError {
3966 pub arg_pos: usize,
3967 pub arg_name: AzString,
3968}
3969
3970#[derive(Debug, Clone, PartialEq)]
3972#[repr(C)]
3973pub struct WhiteSpaceInComponentTypeError {
3974 pub arg_pos: usize,
3975 pub arg_name: AzString,
3976 pub arg_type: AzString,
3977}
3978
3979#[derive(Debug, Clone, PartialEq)]
3980#[repr(C, u8)]
3981pub enum ComponentParseError {
3982 NotAComponent,
3984 UnnamedComponent,
3986 MissingName(usize),
3988 MissingType(MissingTypeError),
3991 WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
3994 WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
3997 CssError(CssParseErrorOwned),
3999}
4000
4001impl<'a> fmt::Display for DomXmlParseError {
4002 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4003 use self::DomXmlParseError::*;
4004 match self {
4005 NoHtmlNode => write!(
4006 f,
4007 "No <html> node found as the root of the file - empty file?"
4008 ),
4009 MultipleHtmlRootNodes => write!(
4010 f,
4011 "Multiple <html> nodes found as the root of the file - only one root node allowed"
4012 ),
4013 NoBodyInHtml => write!(
4014 f,
4015 "No <body> node found as a direct child of an <html> node - malformed DOM \
4016 hierarchy?"
4017 ),
4018 MultipleBodyNodes => write!(
4019 f,
4020 "Multiple <body> nodes present, only one <body> node is allowed"
4021 ),
4022 Xml(e) => write!(f, "Error parsing XML: {}", e),
4023 MalformedHierarchy(e) => write!(
4024 f,
4025 "Invalid </{}> tag: expected </{}>",
4026 e.got.as_str(),
4027 e.expected.as_str()
4028 ),
4029 RenderDom(e) => write!(f, "Error rendering DOM: {}", e),
4030 Component(c) => write!(f, "Error parsing component in <head> node:\r\n{}", c),
4031 Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
4032 }
4033 }
4034}
4035
4036impl<'a> fmt::Display for ComponentParseError {
4037 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4038 use self::ComponentParseError::*;
4039 match self {
4040 NotAComponent => write!(f, "Expected <component/> node, found no such node"),
4041 UnnamedComponent => write!(
4042 f,
4043 "Found <component/> tag with out a \"name\" attribute, component must have a name"
4044 ),
4045 MissingName(arg_pos) => write!(
4046 f,
4047 "Argument at position {} is either empty or has no name",
4048 arg_pos
4049 ),
4050 MissingType(e) => write!(
4051 f,
4052 "Argument \"{}\" at position {} doesn't have a `: type`",
4053 e.arg_name, e.arg_pos
4054 ),
4055 WhiteSpaceInComponentName(e) => {
4056 write!(
4057 f,
4058 "Missing `:` between the name and the type in argument {} (around \"{}\")",
4059 e.arg_pos, e.arg_name
4060 )
4061 }
4062 WhiteSpaceInComponentType(e) => {
4063 write!(
4064 f,
4065 "Missing `,` between two arguments (in argument {}, position {}, around \
4066 \"{}\")",
4067 e.arg_name, e.arg_pos, e.arg_type
4068 )
4069 }
4070 CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
4071 }
4072 }
4073}
4074
4075impl fmt::Display for ComponentError {
4076 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4077 use self::ComponentError::*;
4078 match self {
4079 UselessFunctionArgument(e) => {
4080 write!(
4081 f,
4082 "Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
4083 e.component_name, e.argument_name, e.valid_args
4084 )
4085 }
4086 UnknownComponent(name) => write!(f, "Unknown component: \"{}\"", name),
4087 }
4088 }
4089}
4090
4091impl<'a> fmt::Display for RenderDomError {
4092 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4093 use self::RenderDomError::*;
4094 match self {
4095 Component(c) => write!(f, "{}", c),
4096 CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
4097 }
4098 }
4099}
4100
4101
4102pub fn get_html_node<'a>(root_nodes: &'a [XmlNodeChild]) -> Result<&'a XmlNode, DomXmlParseError> {
4105 let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
4106 if let XmlNodeChild::Element(node) = child {
4107 let node_type_normalized = normalize_casing(&node.node_type);
4108 if &node_type_normalized == "html" {
4109 Some(node)
4110 } else {
4111 None
4112 }
4113 } else {
4114 None
4115 }
4116 });
4117
4118 let html_node = html_node_iterator
4119 .next()
4120 .ok_or(DomXmlParseError::NoHtmlNode)?;
4121 if html_node_iterator.next().is_some() {
4122 Err(DomXmlParseError::MultipleHtmlRootNodes)
4123 } else {
4124 Ok(html_node)
4125 }
4126}
4127
4128pub fn get_body_node<'a>(root_nodes: &'a [XmlNodeChild]) -> Result<&'a XmlNode, DomXmlParseError> {
4131 let direct_body = root_nodes.iter().filter_map(|child| {
4133 if let XmlNodeChild::Element(node) = child {
4134 let node_type_normalized = normalize_casing(&node.node_type);
4135 if &node_type_normalized == "body" {
4136 Some(node)
4137 } else {
4138 None
4139 }
4140 } else {
4141 None
4142 }
4143 }).next();
4144
4145 if let Some(body) = direct_body {
4146 return Ok(body);
4147 }
4148
4149 fn find_body_recursive<'a>(nodes: &'a [XmlNodeChild]) -> Option<&'a XmlNode> {
4152 for child in nodes {
4153 if let XmlNodeChild::Element(node) = child {
4154 let node_type_normalized = normalize_casing(&node.node_type);
4155 if &node_type_normalized == "body" {
4156 return Some(node);
4157 }
4158 if let Some(found) = find_body_recursive(node.children.as_ref()) {
4160 return Some(found);
4161 }
4162 }
4163 }
4164 None
4165 }
4166
4167 find_body_recursive(root_nodes).ok_or(DomXmlParseError::NoBodyInHtml)
4168}
4169
4170fn find_node_by_type<'a>(
4174 root_nodes: &'a [XmlNodeChild],
4175 node_type: &str,
4176) -> Option<&'a XmlNode> {
4177 for child in root_nodes {
4179 if let XmlNodeChild::Element(node) = child {
4180 if normalize_casing(&node.node_type).as_str() == node_type {
4181 return Some(node);
4182 }
4183 }
4184 }
4185
4186 for child in root_nodes {
4188 if let XmlNodeChild::Element(node) = child {
4189 if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
4190 return Some(found);
4191 }
4192 }
4193 }
4194
4195 None
4196}
4197
4198pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
4199 node.attributes
4200 .iter()
4201 .find(|n| normalize_casing(&n.key.as_str()).as_str() == attribute)
4202 .map(|s| &s.value)
4203}
4204
4205pub fn normalize_casing(input: &str) -> String {
4207 let mut words: Vec<String> = Vec::new();
4208 let mut cur_str = Vec::new();
4209
4210 for ch in input.chars() {
4211 if ch.is_uppercase() || ch == '_' || ch == '-' {
4212 if !cur_str.is_empty() {
4213 words.push(cur_str.iter().collect());
4214 cur_str.clear();
4215 }
4216 if ch.is_uppercase() {
4217 cur_str.extend(ch.to_lowercase());
4218 }
4219 } else {
4220 cur_str.extend(ch.to_lowercase());
4221 }
4222 }
4223
4224 if !cur_str.is_empty() {
4225 words.push(cur_str.iter().collect());
4226 cur_str.clear();
4227 }
4228
4229 words.join("_")
4230}
4231
4232#[allow(trivial_casts)]
4235pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
4236 let mut hierarchy = hierarchy.to_vec();
4237 hierarchy.reverse();
4238 let item = match hierarchy.pop() {
4239 Some(s) => s,
4240 None => return Some(root_node),
4241 };
4242 let child = root_node.children.as_mut().get_mut(item)?;
4243 match child {
4244 XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
4245 XmlNodeChild::Text(_) => None, }
4247}
4248
4249fn get_item_internal<'a>(
4250 hierarchy: &mut Vec<usize>,
4251 root_node: &'a mut XmlNode,
4252) -> Option<&'a mut XmlNode> {
4253 if hierarchy.is_empty() {
4254 return Some(root_node);
4255 }
4256 let cur_item = match hierarchy.pop() {
4257 Some(s) => s,
4258 None => return Some(root_node),
4259 };
4260 let child = root_node.children.as_mut().get_mut(cur_item)?;
4261 match child {
4262 XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
4263 XmlNodeChild::Text(_) => None, }
4265}
4266
4267pub fn str_to_dom<'a>(
4270 root_nodes: &'a [XmlNodeChild],
4271 component_map: &'a ComponentMap,
4272 max_width: Option<f32>,
4273) -> Result<StyledDom, DomXmlParseError> {
4274 str_to_dom_fast(root_nodes, component_map, max_width)
4276}
4277
4278fn str_to_dom_fast<'a>(
4283 root_nodes: &'a [XmlNodeChild],
4284 component_map: &'a ComponentMap,
4285 max_width: Option<f32>,
4286) -> Result<StyledDom, DomXmlParseError> {
4287 let html_node = get_html_node(root_nodes)?;
4288 let body_node = get_body_node(html_node.children.as_ref())?;
4289
4290 let mut global_style = None;
4291
4292 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
4293 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
4294 let text = style_node.get_text_content();
4295 if !text.is_empty() {
4296 let parsed_css = Css::from_string(text.into());
4297 global_style = Some(parsed_css);
4298 }
4299 }
4300 }
4301
4302 render_dom_from_body_node_fast(&body_node, global_style, component_map, max_width)
4303 .map_err(|e| e.into())
4304}
4305
4306pub fn str_to_dom_unstyled<'a>(
4315 root_nodes: &'a [XmlNodeChild],
4316 component_map: &'a ComponentMap,
4317) -> Result<Dom, DomXmlParseError> {
4318 let html_node = get_html_node(root_nodes)?;
4319 let body_node = get_body_node(html_node.children.as_ref())?;
4320
4321 let mut global_style = None;
4322
4323 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
4324 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
4325 let text = style_node.get_text_content();
4326 if !text.is_empty() {
4327 let parsed_css = Css::from_string(text.into());
4328 global_style = Some(parsed_css);
4329 }
4330 }
4331 }
4332
4333 let body_dom = xml_node_to_dom_fast(&body_node, component_map, false)
4335 .map_err(|e| DomXmlParseError::from(e))?;
4336
4337 use crate::dom::NodeType;
4339 let root_node_type = body_dom.root.node_type.clone();
4340
4341 let mut full_dom = match root_node_type {
4342 NodeType::Html => body_dom,
4343 NodeType::Body => Dom::create_html().with_child(body_dom),
4344 _ => {
4345 let body_wrapper = Dom::create_body().with_child(body_dom);
4346 Dom::create_html().with_child(body_wrapper)
4347 }
4348 };
4349
4350 if let Some(css) = global_style {
4352 let mut css_vec: Vec<Css> = Vec::new();
4353 css_vec.push(css);
4354 full_dom.css = css_vec.into();
4355 }
4356
4357 Ok(full_dom)
4358}
4359
4360pub fn str_to_rust_code<'a>(
4363 root_nodes: &'a [XmlNodeChild],
4364 imports: &str,
4365 component_map: &'a ComponentMap,
4366) -> Result<String, CompileError> {
4367 let html_node = get_html_node(&root_nodes)?;
4368 let body_node = get_body_node(html_node.children.as_ref())?;
4369 let mut global_style = Css::empty();
4370
4371 if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
4372 if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
4373 let text = style_node.get_text_content();
4374 if !text.is_empty() {
4375 let parsed_css = azul_css::parser2::new_from_str(&text).0;
4376 global_style = parsed_css;
4377 }
4378 }
4379 }
4380
4381 global_style.sort_by_specificity();
4382
4383 let mut css_blocks = BTreeMap::new();
4384 let mut extra_blocks = VecContents::default();
4385 let app_source = compile_body_node_to_rust_code(
4386 &body_node,
4387 component_map,
4388 &mut extra_blocks,
4389 &mut css_blocks,
4390 &global_style,
4391 CssMatcher {
4392 path: Vec::new(),
4393 indices_in_parent: vec![0],
4394 children_length: vec![body_node.children.as_ref().len()],
4395 },
4396 )?;
4397
4398 let app_source = app_source
4399 .lines()
4400 .map(|l| format!(" {}", l))
4401 .collect::<Vec<String>>()
4402 .join("\r\n");
4403
4404 let t = " ";
4405 let css_blocks = css_blocks
4406 .iter()
4407 .map(|(k, v)| {
4408 let v = v
4409 .lines()
4410 .map(|l| format!("{}{}{}", t, t, l))
4411 .collect::<Vec<String>>()
4412 .join("\r\n");
4413
4414 format!(
4415 " const {}_PROPERTIES: &[NodeDataInlineCssProperty] = \
4416 &[\r\n{}\r\n{}];\r\n{}const {}: NodeDataInlineCssPropertyVec = \
4417 NodeDataInlineCssPropertyVec::from_const_slice({}_PROPERTIES);",
4418 k, v, t, t, k, k
4419 )
4420 })
4421 .collect::<Vec<_>>()
4422 .join(&format!("{}\r\n\r\n", t));
4423
4424 let mut extra_block_string = extra_blocks.format(1);
4425
4426 let main_func = "
4427
4428use azul::{
4429 app::{App, AppConfig, LayoutSolver},
4430 css::Css,
4431 dom::Dom,
4432 callbacks::{RefAny, LayoutCallbackInfo},
4433 window::{WindowCreateOptions, WindowFrame},
4434};
4435
4436struct Data { }
4437
4438extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
4439 let dom = crate::ui::render();
4440 dom.with_component_css(Css::empty()) // styles are applied inline
4441}
4442
4443fn main() {
4444 let app = App::new(RefAny::new(Data { }), AppConfig::new(LayoutSolver::Default));
4445 let mut window = WindowCreateOptions::new(render);
4446 window.state.flags.frame = WindowFrame::Maximized;
4447 app.run(window);
4448}";
4449
4450 let source_code = format!(
4451 "#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
4452 code\r\n{}\r\n{}\r\n\r\n{}{}",
4453 imports,
4454 compile_components(Vec::new()), format!(
4456 "#[allow(unused_imports)]\r\npub mod ui {{
4457
4458 pub use crate::components::*;
4459
4460 use azul::css::*;
4461 use azul::str::String as AzString;
4462 use azul::vec::{{
4463 DomVec, IdOrClassVec, NodeDataInlineCssPropertyVec,
4464 StyleBackgroundSizeVec, StyleBackgroundRepeatVec,
4465 StyleBackgroundContentVec, StyleTransformVec,
4466 StyleFontFamilyVec, StyleBackgroundPositionVec,
4467 NormalizedLinearColorStopVec, NormalizedRadialColorStopVec,
4468 }};
4469 use azul::dom::{{
4470 Dom, IdOrClass, TabIndex,
4471 IdOrClass::{{Id, Class}},
4472 NodeDataInlineCssProperty,
4473 }};\r\n\r\n{}\r\n\r\n{}
4474
4475 pub fn render() -> Dom {{\r\n{}\r\n }}\r\n}}",
4476 extra_block_string, css_blocks, app_source
4477 ),
4478 main_func,
4479 );
4480
4481 Ok(source_code)
4482}
4483
4484fn compile_components(
4486 components: Vec<(
4487 ComponentName,
4488 CompiledComponent,
4489 ComponentArguments,
4490 BTreeMap<String, String>,
4491 )>,
4492) -> String {
4493 let cs = components
4494 .iter()
4495 .map(|(name, function_body, function_args, css_blocks)| {
4496 let name = &normalize_casing(&name);
4497 let f = compile_component(name, function_args, function_body)
4498 .lines()
4499 .map(|l| format!(" {}", l))
4500 .collect::<Vec<String>>()
4501 .join("\r\n");
4502
4503 format!(
4506 "#[allow(unused_imports)]\r\npub mod {} {{\r\n use azul::dom::Dom;\r\n use \
4507 azul::str::String as AzString;\r\n{}\r\n}}",
4508 name, f
4509 )
4510 })
4511 .collect::<Vec<String>>()
4512 .join("\r\n\r\n");
4513
4514 let cs = cs
4515 .lines()
4516 .map(|l| format!(" {}", l))
4517 .collect::<Vec<String>>()
4518 .join("\r\n");
4519
4520 if cs.is_empty() {
4521 cs
4522 } else {
4523 format!("pub mod components {{\r\n{}\r\n}}", cs)
4524 }
4525}
4526
4527fn format_component_args(component_args: &ComponentArgumentVec) -> String {
4528 let mut args = component_args
4529 .iter()
4530 .map(|a| format!("{}: {}", a.name, a.arg_type))
4531 .collect::<Vec<String>>();
4532
4533 args.sort_by(|a, b| b.cmp(&a));
4534
4535 args.join(", ")
4536}
4537
4538pub fn compile_component(
4539 component_name: &str,
4540 component_args: &ComponentArguments,
4541 component_function_body: &str,
4542) -> String {
4543 let component_name = &normalize_casing(&component_name);
4544 let function_args = format_component_args(&component_args.args);
4545 let component_function_body = component_function_body
4546 .lines()
4547 .map(|l| format!(" {}", l))
4548 .collect::<Vec<String>>()
4549 .join("\r\n");
4550 let should_inline = component_function_body.lines().count() == 1;
4551 format!(
4552 "{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
4553 if should_inline { "#[inline]\r\n" } else { "" },
4554 if component_args.accepts_text {
4556 "text: AzString"
4557 } else {
4558 ""
4559 },
4560 if function_args.is_empty() || !component_args.accepts_text {
4561 ""
4562 } else {
4563 ", "
4564 },
4565 function_args,
4566 component_function_body,
4567 )
4568}
4569
4570fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
4572 attr?.as_str().trim().parse::<f32>().ok()
4573}
4574
4575fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
4577 let nums: Vec<f32> = pts
4578 .split(|c: char| c == ',' || c.is_ascii_whitespace())
4579 .filter(|s| !s.is_empty())
4580 .filter_map(|s| s.parse::<f32>().ok())
4581 .collect();
4582 if nums.len() < 4 || nums.len() % 2 != 0 {
4583 return None;
4584 }
4585 let mut elements = Vec::new();
4586 let points: Vec<azul_css::props::basic::SvgPoint> = nums
4587 .chunks_exact(2)
4588 .map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
4589 .collect();
4590 for w in points.windows(2) {
4591 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(w[0], w[1])));
4592 }
4593 if close && points.len() >= 2 {
4594 let first = points[0];
4595 let last = *points.last().unwrap();
4596 if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
4597 elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(last, first)));
4598 }
4599 }
4600 Some(crate::svg::SvgMultiPolygon {
4601 rings: crate::svg::SvgPathVec::from_vec(vec![
4602 crate::svg::SvgPath {
4603 items: crate::svg::SvgPathElementVec::from_vec(elements),
4604 }
4605 ]),
4606 })
4607}
4608
4609fn xml_node_to_dom_fast<'a>(
4612 xml_node: &'a XmlNode,
4613 component_map: &'a ComponentMap,
4614 inside_svg: bool,
4615) -> Result<Dom, RenderDomError> {
4616 use crate::dom::{Dom, NodeType, IdOrClass, TabIndex};
4617
4618 let component_name = normalize_casing(&xml_node.node_type);
4619
4620 let node_type = tag_to_node_type(&component_name);
4622 let mut dom = Dom::create_node(node_type);
4623
4624 let mut ids_and_classes = Vec::new();
4626 if let Some(id_str) = xml_node.attributes.get_key("id") {
4627 for id in id_str.split_whitespace() {
4628 ids_and_classes.push(IdOrClass::Id(id.into()));
4629 }
4630 }
4631 if let Some(class_str) = xml_node.attributes.get_key("class") {
4632 for class in class_str.split_whitespace() {
4633 ids_and_classes.push(IdOrClass::Class(class.into()));
4634 }
4635 }
4636 if !ids_and_classes.is_empty() {
4637 dom.root.set_ids_and_classes(ids_and_classes.into());
4638 }
4639
4640 if let Some(focusable) = xml_node.attributes.get_key("focusable").and_then(|f| parse_bool(f.as_str())) {
4642 match focusable {
4643 true => dom.root.set_tab_index(TabIndex::Auto),
4644 false => dom.root.set_tab_index(TabIndex::NoKeyboardFocus),
4645 }
4646 }
4647
4648 if let Some(tab_index) = xml_node.attributes.get_key("tabindex").and_then(|val| val.parse::<isize>().ok()) {
4650 match tab_index {
4651 0 => dom.root.set_tab_index(TabIndex::Auto),
4652 i if i > 0 => dom.root.set_tab_index(TabIndex::OverrideInParent(i as u32)),
4653 _ => dom.root.set_tab_index(TabIndex::NoKeyboardFocus),
4654 }
4655 }
4656
4657 if let Some(style) = xml_node.attributes.get_key("style") {
4659 let css_key_map = azul_css::props::property::get_css_key_map();
4660 let mut attributes = Vec::new();
4661 for s in style.as_str().split(";") {
4662 let mut s = s.split(":");
4663 let key = match s.next() { Some(s) => s, None => continue };
4664 let value = match s.next() { Some(s) => s, None => continue };
4665 let _ = azul_css::parser2::parse_css_declaration(
4666 key.trim(), value.trim(),
4667 azul_css::parser2::ErrorLocationRange::default(),
4668 &css_key_map, &mut Vec::new(), &mut attributes,
4669 );
4670 }
4671 let props = attributes.into_iter().filter_map(|s| {
4672 use azul_css::dynamic_selector::CssPropertyWithConditions;
4673 match s {
4674 CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
4675 _ => None,
4676 }
4677 }).collect::<Vec<_>>();
4678 if !props.is_empty() {
4679 dom.root.set_css_props(props.into());
4680 }
4681 }
4682
4683 let tag = component_name.as_str();
4685 let child_inside_svg = inside_svg || tag == "svg";
4686 let is_svg_shape = inside_svg && matches!(tag, "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline");
4687
4688 if is_svg_shape {
4689 let clip = match tag {
4690 "path" => {
4691 xml_node.attributes.get_key("d").and_then(|d| {
4692 crate::svg_path_parser::parse_svg_path_d(d.as_str()).ok()
4693 })
4694 }
4695 "circle" => {
4696 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
4697 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
4698 let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
4699 if r > 0.0 {
4700 Some(crate::svg::SvgMultiPolygon {
4701 rings: crate::svg::SvgPathVec::from_vec(vec![
4702 crate::svg_path_parser::svg_circle_to_paths(cx, cy, r)
4703 ]),
4704 })
4705 } else {
4706 None
4707 }
4708 }
4709 "rect" => {
4710 let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
4711 let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
4712 let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
4713 let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
4714 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
4715 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
4716 if w > 0.0 && h > 0.0 {
4717 Some(crate::svg::SvgMultiPolygon {
4718 rings: crate::svg::SvgPathVec::from_vec(vec![
4719 crate::svg_path_parser::svg_rect_to_path(x, y, w, h, rx, ry)
4720 ]),
4721 })
4722 } else {
4723 None
4724 }
4725 }
4726 "ellipse" => {
4727 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
4728 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
4729 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
4730 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
4731 if rx > 0.0 && ry > 0.0 {
4732 use azul_css::props::basic::{SvgPoint, SvgCubicCurve};
4734 const KAPPA: f32 = 0.5522847498;
4735 let kx = rx * KAPPA;
4736 let ky = ry * KAPPA;
4737 let elements = vec![
4738 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
4739 start: SvgPoint { x: cx, y: cy - ry },
4740 ctrl_1: SvgPoint { x: cx + kx, y: cy - ry },
4741 ctrl_2: SvgPoint { x: cx + rx, y: cy - ky },
4742 end: SvgPoint { x: cx + rx, y: cy },
4743 }),
4744 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
4745 start: SvgPoint { x: cx + rx, y: cy },
4746 ctrl_1: SvgPoint { x: cx + rx, y: cy + ky },
4747 ctrl_2: SvgPoint { x: cx + kx, y: cy + ry },
4748 end: SvgPoint { x: cx, y: cy + ry },
4749 }),
4750 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
4751 start: SvgPoint { x: cx, y: cy + ry },
4752 ctrl_1: SvgPoint { x: cx - kx, y: cy + ry },
4753 ctrl_2: SvgPoint { x: cx - rx, y: cy + ky },
4754 end: SvgPoint { x: cx - rx, y: cy },
4755 }),
4756 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
4757 start: SvgPoint { x: cx - rx, y: cy },
4758 ctrl_1: SvgPoint { x: cx - rx, y: cy - ky },
4759 ctrl_2: SvgPoint { x: cx - kx, y: cy - ry },
4760 end: SvgPoint { x: cx, y: cy - ry },
4761 }),
4762 ];
4763 Some(crate::svg::SvgMultiPolygon {
4764 rings: crate::svg::SvgPathVec::from_vec(vec![
4765 crate::svg::SvgPath { items: crate::svg::SvgPathElementVec::from_vec(elements) }
4766 ]),
4767 })
4768 } else {
4769 None
4770 }
4771 }
4772 "line" => {
4773 let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
4774 let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
4775 let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
4776 let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
4777 Some(crate::svg::SvgMultiPolygon {
4778 rings: crate::svg::SvgPathVec::from_vec(vec![
4779 crate::svg::SvgPath {
4780 items: crate::svg::SvgPathElementVec::from_vec(vec![
4781 crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
4782 azul_css::props::basic::SvgPoint { x: x1, y: y1 },
4783 azul_css::props::basic::SvgPoint { x: x2, y: y2 },
4784 ))
4785 ]),
4786 }
4787 ]),
4788 })
4789 }
4790 "polygon" | "polyline" => {
4791 xml_node.attributes.get_key("points").and_then(|pts| {
4792 parse_svg_points(pts.as_str(), tag == "polygon")
4793 })
4794 }
4795 _ => None,
4796 };
4797
4798 if let Some(mp) = clip {
4799 dom.root.set_svg_data(crate::dom::SvgNodeData::Path(mp));
4800 }
4801
4802 }
4803
4804 let mut children = Vec::new();
4806 for child in xml_node.children.as_ref().iter() {
4807 match child {
4808 XmlNodeChild::Element(child_node) => {
4809 let child_dom = xml_node_to_dom_fast(child_node, component_map, child_inside_svg)?;
4810 children.push(child_dom);
4811 }
4812 XmlNodeChild::Text(text) => {
4813 let text_dom = Dom::create_text(AzString::from(text.as_str()));
4814 children.push(text_dom);
4815 }
4816 }
4817 }
4818
4819 if !children.is_empty() {
4820 dom = dom.with_children(children.into());
4821 }
4822
4823 Ok(dom)
4824}
4825
4826pub struct CompactDomBuilder {
4829 hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
4830 node_data: Vec<crate::dom::NodeData>,
4831 css: Vec<crate::dom::CssWithNodeId>,
4832 stack: Vec<(usize, Option<usize>)>,
4834}
4835
4836impl CompactDomBuilder {
4837 pub fn new() -> Self {
4838 Self {
4839 hierarchy: Vec::new(),
4840 node_data: Vec::new(),
4841 css: Vec::new(),
4842 stack: Vec::new(),
4843 }
4844 }
4845
4846 pub fn with_capacity(cap: usize) -> Self {
4847 Self {
4848 hierarchy: Vec::with_capacity(cap),
4849 node_data: Vec::with_capacity(cap),
4850 css: Vec::new(),
4851 stack: Vec::new(),
4852 }
4853 }
4854
4855 pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
4857 use crate::id::NodeId;
4858 use crate::styled_dom::NodeHierarchyItem;
4859
4860 let idx = self.hierarchy.len();
4861
4862 let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
4864 NodeId::into_raw(&Some(NodeId::new(parent_idx)))
4865 } else {
4866 0 };
4868
4869 let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
4871 prev_child.map(|pi| NodeId::into_raw(&Some(NodeId::new(pi)))).unwrap_or(0)
4872 } else {
4873 0
4874 };
4875
4876 if let Some(&(_, Some(prev_idx))) = self.stack.last() {
4878 self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
4879 }
4880
4881 if let Some(parent) = self.stack.last_mut() {
4883 parent.1 = Some(idx);
4884 }
4885
4886 self.hierarchy.push(NodeHierarchyItem {
4888 parent: parent_raw,
4889 previous_sibling: prev_sibling_raw,
4890 next_sibling: 0, last_child: 0, });
4893 self.node_data.push(node_data);
4894
4895 self.stack.push((idx, None));
4897 }
4898
4899 pub fn close_node(&mut self) {
4901 use crate::id::NodeId;
4902
4903 if let Some((idx, last_child_idx)) = self.stack.pop() {
4904 self.hierarchy[idx].last_child = last_child_idx
4906 .map(|lc| NodeId::into_raw(&Some(NodeId::new(lc))))
4907 .unwrap_or(0);
4908 }
4909 }
4910
4911 pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
4913 self.open_node(node_data);
4914 self.close_node();
4915 }
4916
4917 pub fn add_css(&mut self, node_id: usize, css: azul_css::css::Css) {
4919 self.css.push(crate::dom::CssWithNodeId { node_id, css });
4920 }
4921
4922 pub fn finish(self) -> crate::dom::FastDom {
4924 crate::dom::FastDom {
4925 node_hierarchy: self.hierarchy.into(),
4926 node_data: self.node_data.into(),
4927 css: self.css.into(),
4928 }
4929 }
4930}
4931
4932fn xml_node_to_fast_dom<'a>(
4935 xml_node: &'a XmlNode,
4936 component_map: &'a ComponentMap,
4937 inside_svg: bool,
4938 builder: &mut CompactDomBuilder,
4939) -> Result<(), RenderDomError> {
4940 use crate::dom::{Dom, NodeType, NodeData, IdOrClass, TabIndex};
4941
4942 let component_name = normalize_casing(&xml_node.node_type);
4943 let node_type = tag_to_node_type(&component_name);
4944 let mut node_data = NodeData::create_node(node_type);
4945
4946 let mut ids_and_classes = Vec::new();
4948 if let Some(id_str) = xml_node.attributes.get_key("id") {
4949 for id in id_str.split_whitespace() {
4950 ids_and_classes.push(IdOrClass::Id(id.into()));
4951 }
4952 }
4953 if let Some(class_str) = xml_node.attributes.get_key("class") {
4954 for class in class_str.split_whitespace() {
4955 ids_and_classes.push(IdOrClass::Class(class.into()));
4956 }
4957 }
4958 if !ids_and_classes.is_empty() {
4959 node_data.set_ids_and_classes(ids_and_classes.into());
4960 }
4961
4962 if let Some(focusable) = xml_node.attributes.get_key("focusable").and_then(|f| parse_bool(f.as_str())) {
4964 match focusable {
4965 true => node_data.set_tab_index(TabIndex::Auto),
4966 false => node_data.set_tab_index(TabIndex::NoKeyboardFocus),
4967 }
4968 }
4969
4970 if let Some(tab_index) = xml_node.attributes.get_key("tabindex").and_then(|val| val.parse::<isize>().ok()) {
4972 match tab_index {
4973 0 => node_data.set_tab_index(TabIndex::Auto),
4974 i if i > 0 => node_data.set_tab_index(TabIndex::OverrideInParent(i as u32)),
4975 _ => node_data.set_tab_index(TabIndex::NoKeyboardFocus),
4976 }
4977 }
4978
4979 if let Some(style) = xml_node.attributes.get_key("style") {
4981 let css_key_map = azul_css::props::property::get_css_key_map();
4982 let mut attributes = Vec::new();
4983 for s in style.as_str().split(";") {
4984 let mut s = s.split(":");
4985 let key = match s.next() { Some(s) => s, None => continue };
4986 let value = match s.next() { Some(s) => s, None => continue };
4987 let _ = azul_css::parser2::parse_css_declaration(
4988 key.trim(), value.trim(),
4989 azul_css::parser2::ErrorLocationRange::default(),
4990 &css_key_map, &mut Vec::new(), &mut attributes,
4991 );
4992 }
4993 let props = attributes.into_iter().filter_map(|s| {
4994 use azul_css::dynamic_selector::CssPropertyWithConditions;
4995 match s {
4996 CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
4997 _ => None,
4998 }
4999 }).collect::<Vec<_>>();
5000 if !props.is_empty() {
5001 node_data.set_css_props(props.into());
5002 }
5003 }
5004
5005 let tag = component_name.as_str();
5007 let child_inside_svg = inside_svg || tag == "svg";
5008 let is_svg_shape = inside_svg && matches!(tag, "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline");
5009
5010 if is_svg_shape {
5011 let clip = match tag {
5012 "path" => {
5013 xml_node.attributes.get_key("d").and_then(|d| {
5014 crate::svg_path_parser::parse_svg_path_d(d.as_str()).ok()
5015 })
5016 }
5017 "circle" => {
5018 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5019 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5020 let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
5021 if r > 0.0 {
5022 Some(crate::svg::SvgMultiPolygon {
5023 rings: crate::svg::SvgPathVec::from_vec(vec![
5024 crate::svg_path_parser::svg_circle_to_paths(cx, cy, r)
5025 ]),
5026 })
5027 } else {
5028 None
5029 }
5030 }
5031 "rect" => {
5032 let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
5033 let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
5034 let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
5035 let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
5036 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5037 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
5038 if w > 0.0 && h > 0.0 {
5039 Some(crate::svg::SvgMultiPolygon {
5040 rings: crate::svg::SvgPathVec::from_vec(vec![
5041 crate::svg_path_parser::svg_rect_to_path(x, y, w, h, rx, ry)
5042 ]),
5043 })
5044 } else {
5045 None
5046 }
5047 }
5048 "ellipse" => {
5049 let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
5050 let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
5051 let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
5052 let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
5053 if rx > 0.0 && ry > 0.0 {
5054 use azul_css::props::basic::{SvgPoint, SvgCubicCurve};
5055 const KAPPA: f32 = 0.5522847498;
5056 let kx = rx * KAPPA;
5057 let ky = ry * KAPPA;
5058 let elements = vec![
5059 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5060 start: SvgPoint { x: cx, y: cy - ry },
5061 ctrl_1: SvgPoint { x: cx + kx, y: cy - ry },
5062 ctrl_2: SvgPoint { x: cx + rx, y: cy - ky },
5063 end: SvgPoint { x: cx + rx, y: cy },
5064 }),
5065 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5066 start: SvgPoint { x: cx + rx, y: cy },
5067 ctrl_1: SvgPoint { x: cx + rx, y: cy + ky },
5068 ctrl_2: SvgPoint { x: cx + kx, y: cy + ry },
5069 end: SvgPoint { x: cx, y: cy + ry },
5070 }),
5071 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5072 start: SvgPoint { x: cx, y: cy + ry },
5073 ctrl_1: SvgPoint { x: cx - kx, y: cy + ry },
5074 ctrl_2: SvgPoint { x: cx - rx, y: cy + ky },
5075 end: SvgPoint { x: cx - rx, y: cy },
5076 }),
5077 crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
5078 start: SvgPoint { x: cx - rx, y: cy },
5079 ctrl_1: SvgPoint { x: cx - rx, y: cy - ky },
5080 ctrl_2: SvgPoint { x: cx - kx, y: cy - ry },
5081 end: SvgPoint { x: cx, y: cy - ry },
5082 }),
5083 ];
5084 Some(crate::svg::SvgMultiPolygon {
5085 rings: crate::svg::SvgPathVec::from_vec(vec![
5086 crate::svg::SvgPath { items: crate::svg::SvgPathElementVec::from_vec(elements) }
5087 ]),
5088 })
5089 } else {
5090 None
5091 }
5092 }
5093 "line" => {
5094 let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
5095 let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
5096 let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
5097 let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
5098 Some(crate::svg::SvgMultiPolygon {
5099 rings: crate::svg::SvgPathVec::from_vec(vec![
5100 crate::svg::SvgPath {
5101 items: crate::svg::SvgPathElementVec::from_vec(vec![
5102 crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
5103 azul_css::props::basic::SvgPoint { x: x1, y: y1 },
5104 azul_css::props::basic::SvgPoint { x: x2, y: y2 },
5105 ))
5106 ]),
5107 }
5108 ]),
5109 })
5110 }
5111 "polygon" | "polyline" => {
5112 xml_node.attributes.get_key("points").and_then(|pts| {
5113 parse_svg_points(pts.as_str(), tag == "polygon")
5114 })
5115 }
5116 _ => None,
5117 };
5118 if let Some(mp) = clip {
5119 node_data.set_svg_data(crate::dom::SvgNodeData::Path(mp));
5120 }
5121 }
5122
5123 builder.open_node(node_data);
5125
5126 for child in xml_node.children.as_ref().iter() {
5128 match child {
5129 XmlNodeChild::Element(child_node) => {
5130 xml_node_to_fast_dom(child_node, component_map, child_inside_svg, builder)?;
5131 }
5132 XmlNodeChild::Text(text) => {
5133 builder.add_leaf(NodeData::create_text(AzString::from(text.as_str())));
5134 }
5135 }
5136 }
5137
5138 builder.close_node();
5140
5141 Ok(())
5142}
5143
5144fn render_dom_from_body_node_fast<'a>(
5147 body_node: &'a XmlNode,
5148 mut global_css: Option<Css>,
5149 component_map: &'a ComponentMap,
5150 max_width: Option<f32>,
5151) -> Result<StyledDom, RenderDomError> {
5152 use crate::dom::{NodeData, NodeType};
5153
5154 let mut builder = CompactDomBuilder::new();
5155
5156 builder.open_node(NodeData::create_node(NodeType::Html));
5159 xml_node_to_fast_dom(body_node, component_map, false, &mut builder)?;
5161 builder.close_node();
5163
5164 let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
5166 if let Some(max_width) = max_width {
5167 let max_width_css = Css::from_string(
5168 format!("html {{ max-width: {max_width}px; }}").into(),
5169 );
5170 combined_rules.extend(max_width_css.rules.into_library_owned_vec());
5171 }
5172 if let Some(css) = global_css.take() {
5173 combined_rules.extend(css.rules.into_library_owned_vec());
5174 }
5175 let combined_css = Css::new(combined_rules);
5176
5177 let mut fast_dom = builder.finish();
5179 fast_dom.css = vec![crate::dom::CssWithNodeId {
5180 node_id: 0, css: combined_css,
5182 }].into();
5183
5184 let styled = StyledDom::create_from_fast_dom(fast_dom);
5186 Ok(styled)
5187}
5188
5189fn set_stringified_attributes(
5192 dom_string: &mut String,
5193 xml_attributes: &XmlAttributeMap,
5194 filtered_xml_attributes: &ComponentArgumentVec,
5195 tabs: usize,
5196) {
5197 let t0 = String::from(" ").repeat(tabs);
5198 let t = String::from(" ").repeat(tabs + 1);
5199
5200 let mut ids_and_classes = String::new();
5202
5203 for id in xml_attributes
5204 .get_key("id")
5205 .map(|s| s.split_whitespace().collect::<Vec<_>>())
5206 .unwrap_or_default()
5207 {
5208 ids_and_classes.push_str(&format!(
5209 "{} Id(AzString::from_const_str(\"{}\")),\r\n",
5210 t0,
5211 format_args_dynamic(id, &filtered_xml_attributes)
5212 ));
5213 }
5214
5215 for class in xml_attributes
5216 .get_key("class")
5217 .map(|s| s.split_whitespace().collect::<Vec<_>>())
5218 .unwrap_or_default()
5219 {
5220 ids_and_classes.push_str(&format!(
5221 "{} Class(AzString::from_const_str(\"{}\")),\r\n",
5222 t0,
5223 format_args_dynamic(class, &filtered_xml_attributes)
5224 ));
5225 }
5226
5227 if !ids_and_classes.is_empty() {
5228 use azul_css::format_rust_code::GetHash;
5229 let id = ids_and_classes.get_hash();
5230 dom_string.push_str(&format!(
5231 "\r\n{t0}.with_ids_and_classes({{\r\n{t}const IDS_AND_CLASSES_{id}: &[IdOrClass] = \
5232 &[\r\n{t}{ids_and_classes}\r\n{t}];\r\\
5233 n{t}IdOrClassVec::from_const_slice(IDS_AND_CLASSES_{id})\r\n{t0}}})",
5234 t0 = t0,
5235 t = t,
5236 ids_and_classes = ids_and_classes,
5237 id = id
5238 ));
5239 }
5240
5241 if let Some(focusable) = xml_attributes
5242 .get_key("focusable")
5243 .map(|f| format_args_dynamic(f, &filtered_xml_attributes))
5244 .and_then(|f| parse_bool(&f))
5245 {
5246 match focusable {
5247 true => dom_string.push_str(&format!("\r\n{}.with_tab_index(TabIndex::Auto)", t)),
5248 false => dom_string.push_str(&format!(
5249 "\r\n{}.with_tab_index(TabIndex::NoKeyboardFocus)",
5250 t
5251 )),
5252 }
5253 }
5254
5255 if let Some(tab_index) = xml_attributes
5256 .get_key("tabindex")
5257 .map(|val| format_args_dynamic(val, &filtered_xml_attributes))
5258 .and_then(|val| val.parse::<isize>().ok())
5259 {
5260 match tab_index {
5261 0 => dom_string.push_str(&format!("\r\n{}.with_tab_index(TabIndex::Auto)", t)),
5262 i if i > 0 => dom_string.push_str(&format!(
5263 "\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
5264 t, i as usize
5265 )),
5266 _ => dom_string.push_str(&format!(
5267 "\r\n{}.with_tab_index(TabIndex::NoKeyboardFocus)",
5268 t
5269 )),
5270 }
5271 }
5272}
5273
5274#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
5276pub enum DynamicItem {
5277 Var {
5279 name: String,
5280 format_spec: Option<String>,
5282 },
5283 Str(String),
5284}
5285
5286pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
5304 use self::DynamicItem::*;
5305
5306 let input: Vec<char> = input.chars().collect();
5307 let input_chars_len = input.len();
5308
5309 let mut items = Vec::new();
5310 let mut current_idx = 0;
5311 let mut last_idx = 0;
5312
5313 while current_idx < input_chars_len {
5314 let c = input[current_idx];
5315 match c {
5316 '{' if input.get(current_idx + 1).copied() != Some('{') => {
5317 let mut start_offset = 1;
5319 let mut has_found_variable = false;
5320 while let Some(c) = input.get(current_idx + start_offset) {
5321 if c.is_whitespace() {
5322 break;
5323 }
5324 if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
5325 {
5326 start_offset += 1;
5327 has_found_variable = true;
5328 break;
5329 }
5330 start_offset += 1;
5331 }
5332
5333 if has_found_variable {
5337 if last_idx != current_idx {
5338 items.push(Str(input[last_idx..current_idx].iter().collect()));
5339 }
5340
5341 let var_content: String = input
5343 [(current_idx + 1)..(current_idx + start_offset - 1)]
5344 .iter()
5345 .collect();
5346 let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
5348 let name = var_content[..colon_pos].to_string();
5349 let spec = var_content[(colon_pos + 1)..].to_string();
5350 (name, Some(spec))
5351 } else {
5352 (var_content, None)
5353 };
5354 items.push(Var { name: var_name, format_spec });
5355 current_idx = current_idx + start_offset;
5356 last_idx = current_idx;
5357 } else {
5358 current_idx += start_offset;
5359 }
5360 }
5361 _ => {
5362 current_idx += 1;
5363 }
5364 }
5365 }
5366
5367 if current_idx != last_idx {
5368 items.push(Str(input[last_idx..].iter().collect()));
5369 }
5370
5371 for item in &mut items {
5372 if let Str(s) = item {
5374 *s = s.replace("{{", "{").replace("}}", "}");
5375 }
5376 }
5377
5378 items
5379}
5380
5381fn combine_and_replace_dynamic_items(
5388 input: &[DynamicItem],
5389 variables: &ComponentArgumentVec,
5390) -> String {
5391 let mut s = String::new();
5392
5393 for item in input {
5394 match item {
5395 DynamicItem::Var { name, format_spec } => {
5396 let variable_name = normalize_casing(name.trim());
5397 match variables
5398 .iter()
5399 .find(|s| s.name.as_str() == variable_name)
5400 .map(|q| &q.arg_type)
5401 {
5402 Some(resolved_var) => {
5403 s.push_str(&resolved_var);
5405 }
5406 None => {
5407 s.push('{');
5408 s.push_str(name);
5409 if let Some(spec) = format_spec {
5410 s.push(':');
5411 s.push_str(spec);
5412 }
5413 s.push('}');
5414 }
5415 }
5416 }
5417 DynamicItem::Str(dynamic_str) => {
5418 s.push_str(&dynamic_str);
5419 }
5420 }
5421 }
5422
5423 s
5424}
5425
5426pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
5445 let dynamic_str_items = split_dynamic_string(input);
5446 combine_and_replace_dynamic_items(&dynamic_str_items, variables)
5447}
5448
5449pub fn prepare_string(input: &str) -> String {
5451 const SPACE: &str = " ";
5452 const RETURN: &str = "\n";
5453
5454 let input = input.trim();
5455
5456 if input.is_empty() {
5457 return String::new();
5458 }
5459
5460 let input = input.replace("<", "<");
5461 let input = input.replace(">", ">");
5462
5463 let input_len = input.len();
5464 let mut final_lines: Vec<String> = Vec::new();
5465 let mut last_line_was_empty = false;
5466
5467 for line in input.lines() {
5468 let line = line.trim();
5469 let line = line.replace(" ", " ");
5470 let current_line_is_empty = line.is_empty();
5471
5472 if !current_line_is_empty {
5473 if last_line_was_empty {
5474 final_lines.push(format!("{}{}", RETURN, line));
5475 } else {
5476 final_lines.push(line.to_string());
5477 }
5478 }
5479
5480 last_line_was_empty = current_line_is_empty;
5481 }
5482
5483 let line_len = final_lines.len();
5484 let mut target = String::with_capacity(input_len);
5485 for (line_idx, line) in final_lines.iter().enumerate() {
5486 if !(line.starts_with(RETURN) || line_idx == 0 || line_idx == line_len.saturating_sub(1)) {
5487 target.push_str(SPACE);
5488 }
5489 target.push_str(line);
5490 }
5491 target
5492}
5493
5494pub fn parse_bool(input: &str) -> Option<bool> {
5496 match input {
5497 "true" => Some(true),
5498 "false" => Some(false),
5499 _ => None,
5500 }
5501}
5502
5503#[derive(Clone)]
5504pub struct CssMatcher {
5505 path: Vec<CssPathSelector>,
5506 indices_in_parent: Vec<usize>,
5507 children_length: Vec<usize>,
5508}
5509
5510impl CssMatcher {
5511 fn get_hash(&self) -> u64 {
5512 use core::hash::Hash;
5513
5514 use std::hash::Hasher;
5515
5516 let mut hasher = std::hash::DefaultHasher::new();
5517 for p in self.path.iter() {
5518 p.hash(&mut hasher);
5519 }
5520 hasher.finish()
5521 }
5522}
5523
5524impl CssMatcher {
5525 fn matches(&self, path: &CssPath) -> bool {
5526 use azul_css::css::CssPathSelector::*;
5527
5528 use crate::style::{CssGroupIterator, CssGroupSplitReason};
5529
5530 if self.path.is_empty() {
5531 return false;
5532 }
5533 if path.selectors.as_ref().is_empty() {
5534 return false;
5535 }
5536
5537 let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
5539 path_groups.reverse();
5540
5541 if path_groups.is_empty() {
5542 return false;
5543 }
5544 let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
5545 self_groups.reverse();
5546 if self_groups.is_empty() {
5547 return false;
5548 }
5549
5550 if self.indices_in_parent.len() != self_groups.len() {
5551 return false;
5552 }
5553 if self.children_length.len() != self_groups.len() {
5554 return false;
5555 }
5556
5557 let mut cur_selfgroup_scan = 0;
5571 let mut cur_pathgroup_scan = 0;
5572 let mut valid = false;
5573 let mut path_group = path_groups[cur_pathgroup_scan].clone();
5574
5575 while cur_selfgroup_scan < self_groups.len() {
5576 let mut advance = None;
5577
5578 for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
5580 let gm = group_matches(
5581 &path_group.0,
5582 &self_groups[cur_selfgroup_scan + id].0,
5583 self.indices_in_parent[cur_selfgroup_scan + id],
5584 self.children_length[cur_selfgroup_scan + id],
5585 );
5586
5587 if gm {
5588 advance = Some(id);
5591 break;
5592 }
5593 }
5594
5595 match advance {
5596 Some(n) => {
5597 if cur_pathgroup_scan == path_groups.len() - 1 {
5600 return cur_selfgroup_scan + n == self_groups.len() - 1;
5602 } else {
5603 cur_pathgroup_scan += 1;
5604 cur_selfgroup_scan += n;
5605 path_group = path_groups[cur_pathgroup_scan].clone();
5606 }
5607 }
5608 None => return false, }
5610 }
5611
5612 return cur_pathgroup_scan == path_groups.len() - 1;
5614 }
5615}
5616
5617fn group_matches(
5620 a: &[&CssPathSelector],
5621 b: &[&CssPathSelector],
5622 idx_in_parent: usize,
5623 parent_children: usize,
5624) -> bool {
5625 use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::*};
5626
5627 for selector in a {
5628 match selector {
5629 Global => {}
5631 PseudoSelector(CssPathPseudoSelector::Hover) => {}
5632 PseudoSelector(CssPathPseudoSelector::Active) => {}
5633 PseudoSelector(CssPathPseudoSelector::Focus) => {}
5634
5635 Type(tag) => {
5636 if !b.iter().any(|t| **t == Type(tag.clone())) {
5637 return false;
5638 }
5639 }
5640 Class(class) => {
5641 if !b.iter().any(|t| **t == Class(class.clone())) {
5642 return false;
5643 }
5644 }
5645 Id(id) => {
5646 if !b.iter().any(|t| **t == Id(id.clone())) {
5647 return false;
5648 }
5649 }
5650 PseudoSelector(CssPathPseudoSelector::First) => {
5651 if idx_in_parent != 0 {
5652 return false;
5653 }
5654 }
5655 PseudoSelector(CssPathPseudoSelector::Last) => {
5656 if idx_in_parent != parent_children.saturating_sub(1) {
5657 return false;
5658 }
5659 }
5660 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
5661 if idx_in_parent != *i as usize {
5662 return false;
5663 }
5664 }
5665 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
5666 if idx_in_parent % 2 != 0 {
5667 return false;
5668 }
5669 }
5670 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
5671 if idx_in_parent % 2 == 0 {
5672 return false;
5673 }
5674 }
5675 PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
5676 if idx_in_parent.saturating_sub(p.offset as usize) % p.pattern_repeat as usize != 0
5677 {
5678 return false;
5679 }
5680 }
5681
5682 _ => return false, }
5684 }
5685
5686 true
5687}
5688
5689struct CssBlock {
5690 ending: Option<CssPathPseudoSelector>,
5691 block: CssRuleBlock,
5692}
5693
5694pub fn compile_body_node_to_rust_code<'a>(
5695 body_node: &'a XmlNode,
5696 component_map: &'a ComponentMap,
5697 extra_blocks: &mut VecContents,
5698 css_blocks: &mut BTreeMap<String, String>,
5699 css: &Css,
5700 mut matcher: CssMatcher,
5701) -> Result<String, CompileError> {
5702 use azul_css::css::CssDeclaration;
5703
5704 let t = "";
5705 let t2 = " ";
5706 let mut dom_string = String::from("Dom::create_body()");
5707 let node_type = CssPathSelector::Type(NodeTypeTag::Body);
5708 matcher.path.push(node_type);
5709
5710 let ids = body_node
5711 .attributes
5712 .get_key("id")
5713 .map(|s| s.split_whitespace().collect::<Vec<_>>())
5714 .unwrap_or_default();
5715 matcher.path.extend(
5716 ids.into_iter()
5717 .map(|id| CssPathSelector::Id(id.to_string().into())),
5718 );
5719 let classes = body_node
5720 .attributes
5721 .get_key("class")
5722 .map(|s| s.split_whitespace().collect::<Vec<_>>())
5723 .unwrap_or_default();
5724 matcher.path.extend(
5725 classes
5726 .into_iter()
5727 .map(|class| CssPathSelector::Class(class.to_string().into())),
5728 );
5729
5730 let matcher_hash = matcher.get_hash();
5731 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
5732 if !css_blocks_for_this_node.is_empty() {
5733 use azul_css::props::property::format_static_css_prop;
5734
5735 let css_strings = css_blocks_for_this_node
5736 .iter()
5737 .rev()
5738 .map(|css_block| {
5739 let wrapper = match css_block.ending {
5740 Some(CssPathPseudoSelector::Hover) => "Hover",
5741 Some(CssPathPseudoSelector::Active) => "Active",
5742 Some(CssPathPseudoSelector::Focus) => "Focus",
5743 _ => "Normal",
5744 };
5745
5746 for declaration in css_block.block.declarations.as_ref().iter() {
5747 let prop = match declaration {
5748 CssDeclaration::Static(s) => s,
5749 CssDeclaration::Dynamic(d) => &d.default_value,
5750 };
5751 extra_blocks.insert_from_css_property(prop);
5752 }
5753
5754 let formatted = css_block
5755 .block
5756 .declarations
5757 .as_ref()
5758 .iter()
5759 .rev()
5760 .map(|s| match &s {
5761 CssDeclaration::Static(s) => format!(
5762 "NodeDataInlineCssProperty::{}({})",
5763 wrapper,
5764 format_static_css_prop(s, 1)
5765 ),
5766 CssDeclaration::Dynamic(d) => format!(
5767 "NodeDataInlineCssProperty::{}({})",
5768 wrapper,
5769 format_static_css_prop(&d.default_value, 1)
5770 ),
5771 })
5772 .collect::<Vec<String>>();
5773
5774 format!("// {}\r\n{}", css_block.block.path, formatted.join(",\r\n"))
5775 })
5776 .collect::<Vec<_>>()
5777 .join(",\r\n");
5778
5779 css_blocks.insert(format!("CSS_MATCH_{:09}", matcher_hash), css_strings);
5780 dom_string.push_str(&format!(
5781 "\r\n{}.with_inline_css_props(CSS_MATCH_{:09})",
5782 t2, matcher_hash
5783 ));
5784 }
5785
5786 if !body_node.children.as_ref().is_empty() {
5787 use azul_css::format_rust_code::GetHash;
5788 let children_hash = body_node.children.as_ref().get_hash();
5789 dom_string.push_str(&format!("\r\n.with_children(DomVec::from_vec(vec![\r\n"));
5790
5791 for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
5792 match child {
5793 XmlNodeChild::Element(child_node) => {
5794 let mut matcher = matcher.clone();
5795 matcher.path.push(CssPathSelector::Children);
5796 matcher.indices_in_parent.push(child_idx);
5797 matcher.children_length.push(body_node.children.len());
5798
5799 dom_string.push_str(&format!(
5800 "{}{},\r\n",
5801 t,
5802 compile_node_to_rust_code_inner(
5803 child_node,
5804 component_map,
5805 1,
5806 extra_blocks,
5807 css_blocks,
5808 css,
5809 matcher,
5810 )?
5811 ));
5812 }
5813 XmlNodeChild::Text(text) => {
5814 let text = text.trim();
5815 if !text.is_empty() {
5816 let escaped = text.replace("\\", "\\\\").replace("\"", "\\\"");
5817 dom_string
5818 .push_str(&format!("{}Dom::create_text(\"{}\".into()),\r\n", t, escaped));
5819 }
5820 }
5821 }
5822 }
5823 dom_string.push_str(&format!("\r\n{}]))", t));
5824 }
5825
5826 let dom_string = dom_string.trim();
5827 Ok(dom_string.to_string())
5828}
5829
5830fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
5831 let mut blocks = Vec::new();
5832
5833 for css_block in css.rules.as_ref() {
5834 if matcher.matches(&css_block.path) {
5835 let mut ending = None;
5836
5837 if let Some(CssPathSelector::PseudoSelector(p)) =
5838 css_block.path.selectors.as_ref().last()
5839 {
5840 ending = Some(p.clone());
5841 }
5842
5843 blocks.push(CssBlock {
5844 ending,
5845 block: css_block.clone(),
5846 });
5847 }
5848 }
5849
5850 blocks
5851}
5852
5853fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
5854 use self::DynamicItem::*;
5855 if input.is_empty() {
5856 String::from("AzString::from_const_str(\"\")")
5857 } else if input.len() == 1 {
5858 match &input[0] {
5860 Var { name, format_spec } => {
5861 let var_name = normalize_casing(name.trim());
5862 if let Some(spec) = format_spec {
5863 format!("format!(\"{{:{}}}\", {}).into()", spec, var_name)
5864 } else {
5865 var_name
5866 }
5867 }
5868 Str(s) => format!("AzString::from_const_str(\"{}\")", s),
5869 }
5870 } else {
5871 let mut formatted_str = String::from("format!(\"");
5873 let mut variables = Vec::new();
5874 for item in input {
5875 match item {
5876 Var { name, format_spec } => {
5877 let variable_name = normalize_casing(name.trim());
5878 if let Some(spec) = format_spec {
5879 formatted_str.push_str(&format!("{{{}:{}}}", variable_name, spec));
5880 } else {
5881 formatted_str.push_str(&format!("{{{}}}", variable_name));
5882 }
5883 variables.push(variable_name.clone());
5884 }
5885 Str(s) => {
5886 let s = s.replace("\"", "\\\"");
5887 formatted_str.push_str(&s);
5888 }
5889 }
5890 }
5891
5892 formatted_str.push('\"');
5893 if !variables.is_empty() {
5894 formatted_str.push_str(", ");
5895 }
5896
5897 formatted_str.push_str(&variables.join(", "));
5898 formatted_str.push_str(").into()");
5899 formatted_str
5900 }
5901}
5902
5903fn format_args_for_rust_code(input: &str) -> String {
5904 let dynamic_str_items = split_dynamic_string(input);
5905 compile_and_format_dynamic_items(&dynamic_str_items)
5906}
5907
5908fn compile_node_to_rust_code_inner<'a>(
5909 node: &XmlNode,
5910 component_map: &'a ComponentMap,
5911 tabs: usize,
5912 extra_blocks: &mut VecContents,
5913 css_blocks: &mut BTreeMap<String, String>,
5914 css: &Css,
5915 mut matcher: CssMatcher,
5916) -> Result<String, CompileError> {
5917 use azul_css::css::CssDeclaration;
5918
5919 let t = String::from(" ").repeat(tabs - 1);
5920 let t2 = String::from(" ").repeat(tabs);
5921
5922 let component_name = normalize_casing(&node.node_type);
5923
5924 let def = component_map.get_unqualified(&component_name);
5926
5927 let node_type_tag = tag_to_node_type_tag(&component_name);
5929 let node_type = CssPathSelector::Type(node_type_tag);
5930
5931 let text_content = node.get_text_content();
5933 let text_content_trimmed = text_content.trim();
5934 let mut dom_string = if let Some(d) = def {
5935 let data_model = xml_attrs_to_data_model(
5936 &d.data_model,
5937 &node.attributes,
5938 if text_content_trimmed.is_empty() { None } else { Some(text_content_trimmed) },
5939 );
5940 match (d.compile_fn)(d, &CompileTarget::Rust, &data_model, tabs) {
5941 ResultStringCompileError::Ok(s) => format!("{}{}", t2, s.as_str()),
5942 ResultStringCompileError::Err(_) => {
5943 let node_type = tag_to_node_type(&component_name);
5945 format!("{}Dom::create_node(NodeType::{:?})", t2, node_type)
5946 }
5947 }
5948 } else {
5949 format!("{}Dom::create_node(NodeType::Div) /* {} */", t2, component_name)
5951 };
5952
5953 matcher.path.push(node_type);
5954 let ids = node
5955 .attributes
5956 .get_key("id")
5957 .map(|s| s.split_whitespace().collect::<Vec<_>>())
5958 .unwrap_or_default();
5959
5960 matcher.path.extend(
5961 ids.into_iter()
5962 .map(|id| CssPathSelector::Id(id.to_string().into())),
5963 );
5964
5965 let classes = node
5966 .attributes
5967 .get_key("class")
5968 .map(|s| s.split_whitespace().collect::<Vec<_>>())
5969 .unwrap_or_default();
5970
5971 matcher.path.extend(
5972 classes
5973 .into_iter()
5974 .map(|class| CssPathSelector::Class(class.to_string().into())),
5975 );
5976
5977 let matcher_hash = matcher.get_hash();
5978 let css_blocks_for_this_node = get_css_blocks(css, &matcher);
5979 if !css_blocks_for_this_node.is_empty() {
5980 use azul_css::props::property::format_static_css_prop;
5981
5982 let css_strings = css_blocks_for_this_node
5983 .iter()
5984 .rev()
5985 .map(|css_block| {
5986 let wrapper = match css_block.ending {
5987 Some(CssPathPseudoSelector::Hover) => "Hover",
5988 Some(CssPathPseudoSelector::Active) => "Active",
5989 Some(CssPathPseudoSelector::Focus) => "Focus",
5990 _ => "Normal",
5991 };
5992
5993 for declaration in css_block.block.declarations.as_ref().iter() {
5994 let prop = match declaration {
5995 CssDeclaration::Static(s) => s,
5996 CssDeclaration::Dynamic(d) => &d.default_value,
5997 };
5998 extra_blocks.insert_from_css_property(prop);
5999 }
6000
6001 let formatted = css_block
6002 .block
6003 .declarations
6004 .as_ref()
6005 .iter()
6006 .rev()
6007 .map(|s| match &s {
6008 CssDeclaration::Static(s) => format!(
6009 "NodeDataInlineCssProperty::{}({})",
6010 wrapper,
6011 format_static_css_prop(s, 1)
6012 ),
6013 CssDeclaration::Dynamic(d) => format!(
6014 "NodeDataInlineCssProperty::{}({})",
6015 wrapper,
6016 format_static_css_prop(&d.default_value, 1)
6017 ),
6018 })
6019 .collect::<Vec<String>>();
6020
6021 format!("// {}\r\n{}", css_block.block.path, formatted.join(",\r\n"))
6022 })
6023 .collect::<Vec<_>>()
6024 .join(",\r\n");
6025
6026 css_blocks.insert(format!("CSS_MATCH_{:09}", matcher_hash), css_strings);
6027 dom_string.push_str(&format!(
6028 "\r\n{}.with_inline_css_props(CSS_MATCH_{:09})",
6029 t2, matcher_hash
6030 ));
6031 }
6032
6033 set_stringified_attributes(
6034 &mut dom_string,
6035 &node.attributes,
6036 &ComponentArgumentVec::new(),
6037 tabs,
6038 );
6039
6040 let mut children_string = node
6041 .children
6042 .as_ref()
6043 .iter()
6044 .enumerate()
6045 .filter_map(|(child_idx, c)| match c {
6046 XmlNodeChild::Element(child_node) => {
6047 let mut matcher = matcher.clone();
6048 matcher.path.push(CssPathSelector::Children);
6049 matcher.indices_in_parent.push(child_idx);
6050 matcher.children_length.push(node.children.len());
6051
6052 Some(compile_node_to_rust_code_inner(
6053 child_node,
6054 component_map,
6055 tabs + 1,
6056 extra_blocks,
6057 css_blocks,
6058 css,
6059 matcher,
6060 ))
6061 }
6062 XmlNodeChild::Text(text) => {
6063 let text = text.trim();
6064 if text.is_empty() {
6065 None
6066 } else {
6067 let t2 = String::from(" ").repeat(tabs);
6068 let escaped = text.replace("\\", "\\\\").replace("\"", "\\\"");
6069 Some(Ok(format!("{}Dom::create_text(\"{}\".into())", t2, escaped)))
6070 }
6071 }
6072 })
6073 .collect::<Result<Vec<_>, _>>()?
6074 .join(&format!(",\r\n"));
6075
6076 if !children_string.is_empty() {
6077 dom_string.push_str(&format!(
6078 "\r\n{}.with_children(DomVec::from_vec(vec![\r\n{}\r\n{}]))",
6079 t2, children_string, t2
6080 ));
6081 }
6082
6083 Ok(dom_string)
6084}
6085
6086#[cfg(test)]
6087mod tests {
6088 use super::*;
6089 use crate::dom::{Dom, NodeType};
6090
6091 #[test]
6092 fn test_inline_span_parsing() {
6093 let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
6097
6098 let expected_dom = Dom::create_p().with_children(
6108 vec![
6109 Dom::create_text("Text before "),
6110 Dom::create_node(NodeType::Span)
6111 .with_children(vec![Dom::create_text("inline text")].into()),
6112 Dom::create_text(" text after."),
6113 ]
6114 .into(),
6115 );
6116
6117 assert_eq!(expected_dom.children.as_ref().len(), 3);
6119
6120 match &expected_dom.children.as_ref()[1].root.node_type {
6122 NodeType::Span => {}
6123 other => panic!("Expected Span, got {:?}", other),
6124 }
6125
6126 assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
6128
6129 println!("Test passed: Inline span parsing structure is correct");
6130 }
6131
6132 #[test]
6133 fn test_xml_node_structure() {
6134 let node = XmlNode {
6138 node_type: "p".into(),
6139 attributes: XmlAttributeMap {
6140 inner: StringPairVec::from_const_slice(&[]),
6141 },
6142 children: vec![
6143 XmlNodeChild::Text("Before ".into()),
6144 XmlNodeChild::Element(XmlNode {
6145 node_type: "span".into(),
6146 children: vec![XmlNodeChild::Text("inline".into())].into(),
6147 ..Default::default()
6148 }),
6149 XmlNodeChild::Text(" after".into()),
6150 ]
6151 .into(),
6152 };
6153
6154 assert_eq!(node.children.as_ref().len(), 3);
6156 assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
6157 assert_eq!(
6158 node.children.as_ref()[1]
6159 .as_element()
6160 .unwrap()
6161 .node_type
6162 .as_str(),
6163 "span"
6164 );
6165 assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
6166
6167 let span = node.children.as_ref()[1].as_element().unwrap();
6169 assert_eq!(span.children.as_ref().len(), 1);
6170 assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
6171
6172 println!("Test passed: XmlNode structure preserves text nodes correctly");
6173 }
6174}