use alloc::{
boxed::Box,
collections::BTreeMap,
string::{String, ToString},
vec::Vec,
};
use core::{fmt, hash::Hash};
use azul_css::{
css::{
Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
NodeTypeTag,
},
format_rust_code::VecContents,
parser2::{CssParseErrorOwned, ErrorLocation},
props::{
basic::{ColorU, StyleFontFamilyVec},
property::CssProperty,
style::{
NormalizedLinearColorStopVec, NormalizedRadialColorStopVec, StyleBackgroundContentVec,
StyleBackgroundPositionVec, StyleBackgroundRepeatVec, StyleBackgroundSizeVec,
StyleTransformVec,
},
},
AzString, OptionString, StringVec, U8Vec,
};
use crate::{
dom::{Dom, NodeType, OptionNodeType},
styled_dom::StyledDom,
window::{AzStringPair, StringPairVec},
};
pub type SyntaxError = String;
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct XmlTagName {
pub inner: AzString,
}
impl From<AzString> for XmlTagName {
fn from(s: AzString) -> Self {
Self { inner: s }
}
}
impl From<String> for XmlTagName {
fn from(s: String) -> Self {
Self { inner: s.into() }
}
}
impl From<&str> for XmlTagName {
fn from(s: &str) -> Self {
Self { inner: s.into() }
}
}
impl core::ops::Deref for XmlTagName {
type Target = AzString;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
pub type XmlTextContent = OptionString;
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct XmlAttributeMap {
pub inner: StringPairVec,
}
impl From<StringPairVec> for XmlAttributeMap {
fn from(v: StringPairVec) -> Self {
Self { inner: v }
}
}
impl core::ops::Deref for XmlAttributeMap {
type Target = StringPairVec;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl core::ops::DerefMut for XmlAttributeMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
type ComponentArgumentName = String;
type ComponentArgumentType = String;
type ComponentArgumentOrder = usize;
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ComponentArgument {
pub name: AzString,
pub arg_type: AzString,
}
impl_vec!(ComponentArgument, ComponentArgumentVec, ComponentArgumentVecDestructor, ComponentArgumentVecDestructorType, ComponentArgumentVecSlice, OptionComponentArgument);
impl_option!(ComponentArgument, OptionComponentArgument, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
impl_vec_debug!(ComponentArgument, ComponentArgumentVec);
impl_vec_partialeq!(ComponentArgument, ComponentArgumentVec);
impl_vec_eq!(ComponentArgument, ComponentArgumentVec);
impl_vec_partialord!(ComponentArgument, ComponentArgumentVec);
impl_vec_ord!(ComponentArgument, ComponentArgumentVec);
impl_vec_hash!(ComponentArgument, ComponentArgumentVec);
impl_vec_clone!(ComponentArgument, ComponentArgumentVec, ComponentArgumentVecDestructor);
impl_vec_mut!(ComponentArgument, ComponentArgumentVec);
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ComponentArguments {
pub args: ComponentArgumentVec,
pub accepts_text: bool,
}
type ComponentName = String;
type CompiledComponent = String;
const DEFAULT_ARGS: [&str; 8] = [
"id",
"class",
"tabindex",
"focusable",
"accepts_text",
"name",
"style",
"args",
];
#[allow(non_camel_case_types)]
pub enum c_void {}
#[repr(C)]
pub enum XmlNodeType {
Root,
Element,
PI,
Comment,
Text,
}
#[repr(C)]
pub struct XmlQualifiedName {
pub local_name: AzString,
pub namespace: OptionString,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum ExternalResourceKind {
Image,
Font,
Stylesheet,
Script,
Icon,
Video,
Audio,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct MimeTypeHint {
pub inner: AzString,
}
impl MimeTypeHint {
pub fn new(s: &str) -> Self {
Self { inner: AzString::from(s) }
}
pub fn from_extension(ext: &str) -> Self {
let mime = match ext.to_lowercase().as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"webp" => "image/webp",
"svg" => "image/svg+xml",
"ico" => "image/x-icon",
"bmp" => "image/bmp",
"avif" => "image/avif",
"ttf" => "font/ttf",
"otf" => "font/otf",
"woff" => "font/woff",
"woff2" => "font/woff2",
"eot" => "application/vnd.ms-fontobject",
"css" => "text/css",
"js" => "application/javascript",
"mjs" => "application/javascript",
"mp4" => "video/mp4",
"webm" => "video/webm",
"ogg" => "video/ogg",
"mp3" => "audio/mpeg",
"wav" => "audio/wav",
"flac" => "audio/flac",
_ => "application/octet-stream",
};
Self { inner: AzString::from(mime) }
}
}
impl_option!(
MimeTypeHint,
OptionMimeTypeHint,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ExternalResource {
pub url: AzString,
pub kind: ExternalResourceKind,
pub mime_type: OptionMimeTypeHint,
pub source_element: AzString,
pub source_attribute: AzString,
}
impl_option!(
ExternalResource,
OptionExternalResource,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec!(ExternalResource, ExternalResourceVec, ExternalResourceVecDestructor, ExternalResourceVecDestructorType, ExternalResourceVecSlice, OptionExternalResource);
impl_vec_mut!(ExternalResource, ExternalResourceVec);
impl_vec_debug!(ExternalResource, ExternalResourceVec);
impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
impl_vec_eq!(ExternalResource, ExternalResourceVec);
impl_vec_partialord!(ExternalResource, ExternalResourceVec);
impl_vec_ord!(ExternalResource, ExternalResourceVec);
impl_vec_hash!(ExternalResource, ExternalResourceVec);
impl_vec_clone!(ExternalResource, ExternalResourceVec, ExternalResourceVecDestructor);
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct Xml {
pub root: XmlNodeChildVec,
}
impl Xml {
pub fn scan_external_resources(&self) -> ExternalResourceVec {
let mut resources = Vec::new();
for child in self.root.as_ref().iter() {
Self::scan_node_child(child, &mut resources);
}
resources.into()
}
fn scan_node_child(child: &XmlNodeChild, resources: &mut Vec<ExternalResource>) {
match child {
XmlNodeChild::Text(text) => {
Self::extract_css_urls(text.as_str(), resources);
}
XmlNodeChild::Element(node) => {
Self::scan_node(node, resources);
}
}
}
fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
let tag_name = node.node_type.inner.as_str().to_lowercase();
let get_attr = |name: &str| -> Option<String> {
node.attributes.inner.as_ref().iter()
.find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
.map(|pair| pair.value.as_str().to_string())
};
match tag_name.as_str() {
"img" => {
if let Some(src) = get_attr("src") {
let mime = Self::guess_mime_from_url(&src, "image");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("img"),
source_attribute: AzString::from("src"),
});
}
if let Some(srcset) = get_attr("srcset") {
for src in Self::parse_srcset(&srcset) {
let mime = Self::guess_mime_from_url(&src, "image");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("img"),
source_attribute: AzString::from("srcset"),
});
}
}
}
"link" => {
if let Some(href) = get_attr("href") {
let rel = get_attr("rel").unwrap_or_default().to_lowercase();
let type_attr = get_attr("type");
let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
let (kind, category) = if rel.contains("stylesheet") {
(ExternalResourceKind::Stylesheet, "stylesheet")
} else if rel.contains("icon") || rel.contains("apple-touch-icon") {
(ExternalResourceKind::Icon, "image")
} else if as_attr == "font" {
(ExternalResourceKind::Font, "font")
} else if as_attr == "script" {
(ExternalResourceKind::Script, "script")
} else if as_attr == "image" {
(ExternalResourceKind::Image, "image")
} else {
(ExternalResourceKind::Unknown, "")
};
let mime = type_attr.map(|t| MimeTypeHint::new(&t))
.or_else(|| Self::guess_mime_from_url(&href, category));
resources.push(ExternalResource {
url: AzString::from(href),
kind,
mime_type: mime.into(),
source_element: AzString::from("link"),
source_attribute: AzString::from("href"),
});
}
}
"script" => {
if let Some(src) = get_attr("src") {
let type_attr = get_attr("type");
let mime = type_attr.map(|t| MimeTypeHint::new(&t))
.or_else(|| Some(MimeTypeHint::new("application/javascript")));
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Script,
mime_type: mime.into(),
source_element: AzString::from("script"),
source_attribute: AzString::from("src"),
});
}
}
"video" => {
if let Some(src) = get_attr("src") {
let mime = Self::guess_mime_from_url(&src, "video");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Video,
mime_type: mime.into(),
source_element: AzString::from("video"),
source_attribute: AzString::from("src"),
});
}
if let Some(poster) = get_attr("poster") {
let mime = Self::guess_mime_from_url(&poster, "image");
resources.push(ExternalResource {
url: AzString::from(poster),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("video"),
source_attribute: AzString::from("poster"),
});
}
}
"audio" => {
if let Some(src) = get_attr("src") {
let mime = Self::guess_mime_from_url(&src, "audio");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Audio,
mime_type: mime.into(),
source_element: AzString::from("audio"),
source_attribute: AzString::from("src"),
});
}
}
"source" => {
if let Some(src) = get_attr("src") {
let type_attr = get_attr("type");
let kind = if type_attr.as_ref().map(|t| t.starts_with("audio")).unwrap_or(false) {
ExternalResourceKind::Audio
} else {
ExternalResourceKind::Video
};
let mime = type_attr.map(|t| MimeTypeHint::new(&t))
.or_else(|| Self::guess_mime_from_url(&src, if kind == ExternalResourceKind::Audio { "audio" } else { "video" }));
resources.push(ExternalResource {
url: AzString::from(src),
kind,
mime_type: mime.into(),
source_element: AzString::from("source"),
source_attribute: AzString::from("src"),
});
}
if let Some(srcset) = get_attr("srcset") {
for src in Self::parse_srcset(&srcset) {
let mime = Self::guess_mime_from_url(&src, "image");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("source"),
source_attribute: AzString::from("srcset"),
});
}
}
}
"a" => {
if let Some(href) = get_attr("href") {
if Self::looks_like_resource(&href) {
let mime = Self::guess_mime_from_url(&href, "");
resources.push(ExternalResource {
url: AzString::from(href),
kind: ExternalResourceKind::Unknown,
mime_type: mime.into(),
source_element: AzString::from("a"),
source_attribute: AzString::from("href"),
});
}
}
}
"virtualized-view" | "embed" | "object" => {
let src_attr = if tag_name == "object" { "data" } else { "src" };
if let Some(src) = get_attr(src_attr) {
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Unknown,
mime_type: OptionMimeTypeHint::None,
source_element: AzString::from(tag_name.clone()),
source_attribute: AzString::from(src_attr),
});
}
}
"style" => {
for child in node.children.as_ref().iter() {
if let XmlNodeChild::Text(text) = child {
Self::extract_css_urls(text.as_str(), resources);
}
}
}
_ => {}
}
if let Some(style) = get_attr("style") {
Self::extract_css_urls(&style, resources);
}
if let Some(bg) = get_attr("background") {
let mime = Self::guess_mime_from_url(&bg, "image");
resources.push(ExternalResource {
url: AzString::from(bg),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from(tag_name),
source_attribute: AzString::from("background"),
});
}
for child in node.children.as_ref().iter() {
Self::scan_node_child(child, resources);
}
}
fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
let mut remaining = css;
while let Some(pos) = remaining.find("url(") {
let after_url = &remaining[pos + 4..];
if let Some(url) = Self::extract_url_value(after_url) {
let mime = Self::guess_mime_from_url(&url, "");
let kind = Self::guess_kind_from_url(&url);
resources.push(ExternalResource {
url: AzString::from(url),
kind,
mime_type: mime.into(),
source_element: AzString::from("style"),
source_attribute: AzString::from("url()"),
});
}
remaining = after_url;
}
remaining = css;
while let Some(pos) = remaining.to_lowercase().find("@import") {
let after_import = &remaining[pos + 7..];
let trimmed = after_import.trim_start();
if trimmed.starts_with("url(") {
if let Some(url) = Self::extract_url_value(&trimmed[4..]) {
resources.push(ExternalResource {
url: AzString::from(url),
kind: ExternalResourceKind::Stylesheet,
mime_type: Some(MimeTypeHint::new("text/css")).into(),
source_element: AzString::from("style"),
source_attribute: AzString::from("@import"),
});
}
} else if let Some(url) = Self::extract_quoted_string(trimmed) {
resources.push(ExternalResource {
url: AzString::from(url),
kind: ExternalResourceKind::Stylesheet,
mime_type: Some(MimeTypeHint::new("text/css")).into(),
source_element: AzString::from("style"),
source_attribute: AzString::from("@import"),
});
}
remaining = after_import;
}
}
fn extract_url_value(s: &str) -> Option<String> {
let trimmed = s.trim_start();
if trimmed.starts_with('"') {
Self::extract_quoted_string(trimmed)
} else if trimmed.starts_with('\'') {
let end = trimmed[1..].find('\'')?;
Some(trimmed[1..1+end].to_string())
} else {
let end = trimmed.find(')')?;
Some(trimmed[..end].trim().to_string())
}
}
fn extract_quoted_string(s: &str) -> Option<String> {
if s.starts_with('"') {
let end = s[1..].find('"')?;
Some(s[1..1+end].to_string())
} else if s.starts_with('\'') {
let end = s[1..].find('\'')?;
Some(s[1..1+end].to_string())
} else {
None
}
}
fn parse_srcset(srcset: &str) -> Vec<String> {
srcset.split(',')
.filter_map(|entry| {
let trimmed = entry.trim();
trimmed.split_whitespace().next().map(|s| s.to_string())
})
.filter(|url| !url.is_empty())
.collect()
}
fn looks_like_resource(url: &str) -> bool {
let lower = url.to_lowercase();
let resource_exts = [
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp",
".ttf", ".otf", ".woff", ".woff2", ".eot",
".css", ".js",
".mp4", ".webm", ".ogg", ".mp3", ".wav",
".pdf", ".zip", ".tar", ".gz",
];
resource_exts.iter().any(|ext| lower.ends_with(ext))
}
fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
let lower = url.to_lowercase();
let path = lower.split('?').next().unwrap_or(&lower);
if path.ends_with(".png") || path.ends_with(".jpg") || path.ends_with(".jpeg")
|| path.ends_with(".gif") || path.ends_with(".webp") || path.ends_with(".svg")
|| path.ends_with(".bmp") || path.ends_with(".avif") {
ExternalResourceKind::Image
} else if path.ends_with(".ttf") || path.ends_with(".otf") || path.ends_with(".woff")
|| path.ends_with(".woff2") || path.ends_with(".eot") {
ExternalResourceKind::Font
} else if path.ends_with(".css") {
ExternalResourceKind::Stylesheet
} else if path.ends_with(".js") || path.ends_with(".mjs") {
ExternalResourceKind::Script
} else if path.ends_with(".mp4") || path.ends_with(".webm") || path.ends_with(".ogg") {
ExternalResourceKind::Video
} else if path.ends_with(".mp3") || path.ends_with(".wav") || path.ends_with(".flac") {
ExternalResourceKind::Audio
} else if path.ends_with(".ico") {
ExternalResourceKind::Icon
} else {
ExternalResourceKind::Unknown
}
}
fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
let lower = url.to_lowercase();
let ext = lower.rsplit('.').next()?;
let ext = ext.split('?').next()?;
let valid_exts = [
"png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif",
"ttf", "otf", "woff", "woff2", "eot",
"css", "js", "mjs",
"mp4", "webm", "ogg", "mp3", "wav", "flac",
];
if valid_exts.contains(&ext) {
Some(MimeTypeHint::from_extension(ext))
} else if !category.is_empty() {
match category {
"image" => Some(MimeTypeHint::new("image/*")),
"font" => Some(MimeTypeHint::new("font/*")),
"stylesheet" => Some(MimeTypeHint::new("text/css")),
"script" => Some(MimeTypeHint::new("application/javascript")),
"video" => Some(MimeTypeHint::new("video/*")),
"audio" => Some(MimeTypeHint::new("audio/*")),
_ => None,
}
} else {
None
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct NonXmlCharError {
pub ch: u32,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidCharError {
pub expected: u8,
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidCharMultipleError {
pub expected: u8,
pub got: U8Vec,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidQuoteError {
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidSpaceError {
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidStringError {
pub got: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum XmlStreamError {
UnexpectedEndOfStream,
InvalidName,
NonXmlChar(NonXmlCharError),
InvalidChar(InvalidCharError),
InvalidCharMultiple(InvalidCharMultipleError),
InvalidQuote(InvalidQuoteError),
InvalidSpace(InvalidSpaceError),
InvalidString(InvalidStringError),
InvalidReference,
InvalidExternalID,
InvalidCommentData,
InvalidCommentEnd,
InvalidCharacterData,
}
impl fmt::Display for XmlStreamError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::XmlStreamError::*;
match self {
UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
InvalidName => write!(f, "Invalid name"),
NonXmlChar(nx) => write!(
f,
"Non-XML character: {:?} at {}",
core::char::from_u32(nx.ch),
nx.pos
),
InvalidChar(ic) => write!(
f,
"Invalid character: expected: {}, got: {} at {}",
ic.expected as char, ic.got as char, ic.pos
),
InvalidCharMultiple(imc) => write!(
f,
"Multiple invalid characters: expected: {}, got: {:?} at {}",
imc.expected,
imc.got.as_ref(),
imc.pos
),
InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
InvalidString(ise) => write!(
f,
"Invalid string: got \"{}\" at {}",
ise.got.as_str(),
ise.pos
),
InvalidReference => write!(f, "Invalid reference"),
InvalidExternalID => write!(f, "Invalid external ID"),
InvalidCommentData => write!(f, "Invalid comment data"),
InvalidCommentEnd => write!(f, "Invalid comment end"),
InvalidCharacterData => write!(f, "Invalid character data"),
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Ord, Hash, Eq)]
#[repr(C)]
pub struct XmlTextPos {
pub row: u32,
pub col: u32,
}
impl fmt::Display for XmlTextPos {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "line {}:{}", self.row, self.col)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct XmlTextError {
pub stream_error: XmlStreamError,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum XmlParseError {
InvalidDeclaration(XmlTextError),
InvalidComment(XmlTextError),
InvalidPI(XmlTextError),
InvalidDoctype(XmlTextError),
InvalidEntity(XmlTextError),
InvalidElement(XmlTextError),
InvalidAttribute(XmlTextError),
InvalidCdata(XmlTextError),
InvalidCharData(XmlTextError),
UnknownToken(XmlTextPos),
}
impl fmt::Display for XmlParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::XmlParseError::*;
match self {
InvalidDeclaration(e) => {
write!(f, "Invalid declaration: {} at {}", e.stream_error, e.pos)
}
InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
InvalidPI(e) => write!(
f,
"Invalid processing instruction: {} at {}",
e.stream_error, e.pos
),
InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
UnknownToken(e) => write!(f, "Unknown token at {}", e),
}
}
}
impl_result!(
Xml,
XmlError,
ResultXmlXmlError,
copy = false,
[Debug, PartialEq, PartialOrd, Clone]
);
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedNamespaceError {
pub ns: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownNamespaceError {
pub ns: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnexpectedCloseTagError {
pub expected: AzString,
pub actual: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownEntityReferenceError {
pub entity: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedAttributeError {
pub attribute: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct MalformedHierarchyError {
pub expected: AzString,
pub got: AzString,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum XmlError {
NoParserAvailable,
InvalidXmlPrefixUri(XmlTextPos),
UnexpectedXmlUri(XmlTextPos),
UnexpectedXmlnsUri(XmlTextPos),
InvalidElementNamePrefix(XmlTextPos),
DuplicatedNamespace(DuplicatedNamespaceError),
UnknownNamespace(UnknownNamespaceError),
UnexpectedCloseTag(UnexpectedCloseTagError),
UnexpectedEntityCloseTag(XmlTextPos),
UnknownEntityReference(UnknownEntityReferenceError),
MalformedEntityReference(XmlTextPos),
EntityReferenceLoop(XmlTextPos),
InvalidAttributeValue(XmlTextPos),
DuplicatedAttribute(DuplicatedAttributeError),
NoRootNode,
SizeLimit,
DtdDetected,
MalformedHierarchy(MalformedHierarchyError),
ParserError(XmlParseError),
UnclosedRootNode,
UnexpectedDeclaration(XmlTextPos),
NodesLimitReached,
AttributesLimitReached,
NamespacesLimitReached,
InvalidName(XmlTextPos),
NonXmlChar(XmlTextPos),
InvalidChar(XmlTextPos),
InvalidChar2(XmlTextPos),
InvalidString(XmlTextPos),
InvalidExternalID(XmlTextPos),
InvalidComment(XmlTextPos),
InvalidCharacterData(XmlTextPos),
UnknownToken(XmlTextPos),
UnexpectedEndOfStream,
}
impl fmt::Display for XmlError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::XmlError::*;
match self {
NoParserAvailable => write!(
f,
"Library was compiled without XML parser (XML parser not available)"
),
InvalidXmlPrefixUri(pos) => {
write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
}
UnexpectedXmlUri(pos) => {
write!(f, "Unexpected XML URI at line {}:{}", pos.row, pos.col)
}
UnexpectedXmlnsUri(pos) => write!(
f,
"Unexpected XML namespace URI at line {}:{}",
pos.row, pos.col
),
InvalidElementNamePrefix(pos) => write!(
f,
"Invalid element name prefix at line {}:{}",
pos.row, pos.col
),
DuplicatedNamespace(ns) => write!(
f,
"Duplicated namespace: \"{}\" at {}",
ns.ns.as_str(),
ns.pos
),
UnknownNamespace(uns) => write!(
f,
"Unknown namespace: \"{}\" at {}",
uns.ns.as_str(),
uns.pos
),
UnexpectedCloseTag(ct) => write!(
f,
"Unexpected close tag: expected \"{}\", got \"{}\" at {}",
ct.expected.as_str(),
ct.actual.as_str(),
ct.pos
),
UnexpectedEntityCloseTag(pos) => write!(
f,
"Unexpected entity close tag at line {}:{}",
pos.row, pos.col
),
UnknownEntityReference(uer) => write!(
f,
"Unexpected entity reference: \"{}\" at {}",
uer.entity, uer.pos
),
MalformedEntityReference(pos) => write!(
f,
"Malformed entity reference at line {}:{}",
pos.row, pos.col
),
EntityReferenceLoop(pos) => write!(
f,
"Entity reference loop (recursive entity reference) at line {}:{}",
pos.row, pos.col
),
InvalidAttributeValue(pos) => {
write!(f, "Invalid attribute value at line {}:{}", pos.row, pos.col)
}
DuplicatedAttribute(ae) => write!(
f,
"Duplicated attribute \"{}\" at line {}:{}",
ae.attribute.as_str(),
ae.pos.row,
ae.pos.col
),
NoRootNode => write!(f, "No root node found"),
SizeLimit => write!(f, "XML file too large (size limit reached)"),
DtdDetected => write!(f, "Document type descriptor detected"),
MalformedHierarchy(e) => write!(
f,
"Malformed hierarchy: expected <{}/> closing tag, got <{}/>",
e.expected.as_str(),
e.got.as_str()
),
ParserError(p) => write!(f, "{}", p),
UnclosedRootNode => write!(f, "unclosed root node"),
UnexpectedDeclaration(tp) => write!(f, "unexpected declaration at {tp}"),
NodesLimitReached => write!(f, "nodes limit reached"),
AttributesLimitReached => write!(f, "attributes limit reached"),
NamespacesLimitReached => write!(f, "namespaces limit reached"),
InvalidName(tp) => write!(f, "invalid name at {tp}"),
NonXmlChar(tp) => write!(f, "non xml char at {tp}"),
InvalidChar(tp) => write!(f, "invalid char at {tp}"),
InvalidChar2(tp) => write!(f, "invalid char2 at {tp}"),
InvalidString(tp) => write!(f, "invalid string at {tp}"),
InvalidExternalID(tp) => write!(f, "invalid externalid at {tp}"),
InvalidComment(tp) => write!(f, "invalid comment at {tp}"),
InvalidCharacterData(tp) => write!(f, "invalid character data at {tp}"),
UnknownToken(tp) => write!(f, "unknown token at {tp}"),
UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ComponentId {
pub collection: AzString,
pub name: AzString,
}
impl ComponentId {
pub fn builtin(name: &str) -> Self {
Self {
collection: AzString::from_const_str("builtin"),
name: AzString::from(name),
}
}
pub fn new(collection: &str, name: &str) -> Self {
Self {
collection: AzString::from(collection),
name: AzString::from(name),
}
}
pub fn qualified_name(&self) -> String {
format!("{}:{}", self.collection.as_str(), self.name.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ComponentCallbackArg {
pub name: AzString,
pub arg_type: ComponentFieldType,
}
impl_vec!(ComponentCallbackArg, ComponentCallbackArgVec, ComponentCallbackArgVecDestructor, ComponentCallbackArgVecDestructorType, ComponentCallbackArgVecSlice, OptionComponentCallbackArg);
impl_option!(ComponentCallbackArg, OptionComponentCallbackArg, copy = false, [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
impl_vec_debug!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_partialeq!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_eq!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_partialord!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_ord!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_hash!(ComponentCallbackArg, ComponentCallbackArgVec);
impl_vec_clone!(ComponentCallbackArg, ComponentCallbackArgVec, ComponentCallbackArgVecDestructor);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ComponentCallbackSignature {
pub return_type: AzString,
pub args: ComponentCallbackArgVec,
}
#[repr(C)]
pub struct ComponentFieldTypeBox {
pub ptr: *mut ComponentFieldType,
}
impl ComponentFieldTypeBox {
pub fn new(t: ComponentFieldType) -> Self {
Self { ptr: Box::into_raw(Box::new(t)) }
}
pub fn as_ref(&self) -> &ComponentFieldType {
unsafe { &*self.ptr }
}
}
impl Clone for ComponentFieldTypeBox {
fn clone(&self) -> Self {
Self::new(unsafe { (*self.ptr).clone() })
}
}
impl Drop for ComponentFieldTypeBox {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { let _ = Box::from_raw(self.ptr); }
}
}
}
impl fmt::Debug for ComponentFieldTypeBox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.ptr.is_null() {
write!(f, "ComponentFieldTypeBox(null)")
} else {
write!(f, "ComponentFieldTypeBox({:?})", unsafe { &*self.ptr })
}
}
}
impl PartialEq for ComponentFieldTypeBox {
fn eq(&self, other: &Self) -> bool {
if self.ptr.is_null() && other.ptr.is_null() { return true; }
if self.ptr.is_null() || other.ptr.is_null() { return false; }
unsafe { *self.ptr == *other.ptr }
}
}
impl Eq for ComponentFieldTypeBox {}
impl PartialOrd for ComponentFieldTypeBox {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ComponentFieldTypeBox {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
match (self.ptr.is_null(), other.ptr.is_null()) {
(true, true) => core::cmp::Ordering::Equal,
(true, false) => core::cmp::Ordering::Less,
(false, true) => core::cmp::Ordering::Greater,
(false, false) => unsafe { (*self.ptr).cmp(&*other.ptr) },
}
}
}
impl core::hash::Hash for ComponentFieldTypeBox {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
if !self.ptr.is_null() {
unsafe { (*self.ptr).hash(state); }
}
}
}
#[repr(C)]
pub struct ComponentFieldValueBox {
pub ptr: *mut ComponentFieldValue,
}
impl ComponentFieldValueBox {
pub fn new(v: ComponentFieldValue) -> Self {
Self { ptr: Box::into_raw(Box::new(v)) }
}
pub fn as_ref(&self) -> &ComponentFieldValue {
unsafe { &*self.ptr }
}
}
impl Clone for ComponentFieldValueBox {
fn clone(&self) -> Self {
Self::new(unsafe { (*self.ptr).clone() })
}
}
impl Drop for ComponentFieldValueBox {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { let _ = Box::from_raw(self.ptr); }
}
}
}
impl fmt::Debug for ComponentFieldValueBox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.ptr.is_null() {
write!(f, "ComponentFieldValueBox(null)")
} else {
write!(f, "ComponentFieldValueBox({:?})", unsafe { &*self.ptr })
}
}
}
impl PartialEq for ComponentFieldValueBox {
fn eq(&self, other: &Self) -> bool {
if self.ptr.is_null() && other.ptr.is_null() { return true; }
if self.ptr.is_null() || other.ptr.is_null() { return false; }
unsafe { *self.ptr == *other.ptr }
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum ComponentFieldType {
String,
Bool,
I32,
I64,
U32,
U64,
Usize,
F32,
F64,
ColorU,
CssProperty,
ImageRef,
FontRef,
StyledDom,
Callback(ComponentCallbackSignature),
RefAny(AzString),
OptionType(ComponentFieldTypeBox),
VecType(ComponentFieldTypeBox),
StructRef(AzString),
EnumRef(AzString),
}
impl ComponentFieldType {
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
match s {
"String" | "string" => return Some(ComponentFieldType::String),
"Bool" | "bool" => return Some(ComponentFieldType::Bool),
"I32" | "i32" => return Some(ComponentFieldType::I32),
"I64" | "i64" => return Some(ComponentFieldType::I64),
"U32" | "u32" => return Some(ComponentFieldType::U32),
"U64" | "u64" => return Some(ComponentFieldType::U64),
"Usize" | "usize" => return Some(ComponentFieldType::Usize),
"F32" | "f32" => return Some(ComponentFieldType::F32),
"F64" | "f64" => return Some(ComponentFieldType::F64),
"ColorU" => return Some(ComponentFieldType::ColorU),
"CssProperty" => return Some(ComponentFieldType::CssProperty),
"ImageRef" => return Some(ComponentFieldType::ImageRef),
"FontRef" => return Some(ComponentFieldType::FontRef),
"StyledDom" => return Some(ComponentFieldType::StyledDom),
"RefAny" => return Some(ComponentFieldType::RefAny(AzString::from(""))),
_ => {}
}
if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
let inner_type = ComponentFieldType::parse(inner)?;
return Some(ComponentFieldType::OptionType(ComponentFieldTypeBox::new(inner_type)));
}
if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
let inner_type = ComponentFieldType::parse(inner)?;
return Some(ComponentFieldType::VecType(ComponentFieldTypeBox::new(inner_type)));
}
if let Some(sig) = s.strip_prefix("Callback(").and_then(|r| r.strip_suffix(')')) {
return Some(ComponentFieldType::Callback(ComponentCallbackSignature {
return_type: AzString::from(sig),
args: Vec::new().into(),
}));
}
if let Some(hint) = s.strip_prefix("RefAny(").and_then(|r| r.strip_suffix(')')) {
return Some(ComponentFieldType::RefAny(AzString::from(hint)));
}
if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
return Some(ComponentFieldType::EnumRef(AzString::from(name)));
}
if let Some(name) = s.strip_prefix("StructRef(").and_then(|r| r.strip_suffix(')')) {
return Some(ComponentFieldType::StructRef(AzString::from(name)));
}
if s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
return Some(ComponentFieldType::StructRef(AzString::from(s)));
}
None
}
pub fn format(&self) -> String {
match self {
ComponentFieldType::String => "String".to_string(),
ComponentFieldType::Bool => "Bool".to_string(),
ComponentFieldType::I32 => "I32".to_string(),
ComponentFieldType::I64 => "I64".to_string(),
ComponentFieldType::U32 => "U32".to_string(),
ComponentFieldType::U64 => "U64".to_string(),
ComponentFieldType::Usize => "Usize".to_string(),
ComponentFieldType::F32 => "F32".to_string(),
ComponentFieldType::F64 => "F64".to_string(),
ComponentFieldType::ColorU => "ColorU".to_string(),
ComponentFieldType::CssProperty => "CssProperty".to_string(),
ComponentFieldType::ImageRef => "ImageRef".to_string(),
ComponentFieldType::FontRef => "FontRef".to_string(),
ComponentFieldType::StyledDom => "StyledDom".to_string(),
ComponentFieldType::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
ComponentFieldType::RefAny(hint) => {
if hint.as_str().is_empty() {
"RefAny".to_string()
} else {
format!("RefAny({})", hint.as_str())
}
}
ComponentFieldType::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
ComponentFieldType::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
ComponentFieldType::StructRef(name) => name.as_str().to_string(),
ComponentFieldType::EnumRef(name) => name.as_str().to_string(),
}
}
}
impl core::fmt::Display for ComponentFieldType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.format())
}
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentEnumVariant {
pub name: AzString,
pub description: AzString,
pub fields: ComponentDataFieldVec,
}
impl_vec!(ComponentEnumVariant, ComponentEnumVariantVec, ComponentEnumVariantVecDestructor, ComponentEnumVariantVecDestructorType, ComponentEnumVariantVecSlice, OptionComponentEnumVariant);
impl_option!(ComponentEnumVariant, OptionComponentEnumVariant, copy = false, [Debug, Clone, PartialEq]);
impl_vec_debug!(ComponentEnumVariant, ComponentEnumVariantVec);
impl_vec_partialeq!(ComponentEnumVariant, ComponentEnumVariantVec);
impl_vec_clone!(ComponentEnumVariant, ComponentEnumVariantVec, ComponentEnumVariantVecDestructor);
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentEnumModel {
pub name: AzString,
pub description: AzString,
pub variants: ComponentEnumVariantVec,
}
impl_vec!(ComponentEnumModel, ComponentEnumModelVec, ComponentEnumModelVecDestructor, ComponentEnumModelVecDestructorType, ComponentEnumModelVecSlice, OptionComponentEnumModel);
impl_option!(ComponentEnumModel, OptionComponentEnumModel, copy = false, [Debug, Clone, PartialEq]);
impl_vec_debug!(ComponentEnumModel, ComponentEnumModelVec);
impl_vec_partialeq!(ComponentEnumModel, ComponentEnumModelVec);
impl_vec_clone!(ComponentEnumModel, ComponentEnumModelVec, ComponentEnumModelVecDestructor);
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum ComponentDefaultValue {
None,
String(AzString),
Bool(bool),
I32(i32),
I64(i64),
U32(u32),
U64(u64),
Usize(usize),
F32(f32),
F64(f64),
ColorU(ColorU),
ComponentInstance(ComponentInstanceDefault),
CallbackFnPointer(AzString),
Json(AzString),
}
impl_option!(ComponentDefaultValue, OptionComponentDefaultValue, copy = false, [Debug, Clone, PartialEq]);
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentInstanceDefault {
pub library: AzString,
pub component: AzString,
pub field_overrides: ComponentFieldOverrideVec,
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentFieldOverride {
pub field_name: AzString,
pub source: ComponentFieldValueSource,
}
impl_vec!(ComponentFieldOverride, ComponentFieldOverrideVec, ComponentFieldOverrideVecDestructor, ComponentFieldOverrideVecDestructorType, ComponentFieldOverrideVecSlice, OptionComponentFieldOverride);
impl_option!(ComponentFieldOverride, OptionComponentFieldOverride, copy = false, [Debug, Clone, PartialEq]);
impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
impl_vec_clone!(ComponentFieldOverride, ComponentFieldOverrideVec, ComponentFieldOverrideVecDestructor);
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum ComponentFieldValueSource {
Default,
Literal(AzString),
Binding(AzString),
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum ComponentFieldValue {
String(AzString),
Bool(bool),
I32(i32),
I64(i64),
U32(u32),
U64(u64),
Usize(usize),
F32(f32),
F64(f64),
ColorU(ColorU),
None,
Some(ComponentFieldValueBox),
Vec(ComponentFieldValueVec),
StyledDom(StyledDom),
Struct(ComponentFieldNamedValueVec),
Enum { variant: AzString, fields: ComponentFieldNamedValueVec },
Callback(AzString),
RefAny(crate::refany::RefAny),
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentFieldNamedValue {
pub name: AzString,
pub value: ComponentFieldValue,
}
impl_vec!(ComponentFieldNamedValue, ComponentFieldNamedValueVec, ComponentFieldNamedValueVecDestructor, ComponentFieldNamedValueVecDestructorType, ComponentFieldNamedValueVecSlice, OptionComponentFieldNamedValue);
impl_option!(ComponentFieldNamedValue, OptionComponentFieldNamedValue, copy = false, [Debug, Clone, PartialEq]);
impl_vec_debug!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
impl_vec_partialeq!(ComponentFieldNamedValue, ComponentFieldNamedValueVec);
impl_vec_clone!(ComponentFieldNamedValue, ComponentFieldNamedValueVec, ComponentFieldNamedValueVecDestructor);
impl ComponentFieldNamedValueVec {
pub fn get_field(&self, name: &str) -> Option<&ComponentFieldValue> {
self.as_ref().iter().find_map(|v| {
if v.name.as_str() == name { Some(&v.value) } else { None }
})
}
pub fn get_string(&self, name: &str) -> Option<&AzString> {
match self.get_field(name) {
Some(ComponentFieldValue::String(s)) => Some(s),
_ => None,
}
}
}
impl_vec!(ComponentFieldValue, ComponentFieldValueVec, ComponentFieldValueVecDestructor, ComponentFieldValueVecDestructorType, ComponentFieldValueVecSlice, OptionComponentFieldValue);
impl_option!(ComponentFieldValue, OptionComponentFieldValue, copy = false, [Debug, Clone, PartialEq]);
impl_vec_debug!(ComponentFieldValue, ComponentFieldValueVec);
impl_vec_partialeq!(ComponentFieldValue, ComponentFieldValueVec);
impl_vec_clone!(ComponentFieldValue, ComponentFieldValueVec, ComponentFieldValueVecDestructor);
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct ComponentDataField {
pub name: AzString,
pub field_type: ComponentFieldType,
pub default_value: OptionComponentDefaultValue,
pub required: bool,
pub description: AzString,
}
impl_vec!(ComponentDataField, ComponentDataFieldVec, ComponentDataFieldVecDestructor, ComponentDataFieldVecDestructorType, ComponentDataFieldVecSlice, OptionComponentDataField);
impl_option!(ComponentDataField, OptionComponentDataField, copy = false, [Debug, Clone, PartialEq]);
impl_vec_debug!(ComponentDataField, ComponentDataFieldVec);
impl_vec_partialeq!(ComponentDataField, ComponentDataFieldVec);
impl_vec_clone!(ComponentDataField, ComponentDataFieldVec, ComponentDataFieldVecDestructor);
#[derive(Debug, Clone)]
#[repr(C)]
pub struct ComponentDataModel {
pub name: AzString,
pub description: AzString,
pub fields: ComponentDataFieldVec,
}
impl ComponentDataModel {
pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
self.fields.as_ref().iter().find(|f| f.name.as_str() == name)
}
pub fn get_default_string(&self, name: &str) -> Option<&AzString> {
self.get_field(name).and_then(|f| {
match &f.default_value {
OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s),
_ => None,
}
})
}
pub fn with_default(mut self, name: &str, value: ComponentDefaultValue) -> Self {
let mut fields_vec = core::mem::replace(
&mut self.fields,
ComponentDataFieldVec::from_const_slice(&[]),
).into_library_owned_vec();
for f in fields_vec.iter_mut() {
if f.name.as_str() == name {
f.default_value = OptionComponentDefaultValue::Some(value);
break;
}
}
self.fields = ComponentDataFieldVec::from_vec(fields_vec);
self
}
}
impl_vec!(ComponentDataModel, ComponentDataModelVec, ComponentDataModelVecDestructor, ComponentDataModelVecDestructorType, ComponentDataModelVecSlice, OptionComponentDataModel);
impl_option!(ComponentDataModel, OptionComponentDataModel, copy = false, [Debug, Clone]);
impl_vec_debug!(ComponentDataModel, ComponentDataModelVec);
impl_vec_clone!(ComponentDataModel, ComponentDataModelVec, ComponentDataModelVecDestructor);
impl_vec_mut!(ComponentDataModel, ComponentDataModelVec);
#[cfg(feature = "serde-json")]
mod serde_impl {
use super::*;
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use serde::ser::SerializeStruct;
fn ser_azstring<S: Serializer>(s: &AzString, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(s.as_str())
}
fn de_azstring<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AzString, D::Error> {
let s = alloc::string::String::deserialize(deserializer)?;
Ok(AzString::from(s.as_str()))
}
impl Serialize for ComponentFieldType {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&field_type_to_string(self))
}
}
impl<'de> Deserialize<'de> for ComponentFieldType {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = alloc::string::String::deserialize(deserializer)?;
Ok(string_to_field_type(&s))
}
}
fn field_type_to_string(ft: &ComponentFieldType) -> alloc::string::String {
match ft {
ComponentFieldType::String => "String".into(),
ComponentFieldType::Bool => "bool".into(),
ComponentFieldType::I32 => "i32".into(),
ComponentFieldType::I64 => "i64".into(),
ComponentFieldType::U32 => "u32".into(),
ComponentFieldType::U64 => "u64".into(),
ComponentFieldType::Usize => "usize".into(),
ComponentFieldType::F32 => "f32".into(),
ComponentFieldType::F64 => "f64".into(),
ComponentFieldType::ColorU => "ColorU".into(),
ComponentFieldType::CssProperty => "CssProperty".into(),
ComponentFieldType::ImageRef => "ImageRef".into(),
ComponentFieldType::FontRef => "FontRef".into(),
ComponentFieldType::StyledDom => "Dom".into(),
ComponentFieldType::Callback(sig) => alloc::format!(
"Callback({})",
sig.return_type.as_str()
),
ComponentFieldType::RefAny(hint) => alloc::format!("RefAny({})", hint.as_str()),
ComponentFieldType::OptionType(inner) => alloc::format!(
"Option<{}>",
field_type_to_string(inner.as_ref())
),
ComponentFieldType::VecType(inner) => alloc::format!(
"Vec<{}>",
field_type_to_string(inner.as_ref())
),
ComponentFieldType::StructRef(name) => alloc::format!("struct:{}", name.as_str()),
ComponentFieldType::EnumRef(name) => alloc::format!("enum:{}", name.as_str()),
}
}
fn string_to_field_type(s: &str) -> ComponentFieldType {
match s {
"String" | "string" => ComponentFieldType::String,
"bool" | "Bool" => ComponentFieldType::Bool,
"i32" | "I32" => ComponentFieldType::I32,
"i64" | "I64" => ComponentFieldType::I64,
"u32" | "U32" => ComponentFieldType::U32,
"u64" | "U64" => ComponentFieldType::U64,
"usize" | "Usize" => ComponentFieldType::Usize,
"f32" | "F32" => ComponentFieldType::F32,
"f64" | "F64" => ComponentFieldType::F64,
"ColorU" | "Color" | "color" => ComponentFieldType::ColorU,
"CssProperty" => ComponentFieldType::CssProperty,
"ImageRef" | "Image" => ComponentFieldType::ImageRef,
"FontRef" | "Font" => ComponentFieldType::FontRef,
"Dom" | "StyledDom" | "Children" => ComponentFieldType::StyledDom,
other => {
if let Some(inner) = other.strip_prefix("Option<").and_then(|s| s.strip_suffix('>')) {
ComponentFieldType::OptionType(ComponentFieldTypeBox::new(string_to_field_type(inner)))
} else if let Some(inner) = other.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
ComponentFieldType::VecType(ComponentFieldTypeBox::new(string_to_field_type(inner)))
} else if let Some(name) = other.strip_prefix("struct:") {
ComponentFieldType::StructRef(AzString::from(name))
} else if let Some(name) = other.strip_prefix("enum:") {
ComponentFieldType::EnumRef(AzString::from(name))
} else if other.starts_with("Callback") {
let ret = other.strip_prefix("Callback(")
.and_then(|s| s.strip_suffix(')'))
.unwrap_or("()");
ComponentFieldType::Callback(ComponentCallbackSignature {
return_type: AzString::from(ret),
args: ComponentCallbackArgVec::from_const_slice(&[]),
})
} else if other.starts_with("RefAny") {
let hint = other.strip_prefix("RefAny(")
.and_then(|s| s.strip_suffix(')'))
.unwrap_or("");
ComponentFieldType::RefAny(AzString::from(hint))
} else {
ComponentFieldType::String }
}
}
}
impl Serialize for ComponentDefaultValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
ComponentDefaultValue::None => serializer.serialize_none(),
ComponentDefaultValue::String(s) => serializer.serialize_str(s.as_str()),
ComponentDefaultValue::Bool(b) => serializer.serialize_bool(*b),
ComponentDefaultValue::I32(v) => serializer.serialize_i32(*v),
ComponentDefaultValue::I64(v) => serializer.serialize_i64(*v),
ComponentDefaultValue::U32(v) => serializer.serialize_u32(*v),
ComponentDefaultValue::U64(v) => serializer.serialize_u64(*v),
ComponentDefaultValue::Usize(v) => serializer.serialize_u64(*v as u64),
ComponentDefaultValue::F32(v) => serializer.serialize_f32(*v),
ComponentDefaultValue::F64(v) => serializer.serialize_f64(*v),
ComponentDefaultValue::ColorU(c) => {
serializer.serialize_str(&alloc::format!("#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a))
}
ComponentDefaultValue::ComponentInstance(ci) => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("library", ci.library.as_str())?;
map.serialize_entry("component", ci.component.as_str())?;
map.end()
}
ComponentDefaultValue::CallbackFnPointer(name) => {
serializer.serialize_str(name.as_str())
}
ComponentDefaultValue::Json(json_str) => {
match serde_json::from_str::<serde_json::Value>(json_str.as_str()) {
Ok(v) => v.serialize(serializer),
Err(_) => serializer.serialize_str(json_str.as_str()),
}
}
}
}
}
impl<'de> Deserialize<'de> for ComponentDefaultValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let val = serde_json::Value::deserialize(deserializer)?;
Ok(match val {
serde_json::Value::Null => ComponentDefaultValue::None,
serde_json::Value::Bool(b) => ComponentDefaultValue::Bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
if let Ok(v) = i32::try_from(i) {
ComponentDefaultValue::I32(v)
} else {
ComponentDefaultValue::I64(i)
}
} else if let Some(f) = n.as_f64() {
ComponentDefaultValue::F64(f)
} else {
ComponentDefaultValue::None
}
}
serde_json::Value::String(s) => {
ComponentDefaultValue::String(AzString::from(s.as_str()))
}
_ => ComponentDefaultValue::None,
})
}
}
impl Serialize for OptionComponentDefaultValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
OptionComponentDefaultValue::Some(v) => v.serialize(serializer),
OptionComponentDefaultValue::None => serializer.serialize_none(),
}
}
}
impl<'de> Deserialize<'de> for OptionComponentDefaultValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let val = Option::<ComponentDefaultValue>::deserialize(deserializer)?;
Ok(match val {
Some(v) => OptionComponentDefaultValue::Some(v),
None => OptionComponentDefaultValue::None,
})
}
}
impl Serialize for ComponentDataField {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("ComponentDataField", 5)?;
s.serialize_field("name", self.name.as_str())?;
s.serialize_field("type", &self.field_type)?;
s.serialize_field("default", &self.default_value)?;
s.serialize_field("required", &self.required)?;
s.serialize_field("description", self.description.as_str())?;
s.end()
}
}
impl<'de> Deserialize<'de> for ComponentDataField {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Helper {
name: alloc::string::String,
#[serde(rename = "type", default = "default_type")]
field_type: ComponentFieldType,
#[serde(default)]
default: OptionComponentDefaultValue,
#[serde(default)]
required: bool,
#[serde(default)]
description: alloc::string::String,
}
fn default_type() -> ComponentFieldType { ComponentFieldType::String }
let h = Helper::deserialize(deserializer)?;
Ok(ComponentDataField {
name: AzString::from(h.name.as_str()),
field_type: h.field_type,
default_value: h.default,
required: h.required,
description: AzString::from(h.description.as_str()),
})
}
}
impl Serialize for ComponentDataModel {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("ComponentDataModel", 3)?;
s.serialize_field("name", self.name.as_str())?;
s.serialize_field("description", self.description.as_str())?;
let fields: alloc::vec::Vec<&ComponentDataField> = self.fields.as_ref().iter().collect();
s.serialize_field("fields", &fields)?;
s.end()
}
}
impl<'de> Deserialize<'de> for ComponentDataModel {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Helper {
#[serde(default)]
name: alloc::string::String,
#[serde(default)]
description: alloc::string::String,
#[serde(default)]
fields: alloc::vec::Vec<ComponentDataField>,
}
let h = Helper::deserialize(deserializer)?;
Ok(ComponentDataModel {
name: AzString::from(h.name.as_str()),
description: AzString::from(h.description.as_str()),
fields: ComponentDataFieldVec::from_vec(h.fields),
})
}
}
}
#[cfg(feature = "serde-json")]
pub use serde_impl::*;
#[cfg(feature = "serde-json")]
impl ComponentDataModel {
pub fn to_json(&self) -> Result<alloc::string::String, alloc::string::String> {
serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{}", e))
}
pub fn from_json(json: &str) -> Result<Self, alloc::string::String> {
serde_json::from_str(json).map_err(|e| alloc::format!("{}", e))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum ComponentSource {
Builtin,
Compiled,
UserDefined,
}
impl Default for ComponentSource {
fn default() -> Self {
ComponentSource::UserDefined
}
}
impl ComponentSource {
pub fn create() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum CompileTarget {
Rust,
C,
Cpp,
Python,
}
impl_result!(
StyledDom,
RenderDomError,
ResultStyledDomRenderDomError,
copy = false,
[Debug, Clone, PartialEq]
);
impl_result!(
AzString,
CompileError,
ResultStringCompileError,
copy = false,
[Debug, Clone, PartialEq]
);
pub type ComponentRenderFn = fn(
&ComponentDef,
&ComponentDataModel,
&ComponentMap,
) -> ResultStyledDomRenderDomError;
pub type ComponentCompileFn = fn(
&ComponentDef,
&CompileTarget,
&ComponentDataModel,
indent: usize,
) -> ResultStringCompileError;
pub type RegisterComponentFnType = extern "C" fn() -> ComponentDef;
#[repr(C)]
pub struct RegisterComponentFn {
pub cb: RegisterComponentFnType,
pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(RegisterComponentFn, RegisterComponentFnType);
pub type RegisterComponentLibraryFnType = extern "C" fn() -> ComponentLibrary;
#[repr(C)]
pub struct RegisterComponentLibraryFn {
pub cb: RegisterComponentLibraryFnType,
pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(RegisterComponentLibraryFn, RegisterComponentLibraryFnType);
#[derive(Clone)]
#[repr(C)]
pub struct ComponentDef {
pub id: ComponentId,
pub display_name: AzString,
pub description: AzString,
pub css: AzString,
pub source: ComponentSource,
pub data_model: ComponentDataModel,
pub render_fn: ComponentRenderFn,
pub compile_fn: ComponentCompileFn,
pub render_fn_source: OptionString,
pub compile_fn_source: OptionString,
}
impl fmt::Debug for ComponentDef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ComponentDef")
.field("id", &self.id)
.field("display_name", &self.display_name)
.field("source", &self.source)
.field("data_model", &self.data_model.name)
.finish()
}
}
impl_vec!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor, ComponentDefVecDestructorType, ComponentDefVecSlice, OptionComponentDef);
impl_option!(ComponentDef, OptionComponentDef, copy = false, [Clone]);
impl_vec_debug!(ComponentDef, ComponentDefVec);
impl_vec_clone!(ComponentDef, ComponentDefVec, ComponentDefVecDestructor);
impl_vec_mut!(ComponentDef, ComponentDefVec);
#[derive(Debug, Clone)]
#[repr(C)]
pub struct ComponentLibrary {
pub name: AzString,
pub version: AzString,
pub description: AzString,
pub components: ComponentDefVec,
pub exportable: bool,
pub modifiable: bool,
pub data_models: ComponentDataModelVec,
pub enum_models: ComponentEnumModelVec,
}
impl_vec!(ComponentLibrary, ComponentLibraryVec, ComponentLibraryVecDestructor, ComponentLibraryVecDestructorType, ComponentLibraryVecSlice, OptionComponentLibrary);
impl_option!(ComponentLibrary, OptionComponentLibrary, copy = false, [Debug, Clone]);
impl_vec_debug!(ComponentLibrary, ComponentLibraryVec);
impl_vec_clone!(ComponentLibrary, ComponentLibraryVec, ComponentLibraryVecDestructor);
impl_vec_mut!(ComponentLibrary, ComponentLibraryVec);
#[derive(Debug, Clone)]
#[repr(C)]
pub struct ComponentMap {
pub libraries: ComponentLibraryVec,
}
impl ComponentMap {
pub fn get(&self, collection: &str, name: &str) -> Option<&ComponentDef> {
self.libraries
.iter()
.find(|lib| lib.name.as_str() == collection)
.and_then(|lib| lib.components.iter().find(|c| c.id.name.as_str() == name))
}
pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
self.get("builtin", name)
}
pub fn get_by_qualified_name(&self, qualified: &str) -> Option<&ComponentDef> {
if let Some((collection, name)) = qualified.split_once(':') {
self.get(collection, name)
} else {
self.get_unqualified(qualified)
}
}
pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
self.libraries.iter().filter(|lib| lib.exportable).collect()
}
pub fn all_components(&self) -> Vec<&ComponentDef> {
self.libraries.iter().flat_map(|lib| lib.components.iter()).collect()
}
}
pub fn tag_to_node_type(tag: &str) -> NodeType {
match tag {
"html" => NodeType::Html,
"head" => NodeType::Head,
"title" => NodeType::Title,
"body" => NodeType::Body,
"div" => NodeType::Div,
"header" => NodeType::Header,
"footer" => NodeType::Footer,
"section" => NodeType::Section,
"article" => NodeType::Article,
"aside" => NodeType::Aside,
"nav" => NodeType::Nav,
"main" => NodeType::Main,
"figure" => NodeType::Figure,
"figcaption" => NodeType::FigCaption,
"address" => NodeType::Address,
"details" => NodeType::Details,
"summary" => NodeType::Summary,
"dialog" => NodeType::Dialog,
"h1" => NodeType::H1,
"h2" => NodeType::H2,
"h3" => NodeType::H3,
"h4" => NodeType::H4,
"h5" => NodeType::H5,
"h6" => NodeType::H6,
"p" => NodeType::P,
"span" => NodeType::Span,
"pre" => NodeType::Pre,
"code" => NodeType::Code,
"blockquote" => NodeType::BlockQuote,
"br" => NodeType::Br,
"hr" => NodeType::Hr,
"ul" => NodeType::Ul,
"ol" => NodeType::Ol,
"li" => NodeType::Li,
"dl" => NodeType::Dl,
"dt" => NodeType::Dt,
"dd" => NodeType::Dd,
"menu" => NodeType::Menu,
"menuitem" => NodeType::MenuItem,
"dir" => NodeType::Dir,
"table" => NodeType::Table,
"caption" => NodeType::Caption,
"thead" => NodeType::THead,
"tbody" => NodeType::TBody,
"tfoot" => NodeType::TFoot,
"tr" => NodeType::Tr,
"th" => NodeType::Th,
"td" => NodeType::Td,
"colgroup" => NodeType::ColGroup,
"col" => NodeType::Col,
"form" => NodeType::Form,
"fieldset" => NodeType::FieldSet,
"legend" => NodeType::Legend,
"label" => NodeType::Label,
"input" => NodeType::Input,
"button" => NodeType::Button,
"select" => NodeType::Select,
"optgroup" => NodeType::OptGroup,
"option" => NodeType::SelectOption,
"textarea" => NodeType::TextArea,
"output" => NodeType::Output,
"progress" => NodeType::Progress,
"meter" => NodeType::Meter,
"datalist" => NodeType::DataList,
"a" => NodeType::A,
"strong" => NodeType::Strong,
"em" => NodeType::Em,
"b" => NodeType::B,
"i" => NodeType::I,
"u" => NodeType::U,
"s" => NodeType::S,
"small" => NodeType::Small,
"mark" => NodeType::Mark,
"del" => NodeType::Del,
"ins" => NodeType::Ins,
"samp" => NodeType::Samp,
"kbd" => NodeType::Kbd,
"var" => NodeType::Var,
"cite" => NodeType::Cite,
"dfn" => NodeType::Dfn,
"abbr" => NodeType::Abbr,
"acronym" => NodeType::Acronym,
"q" => NodeType::Q,
"time" => NodeType::Time,
"sub" => NodeType::Sub,
"sup" => NodeType::Sup,
"big" => NodeType::Big,
"bdo" => NodeType::Bdo,
"bdi" => NodeType::Bdi,
"wbr" => NodeType::Wbr,
"ruby" => NodeType::Ruby,
"rt" => NodeType::Rt,
"rtc" => NodeType::Rtc,
"rp" => NodeType::Rp,
"data" => NodeType::Data,
"canvas" => NodeType::Canvas,
"object" => NodeType::Object,
"param" => NodeType::Param,
"embed" => NodeType::Embed,
"audio" => NodeType::Audio,
"video" => NodeType::Video,
"source" => NodeType::Source,
"track" => NodeType::Track,
"map" => NodeType::Map,
"area" => NodeType::Area,
"svg" => NodeType::Svg,
"g" => NodeType::SvgG,
"defs" => NodeType::SvgDefs,
"symbol" => NodeType::SvgSymbol,
"use" => NodeType::SvgUse,
"switch" => NodeType::SvgSwitch,
"path" => NodeType::SvgPath,
"circle" => NodeType::SvgCircle,
"rect" => NodeType::SvgRect,
"ellipse" => NodeType::SvgEllipse,
"line" => NodeType::SvgLine,
"polygon" => NodeType::SvgPolygon,
"polyline" => NodeType::SvgPolyline,
"tspan" => NodeType::SvgTspan,
"textpath" => NodeType::SvgTextPath,
"lineargradient" => NodeType::SvgLinearGradient,
"radialgradient" => NodeType::SvgRadialGradient,
"stop" => NodeType::SvgStop,
"pattern" => NodeType::SvgPattern,
"clippath" => NodeType::SvgClipPathElement,
"mask" => NodeType::SvgMask,
"filter" => NodeType::SvgFilter,
"feblend" => NodeType::SvgFeBlend,
"fecolormatrix" => NodeType::SvgFeColorMatrix,
"fecomponenttransfer" => NodeType::SvgFeComponentTransfer,
"fecomposite" => NodeType::SvgFeComposite,
"feconvolvematrix" => NodeType::SvgFeConvolveMatrix,
"fediffuselighting" => NodeType::SvgFeDiffuseLighting,
"fedisplacementmap" => NodeType::SvgFeDisplacementMap,
"fedistantlight" => NodeType::SvgFeDistantLight,
"fedropshadow" => NodeType::SvgFeDropShadow,
"feflood" => NodeType::SvgFeFlood,
"fefuncr" => NodeType::SvgFeFuncR,
"fefuncg" => NodeType::SvgFeFuncG,
"fefuncb" => NodeType::SvgFeFuncB,
"fefunca" => NodeType::SvgFeFuncA,
"fegaussianblur" => NodeType::SvgFeGaussianBlur,
"feimage" => NodeType::SvgFeImage,
"femerge" => NodeType::SvgFeMerge,
"femergenode" => NodeType::SvgFeMergeNode,
"femorphology" => NodeType::SvgFeMorphology,
"feoffset" => NodeType::SvgFeOffset,
"fepointlight" => NodeType::SvgFePointLight,
"fespecularlighting" => NodeType::SvgFeSpecularLighting,
"fespotlight" => NodeType::SvgFeSpotLight,
"fetile" => NodeType::SvgFeTile,
"feturbulence" => NodeType::SvgFeTurbulence,
"foreignobject" => NodeType::SvgForeignObject,
"desc" => NodeType::SvgDesc,
"view" => NodeType::SvgView,
"animate" => NodeType::SvgAnimate,
"animatemotion" => NodeType::SvgAnimateMotion,
"animatetransform" => NodeType::SvgAnimateTransform,
"set" => NodeType::SvgSet,
"mpath" => NodeType::SvgMpath,
"meta" => NodeType::Meta,
"link" => NodeType::Link,
"script" => NodeType::Script,
"style" => NodeType::Style,
"base" => NodeType::Base,
_ => NodeType::Div,
}
}
fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
match tag {
"html" => NodeTypeTag::Html,
"head" => NodeTypeTag::Head,
"title" => NodeTypeTag::Title,
"body" => NodeTypeTag::Body,
"div" => NodeTypeTag::Div,
"header" => NodeTypeTag::Header,
"footer" => NodeTypeTag::Footer,
"section" => NodeTypeTag::Section,
"article" => NodeTypeTag::Article,
"aside" => NodeTypeTag::Aside,
"nav" => NodeTypeTag::Nav,
"main" => NodeTypeTag::Main,
"figure" => NodeTypeTag::Figure,
"figcaption" => NodeTypeTag::FigCaption,
"address" => NodeTypeTag::Address,
"details" => NodeTypeTag::Details,
"summary" => NodeTypeTag::Summary,
"dialog" => NodeTypeTag::Dialog,
"h1" => NodeTypeTag::H1,
"h2" => NodeTypeTag::H2,
"h3" => NodeTypeTag::H3,
"h4" => NodeTypeTag::H4,
"h5" => NodeTypeTag::H5,
"h6" => NodeTypeTag::H6,
"p" => NodeTypeTag::P,
"span" => NodeTypeTag::Span,
"pre" => NodeTypeTag::Pre,
"code" => NodeTypeTag::Code,
"blockquote" => NodeTypeTag::BlockQuote,
"br" => NodeTypeTag::Br,
"hr" => NodeTypeTag::Hr,
"ul" => NodeTypeTag::Ul,
"ol" => NodeTypeTag::Ol,
"li" => NodeTypeTag::Li,
"dl" => NodeTypeTag::Dl,
"dt" => NodeTypeTag::Dt,
"dd" => NodeTypeTag::Dd,
"menu" => NodeTypeTag::Menu,
"menuitem" => NodeTypeTag::MenuItem,
"dir" => NodeTypeTag::Dir,
"table" => NodeTypeTag::Table,
"caption" => NodeTypeTag::Caption,
"thead" => NodeTypeTag::THead,
"tbody" => NodeTypeTag::TBody,
"tfoot" => NodeTypeTag::TFoot,
"tr" => NodeTypeTag::Tr,
"th" => NodeTypeTag::Th,
"td" => NodeTypeTag::Td,
"colgroup" => NodeTypeTag::ColGroup,
"col" => NodeTypeTag::Col,
"form" => NodeTypeTag::Form,
"fieldset" => NodeTypeTag::FieldSet,
"legend" => NodeTypeTag::Legend,
"label" => NodeTypeTag::Label,
"input" => NodeTypeTag::Input,
"button" => NodeTypeTag::Button,
"select" => NodeTypeTag::Select,
"optgroup" => NodeTypeTag::OptGroup,
"option" => NodeTypeTag::SelectOption,
"textarea" => NodeTypeTag::TextArea,
"output" => NodeTypeTag::Output,
"progress" => NodeTypeTag::Progress,
"meter" => NodeTypeTag::Meter,
"datalist" => NodeTypeTag::DataList,
"a" => NodeTypeTag::A,
"strong" => NodeTypeTag::Strong,
"em" => NodeTypeTag::Em,
"b" => NodeTypeTag::B,
"i" => NodeTypeTag::I,
"u" => NodeTypeTag::U,
"s" => NodeTypeTag::S,
"small" => NodeTypeTag::Small,
"mark" => NodeTypeTag::Mark,
"del" => NodeTypeTag::Del,
"ins" => NodeTypeTag::Ins,
"samp" => NodeTypeTag::Samp,
"kbd" => NodeTypeTag::Kbd,
"var" => NodeTypeTag::Var,
"cite" => NodeTypeTag::Cite,
"dfn" => NodeTypeTag::Dfn,
"abbr" => NodeTypeTag::Abbr,
"acronym" => NodeTypeTag::Acronym,
"q" => NodeTypeTag::Q,
"time" => NodeTypeTag::Time,
"sub" => NodeTypeTag::Sub,
"sup" => NodeTypeTag::Sup,
"big" => NodeTypeTag::Big,
"bdo" => NodeTypeTag::Bdo,
"bdi" => NodeTypeTag::Bdi,
"wbr" => NodeTypeTag::Wbr,
"ruby" => NodeTypeTag::Ruby,
"rt" => NodeTypeTag::Rt,
"rtc" => NodeTypeTag::Rtc,
"rp" => NodeTypeTag::Rp,
"data" => NodeTypeTag::Data,
"canvas" => NodeTypeTag::Canvas,
"object" => NodeTypeTag::Object,
"param" => NodeTypeTag::Param,
"embed" => NodeTypeTag::Embed,
"audio" => NodeTypeTag::Audio,
"video" => NodeTypeTag::Video,
"source" => NodeTypeTag::Source,
"track" => NodeTypeTag::Track,
"map" => NodeTypeTag::Map,
"area" => NodeTypeTag::Area,
"svg" => NodeTypeTag::Svg,
"g" => NodeTypeTag::SvgG,
"defs" => NodeTypeTag::SvgDefs,
"symbol" => NodeTypeTag::SvgSymbol,
"use" => NodeTypeTag::SvgUse,
"switch" => NodeTypeTag::SvgSwitch,
"path" => NodeTypeTag::SvgPath,
"circle" => NodeTypeTag::SvgCircle,
"rect" => NodeTypeTag::SvgRect,
"ellipse" => NodeTypeTag::SvgEllipse,
"line" => NodeTypeTag::SvgLine,
"polygon" => NodeTypeTag::SvgPolygon,
"polyline" => NodeTypeTag::SvgPolyline,
"tspan" => NodeTypeTag::SvgTspan,
"textpath" => NodeTypeTag::SvgTextPath,
"lineargradient" => NodeTypeTag::SvgLinearGradient,
"radialgradient" => NodeTypeTag::SvgRadialGradient,
"stop" => NodeTypeTag::SvgStop,
"pattern" => NodeTypeTag::SvgPattern,
"clippath" => NodeTypeTag::SvgClipPathElement,
"mask" => NodeTypeTag::SvgMask,
"filter" => NodeTypeTag::SvgFilter,
"feblend" => NodeTypeTag::SvgFeBlend,
"fecolormatrix" => NodeTypeTag::SvgFeColorMatrix,
"fecomponenttransfer" => NodeTypeTag::SvgFeComponentTransfer,
"fecomposite" => NodeTypeTag::SvgFeComposite,
"feconvolvematrix" => NodeTypeTag::SvgFeConvolveMatrix,
"fediffuselighting" => NodeTypeTag::SvgFeDiffuseLighting,
"fedisplacementmap" => NodeTypeTag::SvgFeDisplacementMap,
"fedistantlight" => NodeTypeTag::SvgFeDistantLight,
"fedropshadow" => NodeTypeTag::SvgFeDropShadow,
"feflood" => NodeTypeTag::SvgFeFlood,
"fefuncr" => NodeTypeTag::SvgFeFuncR,
"fefuncg" => NodeTypeTag::SvgFeFuncG,
"fefuncb" => NodeTypeTag::SvgFeFuncB,
"fefunca" => NodeTypeTag::SvgFeFuncA,
"fegaussianblur" => NodeTypeTag::SvgFeGaussianBlur,
"feimage" => NodeTypeTag::SvgFeImage,
"femerge" => NodeTypeTag::SvgFeMerge,
"femergenode" => NodeTypeTag::SvgFeMergeNode,
"femorphology" => NodeTypeTag::SvgFeMorphology,
"feoffset" => NodeTypeTag::SvgFeOffset,
"fepointlight" => NodeTypeTag::SvgFePointLight,
"fespecularlighting" => NodeTypeTag::SvgFeSpecularLighting,
"fespotlight" => NodeTypeTag::SvgFeSpotLight,
"fetile" => NodeTypeTag::SvgFeTile,
"feturbulence" => NodeTypeTag::SvgFeTurbulence,
"foreignobject" => NodeTypeTag::SvgForeignObject,
"desc" => NodeTypeTag::SvgDesc,
"view" => NodeTypeTag::SvgView,
"animate" => NodeTypeTag::SvgAnimate,
"animatemotion" => NodeTypeTag::SvgAnimateMotion,
"animatetransform" => NodeTypeTag::SvgAnimateTransform,
"set" => NodeTypeTag::SvgSet,
"mpath" => NodeTypeTag::SvgMpath,
"meta" => NodeTypeTag::Meta,
"link" => NodeTypeTag::Link,
"script" => NodeTypeTag::Script,
"style" => NodeTypeTag::Style,
"base" => NodeTypeTag::Base,
"img" | "image" => NodeTypeTag::Img,
"icon" => NodeTypeTag::Icon,
_ => NodeTypeTag::Div,
}
}
fn builtin_render_fn(
def: &ComponentDef,
data: &ComponentDataModel,
_component_map: &ComponentMap,
) -> ResultStyledDomRenderDomError {
let node_type = tag_to_node_type(def.id.name.as_str());
let mut dom = Dom::create_node(node_type);
if let Some(text_str) = data.get_default_string("text") {
let prepared = prepare_string(text_str);
if !prepared.is_empty() {
dom = dom.with_children(alloc::vec![Dom::create_text(prepared)].into());
}
}
let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut dom, Css::empty()));
r.into()
}
fn builtin_compile_fn(
def: &ComponentDef,
target: &CompileTarget,
data: &ComponentDataModel,
indent: usize,
) -> ResultStringCompileError {
let node_type = tag_to_node_type(def.id.name.as_str());
let type_name = format!("{:?}", node_type); let text = data.get_default_string("text");
let r: Result<AzString, CompileError> = match target {
CompileTarget::Rust => {
if let Some(text_str) = text {
Ok(format!(
"Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text(AzString::from_const_str(\"{}\"))].into())",
type_name,
text_str.as_str().replace("\\", "\\\\").replace("\"", "\\\"")
).into())
} else {
Ok(format!("Dom::create_node(NodeType::{})", type_name).into())
}
}
CompileTarget::C => {
if let Some(text_str) = text {
Ok(format!(
"AzDom_createText(AzString_fromConstStr(\"{}\"))",
text_str.as_str().replace("\\", "\\\\").replace("\"", "\\\"")
).into())
} else {
Ok(format!("AzDom_create{}()", type_name).into())
}
}
CompileTarget::Cpp => {
Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into())
}
CompileTarget::Python => {
Ok(format!("Dom.{}()", type_name.to_lowercase()).into())
}
};
r.into()
}
fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn core::fmt::Display) {
use crate::dom::{Dom, NodeType};
let text = alloc::format!("{}: {}", field_name, value);
children.push(
Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text)].into()),
);
}
pub fn user_defined_render_fn(
def: &ComponentDef,
data: &ComponentDataModel,
component_map: &ComponentMap,
) -> ResultStyledDomRenderDomError {
use crate::dom::{Dom, NodeType};
use azul_css::css::Css;
let mut children: Vec<Dom> = Vec::new();
for field in data.fields.as_ref().iter() {
let field_name = field.name.as_str();
match &field.default_value {
OptionComponentDefaultValue::None => {
continue;
}
OptionComponentDefaultValue::Some(default_val) => {
match default_val {
ComponentDefaultValue::String(s) => {
let text = s.as_str().trim();
if !text.is_empty() {
let label_dom = Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text.to_string())].into());
children.push(label_dom);
}
}
ComponentDefaultValue::Bool(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::I32(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::I64(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::U32(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::U64(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::Usize(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::F32(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::F64(v) => {
push_scalar_field(&mut children, field_name, v);
}
ComponentDefaultValue::ColorU(c) => {
let text = alloc::format!("{}: #{:02x}{:02x}{:02x}{:02x}", field_name, c.r, c.g, c.b, c.a);
children.push(Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text)].into()));
}
ComponentDefaultValue::ComponentInstance(ci) => {
if let Some(sub_comp) = component_map.get(ci.library.as_str(), ci.component.as_str()) {
let sub_data = sub_comp.data_model.clone();
match (sub_comp.render_fn)(sub_comp, &sub_data, component_map) {
ResultStyledDomRenderDomError::Ok(_styled_dom) => {
let text = alloc::format!("[{}:{}]", ci.library.as_str(), ci.component.as_str());
children.push(Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text)].into()));
}
ResultStyledDomRenderDomError::Err(_) => {
let text = alloc::format!("[Error rendering {}:{}]", ci.library.as_str(), ci.component.as_str());
children.push(Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text)].into()));
}
}
} else {
let text = alloc::format!("[Unknown component {}:{}]", ci.library.as_str(), ci.component.as_str());
children.push(Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text)].into()));
}
}
ComponentDefaultValue::CallbackFnPointer(name) => {
let text = alloc::format!("{}: fn({})", field_name, name.as_str());
children.push(Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text)].into()));
}
ComponentDefaultValue::Json(json_str) => {
let text = alloc::format!("{}: {}", field_name, json_str.as_str());
children.push(Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(text)].into()));
}
ComponentDefaultValue::None => {
continue;
}
}
}
}
}
let mut wrapper = Dom::create_node(NodeType::Div);
if !children.is_empty() {
wrapper = wrapper.with_children(children.into());
}
let css = if !def.css.as_str().is_empty() {
Css::from_string(def.css.clone())
} else {
Css::empty()
};
let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
r.into()
}
pub fn user_defined_compile_fn(
def: &ComponentDef,
target: &CompileTarget,
data: &ComponentDataModel,
indent: usize,
) -> ResultStringCompileError {
let tag = def.id.name.as_str();
let indent_str = " ".repeat(indent * 4);
let inner_indent = " ".repeat((indent + 1) * 4);
let r: Result<AzString, CompileError> = match target {
CompileTarget::Rust => {
let mut lines = Vec::new();
lines.push(alloc::format!("{}// Component: {}", indent_str, tag));
lines.push(alloc::format!("{}let mut children: Vec<Dom> = Vec::new();", indent_str));
for field in data.fields.as_ref().iter() {
let fname = field.name.as_str();
match &field.default_value {
OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"");
lines.push(alloc::format!(
"{}children.push(Dom::create_text(AzString::from_const_str(\"{}\")));",
inner_indent, escaped
));
}
OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
lines.push(alloc::format!(
"{}children.push(Dom::create_text(AzString::from(format!(\"{{}}: {{}}\", \"{}\", {}).as_str())));",
inner_indent, fname, b
));
}
OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
lines.push(alloc::format!(
"{}children.push({}()); // sub-component {}:{}",
inner_indent, fn_name, ci.library.as_str(), ci.component.as_str()
));
}
_ => {
lines.push(alloc::format!(
"{}// field '{}': {:?}",
inner_indent, fname, field.field_type
));
}
}
}
lines.push(alloc::format!(
"{}Dom::create_node(NodeType::Div).with_children(children.into())",
indent_str
));
Ok(lines.join("\n").into())
}
CompileTarget::C => {
let mut lines = Vec::new();
lines.push(alloc::format!("{}/* Component: {} */", indent_str, tag));
lines.push(alloc::format!("{}AzDom root = AzDom_createDiv();", indent_str));
for field in data.fields.as_ref().iter() {
let fname = field.name.as_str();
match &field.default_value {
OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"");
lines.push(alloc::format!(
"{}AzDom_addChild(&root, AzDom_createText(AzString_fromConstStr(\"{}\")));",
inner_indent, escaped
));
}
OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
lines.push(alloc::format!(
"{}AzDom_addChild(&root, {}());",
inner_indent, fn_name
));
}
_ => {
lines.push(alloc::format!(
"{}/* field '{}' */",
inner_indent, fname
));
}
}
}
lines.push(alloc::format!("{}return root;", indent_str));
Ok(lines.join("\n").into())
}
CompileTarget::Cpp => {
let mut lines = Vec::new();
lines.push(alloc::format!("{}// Component: {}", indent_str, tag));
lines.push(alloc::format!("{}auto root = Dom::create_div();", indent_str));
for field in data.fields.as_ref().iter() {
let fname = field.name.as_str();
match &field.default_value {
OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"");
lines.push(alloc::format!(
"{}root.add_child(Dom::create_text(\"{}\"));",
inner_indent, escaped
));
}
OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
lines.push(alloc::format!(
"{}root.add_child({}());",
inner_indent, fn_name
));
}
_ => {
lines.push(alloc::format!(
"{}// field '{}'",
inner_indent, fname
));
}
}
}
lines.push(alloc::format!("{}return root;", indent_str));
Ok(lines.join("\n").into())
}
CompileTarget::Python => {
let mut lines = Vec::new();
lines.push(alloc::format!("{}# Component: {}", indent_str, tag));
lines.push(alloc::format!("{}root = Dom.div()", indent_str));
for field in data.fields.as_ref().iter() {
let fname = field.name.as_str();
match &field.default_value {
OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => {
let escaped = s.as_str().replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "\\'");
lines.push(alloc::format!(
"{}root.add_child(Dom.text('{}'))",
inner_indent, escaped
));
}
OptionComponentDefaultValue::Some(ComponentDefaultValue::ComponentInstance(ci)) => {
let fn_name = alloc::format!("render_{}", ci.component.as_str().replace("-", "_"));
lines.push(alloc::format!(
"{}root.add_child({}())",
inner_indent, fn_name
));
}
_ => {
lines.push(alloc::format!(
"{}# field '{}'",
inner_indent, fname
));
}
}
}
lines.push(alloc::format!("{}return root", indent_str));
Ok(lines.join("\n").into())
}
};
r.into()
}
fn builtin_component_def(tag: &str, display_name: &str, default_text: Option<&str>, css: &str) -> ComponentDef {
let mut fields = builtin_data_model(tag);
if let Some(text) = default_text {
fields.push(data_field("text", ComponentFieldType::String,
Some(ComponentDefaultValue::String(AzString::from(text))),
"Text content of the element"));
}
let model_name = format!("{}Data", display_name);
ComponentDef {
id: ComponentId::builtin(tag),
display_name: AzString::from(display_name),
description: AzString::from(format!("HTML <{}> element", tag).as_str()),
css: AzString::from(css),
source: ComponentSource::Builtin,
data_model: ComponentDataModel {
name: AzString::from(model_name.as_str()),
description: AzString::from(format!("Data model for <{}>", tag).as_str()),
fields: fields.into(),
},
render_fn: builtin_render_fn,
compile_fn: builtin_compile_fn,
render_fn_source: None.into(),
compile_fn_source: None.into(),
}
}
fn data_field(name: &str, ft: ComponentFieldType, default: Option<ComponentDefaultValue>, description: &str) -> ComponentDataField {
let required = default.is_none();
ComponentDataField {
name: AzString::from(name),
field_type: ft,
default_value: match default {
Some(d) => OptionComponentDefaultValue::Some(d),
None => OptionComponentDefaultValue::None,
},
required,
description: AzString::from(description),
}
}
fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
use ComponentFieldType::*;
use ComponentDefaultValue as D;
match tag {
"a" => alloc::vec![
data_field("href", String, Some(D::String(AzString::from_const_str(""))), "URL the link points to"),
data_field("target", String, Some(D::String(AzString::from_const_str(""))), "Where to open the linked document (_blank, _self, _parent, _top)"),
data_field("rel", String, Some(D::String(AzString::from_const_str(""))), "Relationship between current and linked document"),
],
"img" | "image" => alloc::vec![
data_field("src", String, None, "URL of the image"),
data_field("alt", String, Some(D::String(AzString::from_const_str(""))), "Alternative text for the image"),
data_field("width", String, Some(D::String(AzString::from_const_str(""))), "Width of the image"),
data_field("height", String, Some(D::String(AzString::from_const_str(""))), "Height of the image"),
],
"form" => alloc::vec![
data_field("action", String, Some(D::String(AzString::from_const_str(""))), "URL where form data is submitted"),
data_field("method", String, Some(D::String(AzString::from_const_str("GET"))), "HTTP method for form submission (GET or POST)"),
],
"label" => alloc::vec![
data_field("for", String, Some(D::String(AzString::from_const_str(""))), "ID of the form element this label is for"),
],
"button" => alloc::vec![
data_field("type", String, Some(D::String(AzString::from_const_str("button"))), "Button type (button, submit, reset)"),
data_field("disabled", Bool, Some(D::Bool(false)), "Whether the button is disabled"),
],
"td" | "th" => alloc::vec![
data_field("colspan", I32, Some(D::I32(1)), "Number of columns the cell spans"),
data_field("rowspan", I32, Some(D::I32(1)), "Number of rows the cell spans"),
],
"icon" => alloc::vec![
data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Icon name"),
],
"ol" => alloc::vec![
data_field("start", I32, Some(D::I32(1)), "Start value for the ordered list"),
data_field("type", String, Some(D::String(AzString::from_const_str("1"))), "Numbering type (1, A, a, I, i)"),
],
"input" => alloc::vec![
data_field("type", String, Some(D::String(AzString::from_const_str("text"))), "Input type (text, password, email, number, checkbox, radio, etc.)"),
data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name of the input for form submission"),
data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Current value of the input"),
data_field("placeholder", String, Some(D::String(AzString::from_const_str(""))), "Placeholder text"),
data_field("disabled", Bool, Some(D::Bool(false)), "Whether the input is disabled"),
data_field("required", Bool, Some(D::Bool(false)), "Whether the input is required"),
data_field("readonly", Bool, Some(D::Bool(false)), "Whether the input is read-only"),
data_field("checked", Bool, Some(D::Bool(false)), "Whether the checkbox/radio is checked"),
data_field("min", String, Some(D::String(AzString::from_const_str(""))), "Minimum value (for number, range, date)"),
data_field("max", String, Some(D::String(AzString::from_const_str(""))), "Maximum value (for number, range, date)"),
data_field("step", String, Some(D::String(AzString::from_const_str(""))), "Step increment (for number, range)"),
data_field("pattern", String, Some(D::String(AzString::from_const_str(""))), "Regex pattern for validation"),
data_field("maxlength", String, Some(D::String(AzString::from_const_str(""))), "Maximum number of characters"),
],
"select" => alloc::vec![
data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name for form submission"),
data_field("multiple", Bool, Some(D::Bool(false)), "Whether multiple options can be selected"),
data_field("disabled", Bool, Some(D::Bool(false)), "Whether the select is disabled"),
data_field("required", Bool, Some(D::Bool(false)), "Whether selection is required"),
data_field("size", String, Some(D::String(AzString::from_const_str(""))), "Number of visible options"),
],
"option" => alloc::vec![
data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Value submitted with the form"),
data_field("selected", Bool, Some(D::Bool(false)), "Whether this option is selected"),
data_field("disabled", Bool, Some(D::Bool(false)), "Whether this option is disabled"),
],
"optgroup" => alloc::vec![
data_field("label", String, Some(D::String(AzString::from_const_str(""))), "Label for the option group"),
data_field("disabled", Bool, Some(D::Bool(false)), "Whether the group is disabled"),
],
"textarea" => alloc::vec![
data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name for form submission"),
data_field("placeholder", String, Some(D::String(AzString::from_const_str(""))), "Placeholder text"),
data_field("rows", I32, Some(D::I32(2)), "Number of visible text lines"),
data_field("cols", I32, Some(D::I32(20)), "Visible width in average character widths"),
data_field("disabled", Bool, Some(D::Bool(false)), "Whether the textarea is disabled"),
data_field("required", Bool, Some(D::Bool(false)), "Whether content is required"),
data_field("readonly", Bool, Some(D::Bool(false)), "Whether the textarea is read-only"),
data_field("maxlength", String, Some(D::String(AzString::from_const_str(""))), "Maximum number of characters"),
],
"fieldset" => alloc::vec![
data_field("disabled", Bool, Some(D::Bool(false)), "Whether all controls in the fieldset are disabled"),
],
"output" => alloc::vec![
data_field("for", String, Some(D::String(AzString::from_const_str(""))), "IDs of elements that contributed to the output"),
data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Name for form submission"),
],
"progress" => alloc::vec![
data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Current progress value"),
data_field("max", String, Some(D::String(AzString::from_const_str("1"))), "Maximum value"),
],
"meter" => alloc::vec![
data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Current value"),
data_field("min", String, Some(D::String(AzString::from_const_str("0"))), "Minimum value"),
data_field("max", String, Some(D::String(AzString::from_const_str("1"))), "Maximum value"),
data_field("low", String, Some(D::String(AzString::from_const_str(""))), "Low threshold"),
data_field("high", String, Some(D::String(AzString::from_const_str(""))), "High threshold"),
data_field("optimum", String, Some(D::String(AzString::from_const_str(""))), "Optimum value"),
],
"details" => alloc::vec![
data_field("open", Bool, Some(D::Bool(false)), "Whether the details are visible"),
],
"dialog" => alloc::vec![
data_field("open", Bool, Some(D::Bool(false)), "Whether the dialog is active and can be interacted with"),
],
"audio" | "video" => alloc::vec![
data_field("src", String, Some(D::String(AzString::from_const_str(""))), "URL of the media resource"),
data_field("controls", Bool, Some(D::Bool(false)), "Whether to show playback controls"),
data_field("autoplay", Bool, Some(D::Bool(false)), "Whether to start playing automatically"),
data_field("loop", Bool, Some(D::Bool(false)), "Whether to loop playback"),
data_field("muted", Bool, Some(D::Bool(false)), "Whether audio is muted"),
data_field("preload", String, Some(D::String(AzString::from_const_str("auto"))), "Preload hint (none, metadata, auto)"),
],
"source" => alloc::vec![
data_field("src", String, None, "URL of the media resource"),
data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the resource"),
],
"track" => alloc::vec![
data_field("src", String, None, "URL of the track file"),
data_field("kind", String, Some(D::String(AzString::from_const_str("subtitles"))), "Kind of text track (subtitles, captions, descriptions, chapters, metadata)"),
data_field("srclang", String, Some(D::String(AzString::from_const_str(""))), "Language of the track text"),
data_field("label", String, Some(D::String(AzString::from_const_str(""))), "User-readable title for the track"),
data_field("default", Bool, Some(D::Bool(false)), "Whether this is the default track"),
],
"canvas" => alloc::vec![
data_field("width", String, Some(D::String(AzString::from_const_str("300"))), "Width of the canvas in pixels"),
data_field("height", String, Some(D::String(AzString::from_const_str("150"))), "Height of the canvas in pixels"),
],
"embed" => alloc::vec![
data_field("src", String, None, "URL of the resource to embed"),
data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the embedded content"),
data_field("width", String, Some(D::String(AzString::from_const_str(""))), "Width"),
data_field("height", String, Some(D::String(AzString::from_const_str(""))), "Height"),
],
"object" => alloc::vec![
data_field("data", String, Some(D::String(AzString::from_const_str(""))), "URL of the resource"),
data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the resource"),
data_field("width", String, Some(D::String(AzString::from_const_str(""))), "Width"),
data_field("height", String, Some(D::String(AzString::from_const_str(""))), "Height"),
],
"param" => alloc::vec![
data_field("name", String, None, "Name of the parameter"),
data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Value of the parameter"),
],
"area" => alloc::vec![
data_field("shape", String, Some(D::String(AzString::from_const_str("default"))), "Shape of the area (default, rect, circle, poly)"),
data_field("coords", String, Some(D::String(AzString::from_const_str(""))), "Coordinates of the area"),
data_field("href", String, Some(D::String(AzString::from_const_str(""))), "URL for the area link"),
data_field("alt", String, Some(D::String(AzString::from_const_str(""))), "Alternative text"),
data_field("target", String, Some(D::String(AzString::from_const_str(""))), "Where to open the linked document"),
],
"map" => alloc::vec![
data_field("name", String, None, "Name of the image map (referenced by usemap)"),
],
"time" => alloc::vec![
data_field("datetime", String, Some(D::String(AzString::from_const_str(""))), "Machine-readable date/time value"),
],
"data" => alloc::vec![
data_field("value", String, Some(D::String(AzString::from_const_str(""))), "Machine-readable value"),
],
"abbr" | "acronym" | "dfn" => alloc::vec![
data_field("title", String, Some(D::String(AzString::from_const_str(""))), "Full expansion or definition"),
],
"q" | "blockquote" => alloc::vec![
data_field("cite", String, Some(D::String(AzString::from_const_str(""))), "URL of the source of the quotation"),
],
"del" | "ins" => alloc::vec![
data_field("cite", String, Some(D::String(AzString::from_const_str(""))), "URL explaining the change"),
data_field("datetime", String, Some(D::String(AzString::from_const_str(""))), "Date/time of the change"),
],
"bdo" => alloc::vec![
data_field("dir", String, Some(D::String(AzString::from_const_str("ltr"))), "Text direction (ltr, rtl)"),
],
"col" | "colgroup" => alloc::vec![
data_field("span", I32, Some(D::I32(1)), "Number of columns the element spans"),
],
"meta" => alloc::vec![
data_field("name", String, Some(D::String(AzString::from_const_str(""))), "Metadata name"),
data_field("content", String, Some(D::String(AzString::from_const_str(""))), "Metadata value"),
data_field("charset", String, Some(D::String(AzString::from_const_str(""))), "Character encoding"),
data_field("http-equiv", String, Some(D::String(AzString::from_const_str(""))), "HTTP header equivalent"),
],
"link" => alloc::vec![
data_field("rel", String, None, "Relationship type"),
data_field("href", String, Some(D::String(AzString::from_const_str(""))), "URL of the linked resource"),
data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type of the linked resource"),
],
"script" => alloc::vec![
data_field("src", String, Some(D::String(AzString::from_const_str(""))), "URL of external script"),
data_field("type", String, Some(D::String(AzString::from_const_str(""))), "MIME type or module"),
data_field("async", Bool, Some(D::Bool(false)), "Execute asynchronously"),
data_field("defer", Bool, Some(D::Bool(false)), "Defer execution until page load"),
],
"style" => alloc::vec![
data_field("type", String, Some(D::String(AzString::from_const_str("text/css"))), "MIME type of the style sheet"),
],
"base" => alloc::vec![
data_field("href", String, Some(D::String(AzString::from_const_str(""))), "Base URL for relative URLs"),
data_field("target", String, Some(D::String(AzString::from_const_str(""))), "Default target for hyperlinks"),
],
_ => alloc::vec![],
}
}
impl Default for ComponentMap {
fn default() -> Self {
ComponentMap {
libraries: ComponentLibraryVec::from_const_slice(&[]),
}
}
}
impl ComponentMap {
pub fn create() -> Self {
Self::default()
}
pub fn with_builtin() -> Self {
ComponentMap {
libraries: alloc::vec![register_builtin_components()].into(),
}
}
pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
ComponentMap {
libraries: libs.clone(),
}
}
}
fn xml_attrs_to_data_model(
base_model: &ComponentDataModel,
xml_attributes: &XmlAttributeMap,
text_content: Option<&str>,
) -> ComponentDataModel {
let mut model = base_model.clone();
let mut fields_vec = core::mem::replace(
&mut model.fields,
ComponentDataFieldVec::from_const_slice(&[]),
).into_library_owned_vec();
for field in fields_vec.iter_mut() {
if let Some(attr_value) = xml_attributes.get_key(field.name.as_str()) {
field.default_value = OptionComponentDefaultValue::Some(
ComponentDefaultValue::String(attr_value.clone()),
);
}
}
model.fields = ComponentDataFieldVec::from_vec(fields_vec);
if let Some(text) = text_content {
let prepared = prepare_string(text);
if !prepared.is_empty() {
model = model.with_default("text", ComponentDefaultValue::String(AzString::from(prepared.as_str())));
}
}
model
}
fn builtin_if_component() -> ComponentDef {
ComponentDef {
id: ComponentId::builtin("if"),
display_name: AzString::from_const_str("If"),
description: AzString::from_const_str("Conditional rendering: shows 'then' if condition is true, else shows 'else' (if provided)."),
css: AzString::from_const_str(""),
source: ComponentSource::Builtin,
data_model: ComponentDataModel {
name: AzString::from_const_str("IfData"),
description: AzString::from_const_str("Data for conditional rendering"),
fields: alloc::vec![
data_field("condition", ComponentFieldType::Bool, Some(ComponentDefaultValue::Bool(false)), "The boolean condition to evaluate"),
].into(),
},
render_fn: builtin_if_render_fn,
compile_fn: builtin_if_compile_fn,
render_fn_source: None.into(),
compile_fn_source: None.into(),
}
}
fn builtin_if_render_fn(
_comp: &ComponentDef,
data_model: &ComponentDataModel,
_component_map: &ComponentMap,
) -> ResultStyledDomRenderDomError {
let condition = data_model.fields.iter().find(|f| f.name.as_str() == "condition")
.and_then(|f| match &f.default_value { OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => Some(*b), _ => None })
.unwrap_or(false);
let label = if condition { "if: true (then branch)" } else { "if: false (else branch)" };
let mut dom = Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(label)].into());
let css = Css::empty();
ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
}
fn builtin_if_compile_fn(
_comp: &ComponentDef,
target: &CompileTarget,
_data: &ComponentDataModel,
_indent: usize,
) -> ResultStringCompileError {
match target {
CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
"if data.condition {\n // then branch\n Dom::div()\n} else {\n // else branch\n Dom::div()\n}"
)),
CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
"if (data.condition) {\n // then branch\n AzDom_createDiv();\n} else {\n // else branch\n AzDom_createDiv();\n}"
)),
CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
"if (data.condition) {\n // then branch\n Dom::div();\n} else {\n // else branch\n Dom::div();\n}"
)),
CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
"if data.condition:\n # then branch\n Dom.div()\nelse:\n # else branch\n Dom.div()"
)),
}
}
fn builtin_for_component() -> ComponentDef {
ComponentDef {
id: ComponentId::builtin("for"),
display_name: AzString::from_const_str("For Loop"),
description: AzString::from_const_str("Iterative rendering: repeats children 'count' times."),
css: AzString::from_const_str(""),
source: ComponentSource::Builtin,
data_model: ComponentDataModel {
name: AzString::from_const_str("ForData"),
description: AzString::from_const_str("Data for iterative rendering"),
fields: alloc::vec![
data_field("count", ComponentFieldType::U32, Some(ComponentDefaultValue::U32(3)), "Number of iterations"),
].into(),
},
render_fn: builtin_for_render_fn,
compile_fn: builtin_for_compile_fn,
render_fn_source: None.into(),
compile_fn_source: None.into(),
}
}
fn builtin_for_render_fn(
_comp: &ComponentDef,
data_model: &ComponentDataModel,
_component_map: &ComponentMap,
) -> ResultStyledDomRenderDomError {
let count = data_model.fields.iter().find(|f| f.name.as_str() == "count")
.and_then(|f| match &f.default_value { OptionComponentDefaultValue::Some(ComponentDefaultValue::U32(n)) => Some(*n), _ => None })
.unwrap_or(3);
let mut items: alloc::vec::Vec<Dom> = alloc::vec::Vec::new();
for i in 0..count {
items.push(Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(alloc::format!("Item {}", i))].into()));
}
let mut dom = Dom::create_node(NodeType::Div)
.with_children(items.into());
let css = Css::empty();
ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
}
fn builtin_for_compile_fn(
_comp: &ComponentDef,
target: &CompileTarget,
_data: &ComponentDataModel,
_indent: usize,
) -> ResultStringCompileError {
match target {
CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
"let mut children = Vec::new();\nfor i in 0..data.count {\n children.push(Dom::div());\n}\nDom::div().with_children(children)"
)),
CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
"AzDom container = AzDom_createDiv();\nfor (uint32_t i = 0; i < data.count; i++) {\n AzDom_addChild(&container, AzDom_createDiv());\n}"
)),
CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
"auto container = Dom::div();\nfor (uint32_t i = 0; i < data.count; i++) {\n container.add_child(Dom::div());\n}"
)),
CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
"container = Dom.div()\nfor i in range(data.count):\n container.add_child(Dom.div())"
)),
}
}
fn builtin_map_component() -> ComponentDef {
ComponentDef {
id: ComponentId::builtin("map"),
display_name: AzString::from_const_str("Map"),
description: AzString::from_const_str("Map data to DOM: applies a template to each item in a collection."),
css: AzString::from_const_str(""),
source: ComponentSource::Builtin,
data_model: ComponentDataModel {
name: AzString::from_const_str("MapData"),
description: AzString::from_const_str("Data for map rendering"),
fields: alloc::vec![
data_field("data_json", ComponentFieldType::String, Some(ComponentDefaultValue::String(AzString::from_const_str("[]"))), "JSON array of items to map over"),
].into(),
},
render_fn: builtin_map_render_fn,
compile_fn: builtin_map_compile_fn,
render_fn_source: None.into(),
compile_fn_source: None.into(),
}
}
fn builtin_map_render_fn(
_comp: &ComponentDef,
data_model: &ComponentDataModel,
_component_map: &ComponentMap,
) -> ResultStyledDomRenderDomError {
let data_str = data_model.fields.iter().find(|f| f.name.as_str() == "data_json")
.and_then(|f| match &f.default_value { OptionComponentDefaultValue::Some(ComponentDefaultValue::String(s)) => Some(s.as_str().to_string()), _ => None })
.unwrap_or_else(|| "[]".to_string());
let label = alloc::format!("map: data_json={}", data_str);
let mut dom = Dom::create_node(NodeType::Div)
.with_children(alloc::vec![Dom::create_text(label)].into());
let css = Css::empty();
ResultStyledDomRenderDomError::Ok(StyledDom::create(&mut dom, css))
}
fn builtin_map_compile_fn(
_comp: &ComponentDef,
target: &CompileTarget,
_data: &ComponentDataModel,
_indent: usize,
) -> ResultStringCompileError {
match target {
CompileTarget::Rust => ResultStringCompileError::Ok(AzString::from(
"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)"
)),
CompileTarget::C => ResultStringCompileError::Ok(AzString::from(
"// Parse data.data_json and map each item\nAzDom container = AzDom_createDiv();\n// TODO: iterate parsed JSON array"
)),
CompileTarget::Cpp => ResultStringCompileError::Ok(AzString::from(
"// Parse data.data_json and map each item\nauto container = Dom::div();\n// TODO: iterate parsed JSON array"
)),
CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
"import json\nitems = json.loads(data.data_json)\ncontainer = Dom.div()\nfor item in items:\n container.add_child(Dom.div())"
)),
}
}
pub extern "C" fn register_builtin_components() -> ComponentLibrary {
ComponentLibrary {
name: AzString::from_const_str("builtin"),
version: AzString::from_const_str("1.0.0"),
description: AzString::from_const_str("Built-in HTML elements"),
exportable: false,
modifiable: false,
data_models: Vec::new().into(),
enum_models: Vec::new().into(),
components: alloc::vec![
builtin_component_def("html", "HTML", None, ""),
builtin_component_def("head", "Head", None, ""),
builtin_component_def("title", "Title", Some(""), ""),
builtin_component_def("body", "Body", None, ""),
builtin_component_def("div", "Div", None, ""),
builtin_component_def("header", "Header", None, ""),
builtin_component_def("footer", "Footer", None, ""),
builtin_component_def("section", "Section", None, ""),
builtin_component_def("article", "Article", None, ""),
builtin_component_def("aside", "Aside", None, ""),
builtin_component_def("nav", "Nav", None, ""),
builtin_component_def("main", "Main", None, ""),
builtin_component_def("figure", "Figure", None, ""),
builtin_component_def("figcaption", "Figure Caption", Some(""), ""),
builtin_component_def("address", "Address", Some(""), ""),
builtin_component_def("details", "Details", None, ""),
builtin_component_def("summary", "Summary", Some("Details"), ""),
builtin_component_def("dialog", "Dialog", None, ""),
builtin_component_def("h1", "Heading 1", Some("Heading 1"), ""),
builtin_component_def("h2", "Heading 2", Some("Heading 2"), ""),
builtin_component_def("h3", "Heading 3", Some("Heading 3"), ""),
builtin_component_def("h4", "Heading 4", Some("Heading 4"), ""),
builtin_component_def("h5", "Heading 5", Some("Heading 5"), ""),
builtin_component_def("h6", "Heading 6", Some("Heading 6"), ""),
builtin_component_def("p", "Paragraph", Some("Paragraph text"), ""),
builtin_component_def("span", "Span", Some(""), ""),
builtin_component_def("pre", "Preformatted", Some(""), ""),
builtin_component_def("code", "Code", Some(""), ""),
builtin_component_def("blockquote", "Blockquote", Some(""), ""),
builtin_component_def("br", "Line Break", None, ""),
builtin_component_def("hr", "Horizontal Rule", None, ""),
builtin_component_def("icon", "Icon", Some(""), ""),
builtin_component_def("ul", "Unordered List", None, ""),
builtin_component_def("ol", "Ordered List", None, ""),
builtin_component_def("li", "List Item", Some("List item"), ""),
builtin_component_def("dl", "Description List", None, ""),
builtin_component_def("dt", "Description Term", Some(""), ""),
builtin_component_def("dd", "Description Details", Some(""), ""),
builtin_component_def("menu", "Menu", None, ""),
builtin_component_def("menuitem", "Menu Item", Some(""), ""),
builtin_component_def("dir", "Directory List", None, ""),
builtin_component_def("table", "Table", None, ""),
builtin_component_def("caption", "Table Caption", Some(""), ""),
builtin_component_def("thead", "Table Head", None, ""),
builtin_component_def("tbody", "Table Body", None, ""),
builtin_component_def("tfoot", "Table Foot", None, ""),
builtin_component_def("tr", "Table Row", None, ""),
builtin_component_def("th", "Table Header Cell", Some("Header"), ""),
builtin_component_def("td", "Table Data Cell", Some(""), ""),
builtin_component_def("colgroup", "Column Group", None, ""),
builtin_component_def("col", "Column", None, ""),
builtin_component_def("a", "Link", Some("Link text"), ""),
builtin_component_def("strong", "Strong", Some(""), ""),
builtin_component_def("em", "Emphasis", Some(""), ""),
builtin_component_def("b", "Bold", Some(""), ""),
builtin_component_def("i", "Italic", Some(""), ""),
builtin_component_def("u", "Underline", Some(""), ""),
builtin_component_def("s", "Strikethrough", Some(""), ""),
builtin_component_def("small", "Small", Some(""), ""),
builtin_component_def("mark", "Mark", Some(""), ""),
builtin_component_def("del", "Deleted Text", Some(""), ""),
builtin_component_def("ins", "Inserted Text", Some(""), ""),
builtin_component_def("sub", "Subscript", Some(""), ""),
builtin_component_def("sup", "Superscript", Some(""), ""),
builtin_component_def("samp", "Sample Output", Some(""), ""),
builtin_component_def("kbd", "Keyboard Input", Some(""), ""),
builtin_component_def("var", "Variable", Some(""), ""),
builtin_component_def("cite", "Citation", Some(""), ""),
builtin_component_def("dfn", "Definition", Some(""), ""),
builtin_component_def("abbr", "Abbreviation", Some(""), ""),
builtin_component_def("acronym", "Acronym", Some(""), ""),
builtin_component_def("q", "Inline Quote", Some(""), ""),
builtin_component_def("time", "Time", Some(""), ""),
builtin_component_def("big", "Big", Some(""), ""),
builtin_component_def("bdo", "BiDi Override", Some(""), ""),
builtin_component_def("bdi", "BiDi Isolate", Some(""), ""),
builtin_component_def("wbr", "Word Break Opportunity", None, ""),
builtin_component_def("ruby", "Ruby Annotation", None, ""),
builtin_component_def("rt", "Ruby Text", Some(""), ""),
builtin_component_def("rtc", "Ruby Text Container", None, ""),
builtin_component_def("rp", "Ruby Parenthesis", Some(""), ""),
builtin_component_def("data", "Data", Some(""), ""),
builtin_component_def("form", "Form", None, ""),
builtin_component_def("fieldset", "Field Set", None, ""),
builtin_component_def("legend", "Legend", Some("Legend"), ""),
builtin_component_def("label", "Label", Some("Label"), ""),
builtin_component_def("input", "Input", None, ""),
builtin_component_def("button", "Button", Some("Button text"), ""),
builtin_component_def("select", "Select", None, ""),
builtin_component_def("optgroup", "Option Group", None, ""),
builtin_component_def("option", "Option", Some(""), ""),
builtin_component_def("textarea", "Text Area", Some(""), ""),
builtin_component_def("output", "Output", Some(""), ""),
builtin_component_def("progress", "Progress", None, ""),
builtin_component_def("meter", "Meter", None, ""),
builtin_component_def("datalist", "Data List", None, ""),
builtin_component_def("canvas", "Canvas", None, ""),
builtin_component_def("object", "Object", None, ""),
builtin_component_def("param", "Parameter", None, ""),
builtin_component_def("embed", "Embed", None, ""),
builtin_component_def("audio", "Audio", None, ""),
builtin_component_def("video", "Video", None, ""),
builtin_component_def("source", "Source", None, ""),
builtin_component_def("track", "Track", None, ""),
builtin_component_def("map", "Image Map", None, ""),
builtin_component_def("area", "Map Area", None, ""),
builtin_component_def("svg", "SVG", None, ""),
builtin_component_def("meta", "Meta", None, ""),
builtin_component_def("link", "Link (Resource)", None, ""),
builtin_component_def("script", "Script", Some(""), ""),
builtin_component_def("style", "Style", Some(""), ""),
builtin_component_def("base", "Base URL", None, ""),
builtin_if_component(),
builtin_for_component(),
builtin_map_component(),
].into(),
}
}
#[derive(Default)]
pub struct DomXml {
pub parsed_dom: StyledDom,
}
impl DomXml {
#[cfg(test)]
pub fn assert_eq(self, other: StyledDom) {
let mut body = Dom::create_body();
let mut fixed = StyledDom::create(&mut body, Css::empty());
fixed.append_child(other);
if self.parsed_dom != fixed {
panic!(
"\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
----------\r\n{}\r\n",
self.parsed_dom.get_html_string("", "", true),
fixed.get_html_string("", "", true)
);
}
}
pub fn into_styled_dom(self) -> StyledDom {
self.into()
}
}
impl From<DomXml> for StyledDom {
fn from(val: DomXml) -> Self {
val.parsed_dom
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum XmlNodeChild {
Text(AzString),
Element(XmlNode),
}
impl_option!(
XmlNodeChild,
OptionXmlNodeChild,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl XmlNodeChild {
pub fn as_text(&self) -> Option<&str> {
match self {
XmlNodeChild::Text(s) => Some(s.as_str()),
XmlNodeChild::Element(_) => None,
}
}
pub fn as_element(&self) -> Option<&XmlNode> {
match self {
XmlNodeChild::Text(_) => None,
XmlNodeChild::Element(node) => Some(node),
}
}
pub fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
match self {
XmlNodeChild::Text(_) => None,
XmlNodeChild::Element(node) => Some(node),
}
}
}
impl_vec!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor, XmlNodeChildVecDestructorType, XmlNodeChildVecSlice, OptionXmlNodeChild);
impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct XmlNode {
pub node_type: XmlTagName,
pub attributes: XmlAttributeMap,
pub children: XmlNodeChildVec,
}
impl_option!(
XmlNode,
OptionXmlNode,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl XmlNode {
pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
XmlNode {
node_type: node_type.into(),
..Default::default()
}
}
pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
Self {
children: v.into(),
..self
}
}
pub fn get_text_content(&self) -> String {
self.children
.as_ref()
.iter()
.filter_map(|child| child.as_text())
.collect::<Vec<_>>()
.join("")
}
pub fn has_only_text_children(&self) -> bool {
self.children
.as_ref()
.iter()
.all(|child| matches!(child, XmlNodeChild::Text(_)))
}
}
impl_vec!(XmlNode, XmlNodeVec, XmlNodeVecDestructor, XmlNodeVecDestructorType, XmlNodeVecSlice, OptionXmlNode);
impl_vec_mut!(XmlNode, XmlNodeVec);
impl_vec_debug!(XmlNode, XmlNodeVec);
impl_vec_partialeq!(XmlNode, XmlNodeVec);
impl_vec_eq!(XmlNode, XmlNodeVec);
impl_vec_partialord!(XmlNode, XmlNodeVec);
impl_vec_ord!(XmlNode, XmlNodeVec);
impl_vec_hash!(XmlNode, XmlNodeVec);
impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum DomXmlParseError {
NoHtmlNode,
MultipleHtmlRootNodes,
NoBodyInHtml,
MultipleBodyNodes,
Xml(XmlError),
MalformedHierarchy(MalformedHierarchyError),
RenderDom(RenderDomError),
Component(ComponentParseError),
Css(CssParseErrorOwned),
}
impl From<XmlError> for DomXmlParseError {
fn from(e: XmlError) -> Self {
Self::Xml(e)
}
}
impl From<ComponentParseError> for DomXmlParseError {
fn from(e: ComponentParseError) -> Self {
Self::Component(e)
}
}
impl From<RenderDomError> for DomXmlParseError {
fn from(e: RenderDomError) -> Self {
Self::RenderDom(e)
}
}
impl From<CssParseErrorOwned> for DomXmlParseError {
fn from(e: CssParseErrorOwned) -> Self {
Self::Css(e)
}
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum CompileError {
Dom(RenderDomError),
Xml(DomXmlParseError),
Css(CssParseErrorOwned),
}
impl From<ComponentError> for CompileError {
fn from(e: ComponentError) -> Self {
CompileError::Dom(RenderDomError::Component(e))
}
}
impl From<CssParseErrorOwned> for CompileError {
fn from(e: CssParseErrorOwned) -> Self {
CompileError::Css(e)
}
}
impl<'a> fmt::Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::CompileError::*;
match self {
Dom(d) => write!(f, "{}", d),
Xml(s) => write!(f, "{}", s),
Css(s) => write!(f, "{}", s.to_shared()),
}
}
}
impl From<RenderDomError> for CompileError {
fn from(e: RenderDomError) -> Self {
CompileError::Dom(e)
}
}
impl From<DomXmlParseError> for CompileError {
fn from(e: DomXmlParseError) -> Self {
CompileError::Xml(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct UselessFunctionArgumentError {
pub component_name: AzString,
pub argument_name: AzString,
pub valid_args: StringVec,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum ComponentError {
UselessFunctionArgument(UselessFunctionArgumentError),
UnknownComponent(AzString),
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum RenderDomError {
Component(ComponentError),
CssError(CssParseErrorOwned),
}
impl From<ComponentError> for RenderDomError {
fn from(e: ComponentError) -> Self {
Self::Component(e)
}
}
impl From<CssParseErrorOwned> for RenderDomError {
fn from(e: CssParseErrorOwned) -> Self {
Self::CssError(e)
}
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct MissingTypeError {
pub arg_pos: usize,
pub arg_name: AzString,
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct WhiteSpaceInComponentNameError {
pub arg_pos: usize,
pub arg_name: AzString,
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct WhiteSpaceInComponentTypeError {
pub arg_pos: usize,
pub arg_name: AzString,
pub arg_type: AzString,
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum ComponentParseError {
NotAComponent,
UnnamedComponent,
MissingName(usize),
MissingType(MissingTypeError),
WhiteSpaceInComponentName(WhiteSpaceInComponentNameError),
WhiteSpaceInComponentType(WhiteSpaceInComponentTypeError),
CssError(CssParseErrorOwned),
}
impl<'a> fmt::Display for DomXmlParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::DomXmlParseError::*;
match self {
NoHtmlNode => write!(
f,
"No <html> node found as the root of the file - empty file?"
),
MultipleHtmlRootNodes => write!(
f,
"Multiple <html> nodes found as the root of the file - only one root node allowed"
),
NoBodyInHtml => write!(
f,
"No <body> node found as a direct child of an <html> node - malformed DOM \
hierarchy?"
),
MultipleBodyNodes => write!(
f,
"Multiple <body> nodes present, only one <body> node is allowed"
),
Xml(e) => write!(f, "Error parsing XML: {}", e),
MalformedHierarchy(e) => write!(
f,
"Invalid </{}> tag: expected </{}>",
e.got.as_str(),
e.expected.as_str()
),
RenderDom(e) => write!(f, "Error rendering DOM: {}", e),
Component(c) => write!(f, "Error parsing component in <head> node:\r\n{}", c),
Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
}
}
}
impl<'a> fmt::Display for ComponentParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ComponentParseError::*;
match self {
NotAComponent => write!(f, "Expected <component/> node, found no such node"),
UnnamedComponent => write!(
f,
"Found <component/> tag with out a \"name\" attribute, component must have a name"
),
MissingName(arg_pos) => write!(
f,
"Argument at position {} is either empty or has no name",
arg_pos
),
MissingType(e) => write!(
f,
"Argument \"{}\" at position {} doesn't have a `: type`",
e.arg_name, e.arg_pos
),
WhiteSpaceInComponentName(e) => {
write!(
f,
"Missing `:` between the name and the type in argument {} (around \"{}\")",
e.arg_pos, e.arg_name
)
}
WhiteSpaceInComponentType(e) => {
write!(
f,
"Missing `,` between two arguments (in argument {}, position {}, around \
\"{}\")",
e.arg_name, e.arg_pos, e.arg_type
)
}
CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
}
}
}
impl fmt::Display for ComponentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ComponentError::*;
match self {
UselessFunctionArgument(e) => {
write!(
f,
"Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
e.component_name, e.argument_name, e.valid_args
)
}
UnknownComponent(name) => write!(f, "Unknown component: \"{}\"", name),
}
}
}
impl<'a> fmt::Display for RenderDomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::RenderDomError::*;
match self {
Component(c) => write!(f, "{}", c),
CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
}
}
}
pub fn get_html_node<'a>(root_nodes: &'a [XmlNodeChild]) -> Result<&'a XmlNode, DomXmlParseError> {
let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
if let XmlNodeChild::Element(node) = child {
let node_type_normalized = normalize_casing(&node.node_type);
if &node_type_normalized == "html" {
Some(node)
} else {
None
}
} else {
None
}
});
let html_node = html_node_iterator
.next()
.ok_or(DomXmlParseError::NoHtmlNode)?;
if html_node_iterator.next().is_some() {
Err(DomXmlParseError::MultipleHtmlRootNodes)
} else {
Ok(html_node)
}
}
pub fn get_body_node<'a>(root_nodes: &'a [XmlNodeChild]) -> Result<&'a XmlNode, DomXmlParseError> {
let direct_body = root_nodes.iter().filter_map(|child| {
if let XmlNodeChild::Element(node) = child {
let node_type_normalized = normalize_casing(&node.node_type);
if &node_type_normalized == "body" {
Some(node)
} else {
None
}
} else {
None
}
}).next();
if let Some(body) = direct_body {
return Ok(body);
}
fn find_body_recursive<'a>(nodes: &'a [XmlNodeChild]) -> Option<&'a XmlNode> {
for child in nodes {
if let XmlNodeChild::Element(node) = child {
let node_type_normalized = normalize_casing(&node.node_type);
if &node_type_normalized == "body" {
return Some(node);
}
if let Some(found) = find_body_recursive(node.children.as_ref()) {
return Some(found);
}
}
}
None
}
find_body_recursive(root_nodes).ok_or(DomXmlParseError::NoBodyInHtml)
}
fn find_node_by_type<'a>(
root_nodes: &'a [XmlNodeChild],
node_type: &str,
) -> Option<&'a XmlNode> {
for child in root_nodes {
if let XmlNodeChild::Element(node) = child {
if normalize_casing(&node.node_type).as_str() == node_type {
return Some(node);
}
}
}
for child in root_nodes {
if let XmlNodeChild::Element(node) = child {
if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
return Some(found);
}
}
}
None
}
pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
node.attributes
.iter()
.find(|n| normalize_casing(&n.key.as_str()).as_str() == attribute)
.map(|s| &s.value)
}
pub fn normalize_casing(input: &str) -> String {
let mut words: Vec<String> = Vec::new();
let mut cur_str = Vec::new();
for ch in input.chars() {
if ch.is_uppercase() || ch == '_' || ch == '-' {
if !cur_str.is_empty() {
words.push(cur_str.iter().collect());
cur_str.clear();
}
if ch.is_uppercase() {
cur_str.extend(ch.to_lowercase());
}
} else {
cur_str.extend(ch.to_lowercase());
}
}
if !cur_str.is_empty() {
words.push(cur_str.iter().collect());
cur_str.clear();
}
words.join("_")
}
#[allow(trivial_casts)]
pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
let mut hierarchy = hierarchy.to_vec();
hierarchy.reverse();
let item = match hierarchy.pop() {
Some(s) => s,
None => return Some(root_node),
};
let child = root_node.children.as_mut().get_mut(item)?;
match child {
XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
XmlNodeChild::Text(_) => None, }
}
fn get_item_internal<'a>(
hierarchy: &mut Vec<usize>,
root_node: &'a mut XmlNode,
) -> Option<&'a mut XmlNode> {
if hierarchy.is_empty() {
return Some(root_node);
}
let cur_item = match hierarchy.pop() {
Some(s) => s,
None => return Some(root_node),
};
let child = root_node.children.as_mut().get_mut(cur_item)?;
match child {
XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
XmlNodeChild::Text(_) => None, }
}
pub fn str_to_dom<'a>(
root_nodes: &'a [XmlNodeChild],
component_map: &'a ComponentMap,
max_width: Option<f32>,
) -> Result<StyledDom, DomXmlParseError> {
str_to_dom_fast(root_nodes, component_map, max_width)
}
fn str_to_dom_fast<'a>(
root_nodes: &'a [XmlNodeChild],
component_map: &'a ComponentMap,
max_width: Option<f32>,
) -> Result<StyledDom, DomXmlParseError> {
let html_node = get_html_node(root_nodes)?;
let body_node = get_body_node(html_node.children.as_ref())?;
let mut global_style = None;
if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
let text = style_node.get_text_content();
if !text.is_empty() {
let parsed_css = Css::from_string(text.into());
global_style = Some(parsed_css);
}
}
}
render_dom_from_body_node_fast(&body_node, global_style, component_map, max_width)
.map_err(|e| e.into())
}
pub fn str_to_dom_unstyled<'a>(
root_nodes: &'a [XmlNodeChild],
component_map: &'a ComponentMap,
) -> Result<Dom, DomXmlParseError> {
let html_node = get_html_node(root_nodes)?;
let body_node = get_body_node(html_node.children.as_ref())?;
let mut global_style = None;
if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
let text = style_node.get_text_content();
if !text.is_empty() {
let parsed_css = Css::from_string(text.into());
global_style = Some(parsed_css);
}
}
}
let body_dom = xml_node_to_dom_fast(&body_node, component_map, false)
.map_err(|e| DomXmlParseError::from(e))?;
use crate::dom::NodeType;
let root_node_type = body_dom.root.node_type.clone();
let mut full_dom = match root_node_type {
NodeType::Html => body_dom,
NodeType::Body => Dom::create_html().with_child(body_dom),
_ => {
let body_wrapper = Dom::create_body().with_child(body_dom);
Dom::create_html().with_child(body_wrapper)
}
};
if let Some(css) = global_style {
let mut css_vec: Vec<Css> = Vec::new();
css_vec.push(css);
full_dom.css = css_vec.into();
}
Ok(full_dom)
}
pub fn str_to_rust_code<'a>(
root_nodes: &'a [XmlNodeChild],
imports: &str,
component_map: &'a ComponentMap,
) -> Result<String, CompileError> {
let html_node = get_html_node(&root_nodes)?;
let body_node = get_body_node(html_node.children.as_ref())?;
let mut global_style = Css::empty();
if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
let text = style_node.get_text_content();
if !text.is_empty() {
let parsed_css = azul_css::parser2::new_from_str(&text).0;
global_style = parsed_css;
}
}
}
global_style.sort_by_specificity();
let mut css_blocks = BTreeMap::new();
let mut extra_blocks = VecContents::default();
let app_source = compile_body_node_to_rust_code(
&body_node,
component_map,
&mut extra_blocks,
&mut css_blocks,
&global_style,
CssMatcher {
path: Vec::new(),
indices_in_parent: vec![0],
children_length: vec![body_node.children.as_ref().len()],
},
)?;
let app_source = app_source
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
let t = " ";
let css_blocks = css_blocks
.iter()
.map(|(k, v)| {
let v = v
.lines()
.map(|l| format!("{}{}{}", t, t, l))
.collect::<Vec<String>>()
.join("\r\n");
format!(
" const {}_PROPERTIES: &[NodeDataInlineCssProperty] = \
&[\r\n{}\r\n{}];\r\n{}const {}: NodeDataInlineCssPropertyVec = \
NodeDataInlineCssPropertyVec::from_const_slice({}_PROPERTIES);",
k, v, t, t, k, k
)
})
.collect::<Vec<_>>()
.join(&format!("{}\r\n\r\n", t));
let mut extra_block_string = extra_blocks.format(1);
let main_func = "
use azul::{
app::{App, AppConfig, LayoutSolver},
css::Css,
dom::Dom,
callbacks::{RefAny, LayoutCallbackInfo},
window::{WindowCreateOptions, WindowFrame},
};
struct Data { }
extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
let dom = crate::ui::render();
dom.with_component_css(Css::empty()) // styles are applied inline
}
fn main() {
let app = App::new(RefAny::new(Data { }), AppConfig::new(LayoutSolver::Default));
let mut window = WindowCreateOptions::new(render);
window.state.flags.frame = WindowFrame::Maximized;
app.run(window);
}";
let source_code = format!(
"#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
code\r\n{}\r\n{}\r\n\r\n{}{}",
imports,
compile_components(Vec::new()), format!(
"#[allow(unused_imports)]\r\npub mod ui {{
pub use crate::components::*;
use azul::css::*;
use azul::str::String as AzString;
use azul::vec::{{
DomVec, IdOrClassVec, NodeDataInlineCssPropertyVec,
StyleBackgroundSizeVec, StyleBackgroundRepeatVec,
StyleBackgroundContentVec, StyleTransformVec,
StyleFontFamilyVec, StyleBackgroundPositionVec,
NormalizedLinearColorStopVec, NormalizedRadialColorStopVec,
}};
use azul::dom::{{
Dom, IdOrClass, TabIndex,
IdOrClass::{{Id, Class}},
NodeDataInlineCssProperty,
}};\r\n\r\n{}\r\n\r\n{}
pub fn render() -> Dom {{\r\n{}\r\n }}\r\n}}",
extra_block_string, css_blocks, app_source
),
main_func,
);
Ok(source_code)
}
fn compile_components(
components: Vec<(
ComponentName,
CompiledComponent,
ComponentArguments,
BTreeMap<String, String>,
)>,
) -> String {
let cs = components
.iter()
.map(|(name, function_body, function_args, css_blocks)| {
let name = &normalize_casing(&name);
let f = compile_component(name, function_args, function_body)
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
format!(
"#[allow(unused_imports)]\r\npub mod {} {{\r\n use azul::dom::Dom;\r\n use \
azul::str::String as AzString;\r\n{}\r\n}}",
name, f
)
})
.collect::<Vec<String>>()
.join("\r\n\r\n");
let cs = cs
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
if cs.is_empty() {
cs
} else {
format!("pub mod components {{\r\n{}\r\n}}", cs)
}
}
fn format_component_args(component_args: &ComponentArgumentVec) -> String {
let mut args = component_args
.iter()
.map(|a| format!("{}: {}", a.name, a.arg_type))
.collect::<Vec<String>>();
args.sort_by(|a, b| b.cmp(&a));
args.join(", ")
}
pub fn compile_component(
component_name: &str,
component_args: &ComponentArguments,
component_function_body: &str,
) -> String {
let component_name = &normalize_casing(&component_name);
let function_args = format_component_args(&component_args.args);
let component_function_body = component_function_body
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
let should_inline = component_function_body.lines().count() == 1;
format!(
"{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
if should_inline { "#[inline]\r\n" } else { "" },
if component_args.accepts_text {
"text: AzString"
} else {
""
},
if function_args.is_empty() || !component_args.accepts_text {
""
} else {
", "
},
function_args,
component_function_body,
)
}
fn parse_svg_float(attr: Option<&AzString>) -> Option<f32> {
attr?.as_str().trim().parse::<f32>().ok()
}
fn parse_svg_points(pts: &str, close: bool) -> Option<crate::svg::SvgMultiPolygon> {
let nums: Vec<f32> = pts
.split(|c: char| c == ',' || c.is_ascii_whitespace())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse::<f32>().ok())
.collect();
if nums.len() < 4 || nums.len() % 2 != 0 {
return None;
}
let mut elements = Vec::new();
let points: Vec<azul_css::props::basic::SvgPoint> = nums
.chunks_exact(2)
.map(|c| azul_css::props::basic::SvgPoint { x: c[0], y: c[1] })
.collect();
for w in points.windows(2) {
elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(w[0], w[1])));
}
if close && points.len() >= 2 {
let first = points[0];
let last = *points.last().unwrap();
if (first.x - last.x).abs() > 0.001 || (first.y - last.y).abs() > 0.001 {
elements.push(crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(last, first)));
}
}
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg::SvgPath {
items: crate::svg::SvgPathElementVec::from_vec(elements),
}
]),
})
}
fn xml_node_to_dom_fast<'a>(
xml_node: &'a XmlNode,
component_map: &'a ComponentMap,
inside_svg: bool,
) -> Result<Dom, RenderDomError> {
use crate::dom::{Dom, NodeType, IdOrClass, TabIndex};
let component_name = normalize_casing(&xml_node.node_type);
let node_type = tag_to_node_type(&component_name);
let mut dom = Dom::create_node(node_type);
let mut ids_and_classes = Vec::new();
if let Some(id_str) = xml_node.attributes.get_key("id") {
for id in id_str.split_whitespace() {
ids_and_classes.push(IdOrClass::Id(id.into()));
}
}
if let Some(class_str) = xml_node.attributes.get_key("class") {
for class in class_str.split_whitespace() {
ids_and_classes.push(IdOrClass::Class(class.into()));
}
}
if !ids_and_classes.is_empty() {
dom.root.set_ids_and_classes(ids_and_classes.into());
}
if let Some(focusable) = xml_node.attributes.get_key("focusable").and_then(|f| parse_bool(f.as_str())) {
match focusable {
true => dom.root.set_tab_index(TabIndex::Auto),
false => dom.root.set_tab_index(TabIndex::NoKeyboardFocus),
}
}
if let Some(tab_index) = xml_node.attributes.get_key("tabindex").and_then(|val| val.parse::<isize>().ok()) {
match tab_index {
0 => dom.root.set_tab_index(TabIndex::Auto),
i if i > 0 => dom.root.set_tab_index(TabIndex::OverrideInParent(i as u32)),
_ => dom.root.set_tab_index(TabIndex::NoKeyboardFocus),
}
}
if let Some(style) = xml_node.attributes.get_key("style") {
let css_key_map = azul_css::props::property::get_css_key_map();
let mut attributes = Vec::new();
for s in style.as_str().split(";") {
let mut s = s.split(":");
let key = match s.next() { Some(s) => s, None => continue };
let value = match s.next() { Some(s) => s, None => continue };
let _ = azul_css::parser2::parse_css_declaration(
key.trim(), value.trim(),
azul_css::parser2::ErrorLocationRange::default(),
&css_key_map, &mut Vec::new(), &mut attributes,
);
}
let props = attributes.into_iter().filter_map(|s| {
use azul_css::dynamic_selector::CssPropertyWithConditions;
match s {
CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
_ => None,
}
}).collect::<Vec<_>>();
if !props.is_empty() {
dom.root.set_css_props(props.into());
}
}
let tag = component_name.as_str();
let child_inside_svg = inside_svg || tag == "svg";
let is_svg_shape = inside_svg && matches!(tag, "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline");
if is_svg_shape {
let clip = match tag {
"path" => {
xml_node.attributes.get_key("d").and_then(|d| {
crate::svg_path_parser::parse_svg_path_d(d.as_str()).ok()
})
}
"circle" => {
let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
if r > 0.0 {
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg_path_parser::svg_circle_to_paths(cx, cy, r)
]),
})
} else {
None
}
}
"rect" => {
let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
if w > 0.0 && h > 0.0 {
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg_path_parser::svg_rect_to_path(x, y, w, h, rx, ry)
]),
})
} else {
None
}
}
"ellipse" => {
let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
if rx > 0.0 && ry > 0.0 {
use azul_css::props::basic::{SvgPoint, SvgCubicCurve};
const KAPPA: f32 = 0.5522847498;
let kx = rx * KAPPA;
let ky = ry * KAPPA;
let elements = vec![
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx, y: cy - ry },
ctrl_1: SvgPoint { x: cx + kx, y: cy - ry },
ctrl_2: SvgPoint { x: cx + rx, y: cy - ky },
end: SvgPoint { x: cx + rx, y: cy },
}),
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx + rx, y: cy },
ctrl_1: SvgPoint { x: cx + rx, y: cy + ky },
ctrl_2: SvgPoint { x: cx + kx, y: cy + ry },
end: SvgPoint { x: cx, y: cy + ry },
}),
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx, y: cy + ry },
ctrl_1: SvgPoint { x: cx - kx, y: cy + ry },
ctrl_2: SvgPoint { x: cx - rx, y: cy + ky },
end: SvgPoint { x: cx - rx, y: cy },
}),
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx - rx, y: cy },
ctrl_1: SvgPoint { x: cx - rx, y: cy - ky },
ctrl_2: SvgPoint { x: cx - kx, y: cy - ry },
end: SvgPoint { x: cx, y: cy - ry },
}),
];
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg::SvgPath { items: crate::svg::SvgPathElementVec::from_vec(elements) }
]),
})
} else {
None
}
}
"line" => {
let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg::SvgPath {
items: crate::svg::SvgPathElementVec::from_vec(vec![
crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
azul_css::props::basic::SvgPoint { x: x1, y: y1 },
azul_css::props::basic::SvgPoint { x: x2, y: y2 },
))
]),
}
]),
})
}
"polygon" | "polyline" => {
xml_node.attributes.get_key("points").and_then(|pts| {
parse_svg_points(pts.as_str(), tag == "polygon")
})
}
_ => None,
};
if let Some(mp) = clip {
dom.root.set_svg_data(crate::dom::SvgNodeData::Path(mp));
}
}
let mut children = Vec::new();
for child in xml_node.children.as_ref().iter() {
match child {
XmlNodeChild::Element(child_node) => {
let child_dom = xml_node_to_dom_fast(child_node, component_map, child_inside_svg)?;
children.push(child_dom);
}
XmlNodeChild::Text(text) => {
let text_dom = Dom::create_text(AzString::from(text.as_str()));
children.push(text_dom);
}
}
}
if !children.is_empty() {
dom = dom.with_children(children.into());
}
Ok(dom)
}
pub struct CompactDomBuilder {
hierarchy: Vec<crate::styled_dom::NodeHierarchyItem>,
node_data: Vec<crate::dom::NodeData>,
css: Vec<crate::dom::CssWithNodeId>,
stack: Vec<(usize, Option<usize>)>,
}
impl CompactDomBuilder {
pub fn new() -> Self {
Self {
hierarchy: Vec::new(),
node_data: Vec::new(),
css: Vec::new(),
stack: Vec::new(),
}
}
pub fn with_capacity(cap: usize) -> Self {
Self {
hierarchy: Vec::with_capacity(cap),
node_data: Vec::with_capacity(cap),
css: Vec::new(),
stack: Vec::new(),
}
}
pub fn open_node(&mut self, node_data: crate::dom::NodeData) {
use crate::id::NodeId;
use crate::styled_dom::NodeHierarchyItem;
let idx = self.hierarchy.len();
let parent_raw = if let Some(&(parent_idx, _)) = self.stack.last() {
NodeId::into_raw(&Some(NodeId::new(parent_idx)))
} else {
0 };
let prev_sibling_raw = if let Some(&(_, prev_child)) = self.stack.last() {
prev_child.map(|pi| NodeId::into_raw(&Some(NodeId::new(pi)))).unwrap_or(0)
} else {
0
};
if let Some(&(_, Some(prev_idx))) = self.stack.last() {
self.hierarchy[prev_idx].next_sibling = NodeId::into_raw(&Some(NodeId::new(idx)));
}
if let Some(parent) = self.stack.last_mut() {
parent.1 = Some(idx);
}
self.hierarchy.push(NodeHierarchyItem {
parent: parent_raw,
previous_sibling: prev_sibling_raw,
next_sibling: 0, last_child: 0, });
self.node_data.push(node_data);
self.stack.push((idx, None));
}
pub fn close_node(&mut self) {
use crate::id::NodeId;
if let Some((idx, last_child_idx)) = self.stack.pop() {
self.hierarchy[idx].last_child = last_child_idx
.map(|lc| NodeId::into_raw(&Some(NodeId::new(lc))))
.unwrap_or(0);
}
}
pub fn add_leaf(&mut self, node_data: crate::dom::NodeData) {
self.open_node(node_data);
self.close_node();
}
pub fn add_css(&mut self, node_id: usize, css: azul_css::css::Css) {
self.css.push(crate::dom::CssWithNodeId { node_id, css });
}
pub fn finish(self) -> crate::dom::FastDom {
crate::dom::FastDom {
node_hierarchy: self.hierarchy.into(),
node_data: self.node_data.into(),
css: self.css.into(),
}
}
}
fn xml_node_to_fast_dom<'a>(
xml_node: &'a XmlNode,
component_map: &'a ComponentMap,
inside_svg: bool,
builder: &mut CompactDomBuilder,
) -> Result<(), RenderDomError> {
use crate::dom::{Dom, NodeType, NodeData, IdOrClass, TabIndex};
let component_name = normalize_casing(&xml_node.node_type);
let node_type = tag_to_node_type(&component_name);
let mut node_data = NodeData::create_node(node_type);
let mut ids_and_classes = Vec::new();
if let Some(id_str) = xml_node.attributes.get_key("id") {
for id in id_str.split_whitespace() {
ids_and_classes.push(IdOrClass::Id(id.into()));
}
}
if let Some(class_str) = xml_node.attributes.get_key("class") {
for class in class_str.split_whitespace() {
ids_and_classes.push(IdOrClass::Class(class.into()));
}
}
if !ids_and_classes.is_empty() {
node_data.set_ids_and_classes(ids_and_classes.into());
}
if let Some(focusable) = xml_node.attributes.get_key("focusable").and_then(|f| parse_bool(f.as_str())) {
match focusable {
true => node_data.set_tab_index(TabIndex::Auto),
false => node_data.set_tab_index(TabIndex::NoKeyboardFocus),
}
}
if let Some(tab_index) = xml_node.attributes.get_key("tabindex").and_then(|val| val.parse::<isize>().ok()) {
match tab_index {
0 => node_data.set_tab_index(TabIndex::Auto),
i if i > 0 => node_data.set_tab_index(TabIndex::OverrideInParent(i as u32)),
_ => node_data.set_tab_index(TabIndex::NoKeyboardFocus),
}
}
if let Some(style) = xml_node.attributes.get_key("style") {
let css_key_map = azul_css::props::property::get_css_key_map();
let mut attributes = Vec::new();
for s in style.as_str().split(";") {
let mut s = s.split(":");
let key = match s.next() { Some(s) => s, None => continue };
let value = match s.next() { Some(s) => s, None => continue };
let _ = azul_css::parser2::parse_css_declaration(
key.trim(), value.trim(),
azul_css::parser2::ErrorLocationRange::default(),
&css_key_map, &mut Vec::new(), &mut attributes,
);
}
let props = attributes.into_iter().filter_map(|s| {
use azul_css::dynamic_selector::CssPropertyWithConditions;
match s {
CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
_ => None,
}
}).collect::<Vec<_>>();
if !props.is_empty() {
node_data.set_css_props(props.into());
}
}
let tag = component_name.as_str();
let child_inside_svg = inside_svg || tag == "svg";
let is_svg_shape = inside_svg && matches!(tag, "path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline");
if is_svg_shape {
let clip = match tag {
"path" => {
xml_node.attributes.get_key("d").and_then(|d| {
crate::svg_path_parser::parse_svg_path_d(d.as_str()).ok()
})
}
"circle" => {
let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
let r = parse_svg_float(xml_node.attributes.get_key("r")).unwrap_or(0.0);
if r > 0.0 {
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg_path_parser::svg_circle_to_paths(cx, cy, r)
]),
})
} else {
None
}
}
"rect" => {
let x = parse_svg_float(xml_node.attributes.get_key("x")).unwrap_or(0.0);
let y = parse_svg_float(xml_node.attributes.get_key("y")).unwrap_or(0.0);
let w = parse_svg_float(xml_node.attributes.get_key("width")).unwrap_or(0.0);
let h = parse_svg_float(xml_node.attributes.get_key("height")).unwrap_or(0.0);
let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(rx);
if w > 0.0 && h > 0.0 {
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg_path_parser::svg_rect_to_path(x, y, w, h, rx, ry)
]),
})
} else {
None
}
}
"ellipse" => {
let cx = parse_svg_float(xml_node.attributes.get_key("cx")).unwrap_or(0.0);
let cy = parse_svg_float(xml_node.attributes.get_key("cy")).unwrap_or(0.0);
let rx = parse_svg_float(xml_node.attributes.get_key("rx")).unwrap_or(0.0);
let ry = parse_svg_float(xml_node.attributes.get_key("ry")).unwrap_or(0.0);
if rx > 0.0 && ry > 0.0 {
use azul_css::props::basic::{SvgPoint, SvgCubicCurve};
const KAPPA: f32 = 0.5522847498;
let kx = rx * KAPPA;
let ky = ry * KAPPA;
let elements = vec![
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx, y: cy - ry },
ctrl_1: SvgPoint { x: cx + kx, y: cy - ry },
ctrl_2: SvgPoint { x: cx + rx, y: cy - ky },
end: SvgPoint { x: cx + rx, y: cy },
}),
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx + rx, y: cy },
ctrl_1: SvgPoint { x: cx + rx, y: cy + ky },
ctrl_2: SvgPoint { x: cx + kx, y: cy + ry },
end: SvgPoint { x: cx, y: cy + ry },
}),
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx, y: cy + ry },
ctrl_1: SvgPoint { x: cx - kx, y: cy + ry },
ctrl_2: SvgPoint { x: cx - rx, y: cy + ky },
end: SvgPoint { x: cx - rx, y: cy },
}),
crate::svg::SvgPathElement::CubicCurve(SvgCubicCurve {
start: SvgPoint { x: cx - rx, y: cy },
ctrl_1: SvgPoint { x: cx - rx, y: cy - ky },
ctrl_2: SvgPoint { x: cx - kx, y: cy - ry },
end: SvgPoint { x: cx, y: cy - ry },
}),
];
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg::SvgPath { items: crate::svg::SvgPathElementVec::from_vec(elements) }
]),
})
} else {
None
}
}
"line" => {
let x1 = parse_svg_float(xml_node.attributes.get_key("x1")).unwrap_or(0.0);
let y1 = parse_svg_float(xml_node.attributes.get_key("y1")).unwrap_or(0.0);
let x2 = parse_svg_float(xml_node.attributes.get_key("x2")).unwrap_or(0.0);
let y2 = parse_svg_float(xml_node.attributes.get_key("y2")).unwrap_or(0.0);
Some(crate::svg::SvgMultiPolygon {
rings: crate::svg::SvgPathVec::from_vec(vec![
crate::svg::SvgPath {
items: crate::svg::SvgPathElementVec::from_vec(vec![
crate::svg::SvgPathElement::Line(crate::svg::SvgLine::new(
azul_css::props::basic::SvgPoint { x: x1, y: y1 },
azul_css::props::basic::SvgPoint { x: x2, y: y2 },
))
]),
}
]),
})
}
"polygon" | "polyline" => {
xml_node.attributes.get_key("points").and_then(|pts| {
parse_svg_points(pts.as_str(), tag == "polygon")
})
}
_ => None,
};
if let Some(mp) = clip {
node_data.set_svg_data(crate::dom::SvgNodeData::Path(mp));
}
}
builder.open_node(node_data);
for child in xml_node.children.as_ref().iter() {
match child {
XmlNodeChild::Element(child_node) => {
xml_node_to_fast_dom(child_node, component_map, child_inside_svg, builder)?;
}
XmlNodeChild::Text(text) => {
builder.add_leaf(NodeData::create_text(AzString::from(text.as_str())));
}
}
}
builder.close_node();
Ok(())
}
fn render_dom_from_body_node_fast<'a>(
body_node: &'a XmlNode,
mut global_css: Option<Css>,
component_map: &'a ComponentMap,
max_width: Option<f32>,
) -> Result<StyledDom, RenderDomError> {
use crate::dom::{NodeData, NodeType};
let mut builder = CompactDomBuilder::new();
builder.open_node(NodeData::create_node(NodeType::Html));
xml_node_to_fast_dom(body_node, component_map, false, &mut builder)?;
builder.close_node();
let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
if let Some(max_width) = max_width {
let max_width_css = Css::from_string(
format!("html {{ max-width: {max_width}px; }}").into(),
);
combined_rules.extend(max_width_css.rules.into_library_owned_vec());
}
if let Some(css) = global_css.take() {
combined_rules.extend(css.rules.into_library_owned_vec());
}
let combined_css = Css::new(combined_rules);
let mut fast_dom = builder.finish();
fast_dom.css = vec![crate::dom::CssWithNodeId {
node_id: 0, css: combined_css,
}].into();
let styled = StyledDom::create_from_fast_dom(fast_dom);
Ok(styled)
}
fn set_stringified_attributes(
dom_string: &mut String,
xml_attributes: &XmlAttributeMap,
filtered_xml_attributes: &ComponentArgumentVec,
tabs: usize,
) {
let t0 = String::from(" ").repeat(tabs);
let t = String::from(" ").repeat(tabs + 1);
let mut ids_and_classes = String::new();
for id in xml_attributes
.get_key("id")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default()
{
ids_and_classes.push_str(&format!(
"{} Id(AzString::from_const_str(\"{}\")),\r\n",
t0,
format_args_dynamic(id, &filtered_xml_attributes)
));
}
for class in xml_attributes
.get_key("class")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default()
{
ids_and_classes.push_str(&format!(
"{} Class(AzString::from_const_str(\"{}\")),\r\n",
t0,
format_args_dynamic(class, &filtered_xml_attributes)
));
}
if !ids_and_classes.is_empty() {
use azul_css::format_rust_code::GetHash;
let id = ids_and_classes.get_hash();
dom_string.push_str(&format!(
"\r\n{t0}.with_ids_and_classes({{\r\n{t}const IDS_AND_CLASSES_{id}: &[IdOrClass] = \
&[\r\n{t}{ids_and_classes}\r\n{t}];\r\\
n{t}IdOrClassVec::from_const_slice(IDS_AND_CLASSES_{id})\r\n{t0}}})",
t0 = t0,
t = t,
ids_and_classes = ids_and_classes,
id = id
));
}
if let Some(focusable) = xml_attributes
.get_key("focusable")
.map(|f| format_args_dynamic(f, &filtered_xml_attributes))
.and_then(|f| parse_bool(&f))
{
match focusable {
true => dom_string.push_str(&format!("\r\n{}.with_tab_index(TabIndex::Auto)", t)),
false => dom_string.push_str(&format!(
"\r\n{}.with_tab_index(TabIndex::NoKeyboardFocus)",
t
)),
}
}
if let Some(tab_index) = xml_attributes
.get_key("tabindex")
.map(|val| format_args_dynamic(val, &filtered_xml_attributes))
.and_then(|val| val.parse::<isize>().ok())
{
match tab_index {
0 => dom_string.push_str(&format!("\r\n{}.with_tab_index(TabIndex::Auto)", t)),
i if i > 0 => dom_string.push_str(&format!(
"\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
t, i as usize
)),
_ => dom_string.push_str(&format!(
"\r\n{}.with_tab_index(TabIndex::NoKeyboardFocus)",
t
)),
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum DynamicItem {
Var {
name: String,
format_spec: Option<String>,
},
Str(String),
}
pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
use self::DynamicItem::*;
let input: Vec<char> = input.chars().collect();
let input_chars_len = input.len();
let mut items = Vec::new();
let mut current_idx = 0;
let mut last_idx = 0;
while current_idx < input_chars_len {
let c = input[current_idx];
match c {
'{' if input.get(current_idx + 1).copied() != Some('{') => {
let mut start_offset = 1;
let mut has_found_variable = false;
while let Some(c) = input.get(current_idx + start_offset) {
if c.is_whitespace() {
break;
}
if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
{
start_offset += 1;
has_found_variable = true;
break;
}
start_offset += 1;
}
if has_found_variable {
if last_idx != current_idx {
items.push(Str(input[last_idx..current_idx].iter().collect()));
}
let var_content: String = input
[(current_idx + 1)..(current_idx + start_offset - 1)]
.iter()
.collect();
let (var_name, format_spec) = if let Some(colon_pos) = var_content.find(':') {
let name = var_content[..colon_pos].to_string();
let spec = var_content[(colon_pos + 1)..].to_string();
(name, Some(spec))
} else {
(var_content, None)
};
items.push(Var { name: var_name, format_spec });
current_idx = current_idx + start_offset;
last_idx = current_idx;
} else {
current_idx += start_offset;
}
}
_ => {
current_idx += 1;
}
}
}
if current_idx != last_idx {
items.push(Str(input[last_idx..].iter().collect()));
}
for item in &mut items {
if let Str(s) = item {
*s = s.replace("{{", "{").replace("}}", "}");
}
}
items
}
fn combine_and_replace_dynamic_items(
input: &[DynamicItem],
variables: &ComponentArgumentVec,
) -> String {
let mut s = String::new();
for item in input {
match item {
DynamicItem::Var { name, format_spec } => {
let variable_name = normalize_casing(name.trim());
match variables
.iter()
.find(|s| s.name.as_str() == variable_name)
.map(|q| &q.arg_type)
{
Some(resolved_var) => {
s.push_str(&resolved_var);
}
None => {
s.push('{');
s.push_str(name);
if let Some(spec) = format_spec {
s.push(':');
s.push_str(spec);
}
s.push('}');
}
}
}
DynamicItem::Str(dynamic_str) => {
s.push_str(&dynamic_str);
}
}
}
s
}
pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentVec) -> String {
let dynamic_str_items = split_dynamic_string(input);
combine_and_replace_dynamic_items(&dynamic_str_items, variables)
}
pub fn prepare_string(input: &str) -> String {
const SPACE: &str = " ";
const RETURN: &str = "\n";
let input = input.trim();
if input.is_empty() {
return String::new();
}
let input = input.replace("<", "<");
let input = input.replace(">", ">");
let input_len = input.len();
let mut final_lines: Vec<String> = Vec::new();
let mut last_line_was_empty = false;
for line in input.lines() {
let line = line.trim();
let line = line.replace(" ", " ");
let current_line_is_empty = line.is_empty();
if !current_line_is_empty {
if last_line_was_empty {
final_lines.push(format!("{}{}", RETURN, line));
} else {
final_lines.push(line.to_string());
}
}
last_line_was_empty = current_line_is_empty;
}
let line_len = final_lines.len();
let mut target = String::with_capacity(input_len);
for (line_idx, line) in final_lines.iter().enumerate() {
if !(line.starts_with(RETURN) || line_idx == 0 || line_idx == line_len.saturating_sub(1)) {
target.push_str(SPACE);
}
target.push_str(line);
}
target
}
pub fn parse_bool(input: &str) -> Option<bool> {
match input {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
#[derive(Clone)]
pub struct CssMatcher {
path: Vec<CssPathSelector>,
indices_in_parent: Vec<usize>,
children_length: Vec<usize>,
}
impl CssMatcher {
fn get_hash(&self) -> u64 {
use core::hash::Hash;
use std::hash::Hasher;
let mut hasher = std::hash::DefaultHasher::new();
for p in self.path.iter() {
p.hash(&mut hasher);
}
hasher.finish()
}
}
impl CssMatcher {
fn matches(&self, path: &CssPath) -> bool {
use azul_css::css::CssPathSelector::*;
use crate::style::{CssGroupIterator, CssGroupSplitReason};
if self.path.is_empty() {
return false;
}
if path.selectors.as_ref().is_empty() {
return false;
}
let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
path_groups.reverse();
if path_groups.is_empty() {
return false;
}
let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
self_groups.reverse();
if self_groups.is_empty() {
return false;
}
if self.indices_in_parent.len() != self_groups.len() {
return false;
}
if self.children_length.len() != self_groups.len() {
return false;
}
let mut cur_selfgroup_scan = 0;
let mut cur_pathgroup_scan = 0;
let mut valid = false;
let mut path_group = path_groups[cur_pathgroup_scan].clone();
while cur_selfgroup_scan < self_groups.len() {
let mut advance = None;
for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
let gm = group_matches(
&path_group.0,
&self_groups[cur_selfgroup_scan + id].0,
self.indices_in_parent[cur_selfgroup_scan + id],
self.children_length[cur_selfgroup_scan + id],
);
if gm {
advance = Some(id);
break;
}
}
match advance {
Some(n) => {
if cur_pathgroup_scan == path_groups.len() - 1 {
return cur_selfgroup_scan + n == self_groups.len() - 1;
} else {
cur_pathgroup_scan += 1;
cur_selfgroup_scan += n;
path_group = path_groups[cur_pathgroup_scan].clone();
}
}
None => return false, }
}
return cur_pathgroup_scan == path_groups.len() - 1;
}
}
fn group_matches(
a: &[&CssPathSelector],
b: &[&CssPathSelector],
idx_in_parent: usize,
parent_children: usize,
) -> bool {
use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::*};
for selector in a {
match selector {
Global => {}
PseudoSelector(CssPathPseudoSelector::Hover) => {}
PseudoSelector(CssPathPseudoSelector::Active) => {}
PseudoSelector(CssPathPseudoSelector::Focus) => {}
Type(tag) => {
if !b.iter().any(|t| **t == Type(tag.clone())) {
return false;
}
}
Class(class) => {
if !b.iter().any(|t| **t == Class(class.clone())) {
return false;
}
}
Id(id) => {
if !b.iter().any(|t| **t == Id(id.clone())) {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::First) => {
if idx_in_parent != 0 {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::Last) => {
if idx_in_parent != parent_children.saturating_sub(1) {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
if idx_in_parent != *i as usize {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
if idx_in_parent % 2 != 0 {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
if idx_in_parent % 2 == 0 {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
if idx_in_parent.saturating_sub(p.offset as usize) % p.pattern_repeat as usize != 0
{
return false;
}
}
_ => return false, }
}
true
}
struct CssBlock {
ending: Option<CssPathPseudoSelector>,
block: CssRuleBlock,
}
pub fn compile_body_node_to_rust_code<'a>(
body_node: &'a XmlNode,
component_map: &'a ComponentMap,
extra_blocks: &mut VecContents,
css_blocks: &mut BTreeMap<String, String>,
css: &Css,
mut matcher: CssMatcher,
) -> Result<String, CompileError> {
use azul_css::css::CssDeclaration;
let t = "";
let t2 = " ";
let mut dom_string = String::from("Dom::create_body()");
let node_type = CssPathSelector::Type(NodeTypeTag::Body);
matcher.path.push(node_type);
let ids = body_node
.attributes
.get_key("id")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
ids.into_iter()
.map(|id| CssPathSelector::Id(id.to_string().into())),
);
let classes = body_node
.attributes
.get_key("class")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
classes
.into_iter()
.map(|class| CssPathSelector::Class(class.to_string().into())),
);
let matcher_hash = matcher.get_hash();
let css_blocks_for_this_node = get_css_blocks(css, &matcher);
if !css_blocks_for_this_node.is_empty() {
use azul_css::props::property::format_static_css_prop;
let css_strings = css_blocks_for_this_node
.iter()
.rev()
.map(|css_block| {
let wrapper = match css_block.ending {
Some(CssPathPseudoSelector::Hover) => "Hover",
Some(CssPathPseudoSelector::Active) => "Active",
Some(CssPathPseudoSelector::Focus) => "Focus",
_ => "Normal",
};
for declaration in css_block.block.declarations.as_ref().iter() {
let prop = match declaration {
CssDeclaration::Static(s) => s,
CssDeclaration::Dynamic(d) => &d.default_value,
};
extra_blocks.insert_from_css_property(prop);
}
let formatted = css_block
.block
.declarations
.as_ref()
.iter()
.rev()
.map(|s| match &s {
CssDeclaration::Static(s) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(s, 1)
),
CssDeclaration::Dynamic(d) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(&d.default_value, 1)
),
})
.collect::<Vec<String>>();
format!("// {}\r\n{}", css_block.block.path, formatted.join(",\r\n"))
})
.collect::<Vec<_>>()
.join(",\r\n");
css_blocks.insert(format!("CSS_MATCH_{:09}", matcher_hash), css_strings);
dom_string.push_str(&format!(
"\r\n{}.with_inline_css_props(CSS_MATCH_{:09})",
t2, matcher_hash
));
}
if !body_node.children.as_ref().is_empty() {
use azul_css::format_rust_code::GetHash;
let children_hash = body_node.children.as_ref().get_hash();
dom_string.push_str(&format!("\r\n.with_children(DomVec::from_vec(vec![\r\n"));
for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
match child {
XmlNodeChild::Element(child_node) => {
let mut matcher = matcher.clone();
matcher.path.push(CssPathSelector::Children);
matcher.indices_in_parent.push(child_idx);
matcher.children_length.push(body_node.children.len());
dom_string.push_str(&format!(
"{}{},\r\n",
t,
compile_node_to_rust_code_inner(
child_node,
component_map,
1,
extra_blocks,
css_blocks,
css,
matcher,
)?
));
}
XmlNodeChild::Text(text) => {
let text = text.trim();
if !text.is_empty() {
let escaped = text.replace("\\", "\\\\").replace("\"", "\\\"");
dom_string
.push_str(&format!("{}Dom::create_text(\"{}\".into()),\r\n", t, escaped));
}
}
}
}
dom_string.push_str(&format!("\r\n{}]))", t));
}
let dom_string = dom_string.trim();
Ok(dom_string.to_string())
}
fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
let mut blocks = Vec::new();
for css_block in css.rules.as_ref() {
if matcher.matches(&css_block.path) {
let mut ending = None;
if let Some(CssPathSelector::PseudoSelector(p)) =
css_block.path.selectors.as_ref().last()
{
ending = Some(p.clone());
}
blocks.push(CssBlock {
ending,
block: css_block.clone(),
});
}
}
blocks
}
fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
use self::DynamicItem::*;
if input.is_empty() {
String::from("AzString::from_const_str(\"\")")
} else if input.len() == 1 {
match &input[0] {
Var { name, format_spec } => {
let var_name = normalize_casing(name.trim());
if let Some(spec) = format_spec {
format!("format!(\"{{:{}}}\", {}).into()", spec, var_name)
} else {
var_name
}
}
Str(s) => format!("AzString::from_const_str(\"{}\")", s),
}
} else {
let mut formatted_str = String::from("format!(\"");
let mut variables = Vec::new();
for item in input {
match item {
Var { name, format_spec } => {
let variable_name = normalize_casing(name.trim());
if let Some(spec) = format_spec {
formatted_str.push_str(&format!("{{{}:{}}}", variable_name, spec));
} else {
formatted_str.push_str(&format!("{{{}}}", variable_name));
}
variables.push(variable_name.clone());
}
Str(s) => {
let s = s.replace("\"", "\\\"");
formatted_str.push_str(&s);
}
}
}
formatted_str.push('\"');
if !variables.is_empty() {
formatted_str.push_str(", ");
}
formatted_str.push_str(&variables.join(", "));
formatted_str.push_str(").into()");
formatted_str
}
}
fn format_args_for_rust_code(input: &str) -> String {
let dynamic_str_items = split_dynamic_string(input);
compile_and_format_dynamic_items(&dynamic_str_items)
}
fn compile_node_to_rust_code_inner<'a>(
node: &XmlNode,
component_map: &'a ComponentMap,
tabs: usize,
extra_blocks: &mut VecContents,
css_blocks: &mut BTreeMap<String, String>,
css: &Css,
mut matcher: CssMatcher,
) -> Result<String, CompileError> {
use azul_css::css::CssDeclaration;
let t = String::from(" ").repeat(tabs - 1);
let t2 = String::from(" ").repeat(tabs);
let component_name = normalize_casing(&node.node_type);
let def = component_map.get_unqualified(&component_name);
let node_type_tag = tag_to_node_type_tag(&component_name);
let node_type = CssPathSelector::Type(node_type_tag);
let text_content = node.get_text_content();
let text_content_trimmed = text_content.trim();
let mut dom_string = if let Some(d) = def {
let data_model = xml_attrs_to_data_model(
&d.data_model,
&node.attributes,
if text_content_trimmed.is_empty() { None } else { Some(text_content_trimmed) },
);
match (d.compile_fn)(d, &CompileTarget::Rust, &data_model, tabs) {
ResultStringCompileError::Ok(s) => format!("{}{}", t2, s.as_str()),
ResultStringCompileError::Err(_) => {
let node_type = tag_to_node_type(&component_name);
format!("{}Dom::create_node(NodeType::{:?})", t2, node_type)
}
}
} else {
format!("{}Dom::create_node(NodeType::Div) /* {} */", t2, component_name)
};
matcher.path.push(node_type);
let ids = node
.attributes
.get_key("id")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
ids.into_iter()
.map(|id| CssPathSelector::Id(id.to_string().into())),
);
let classes = node
.attributes
.get_key("class")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
classes
.into_iter()
.map(|class| CssPathSelector::Class(class.to_string().into())),
);
let matcher_hash = matcher.get_hash();
let css_blocks_for_this_node = get_css_blocks(css, &matcher);
if !css_blocks_for_this_node.is_empty() {
use azul_css::props::property::format_static_css_prop;
let css_strings = css_blocks_for_this_node
.iter()
.rev()
.map(|css_block| {
let wrapper = match css_block.ending {
Some(CssPathPseudoSelector::Hover) => "Hover",
Some(CssPathPseudoSelector::Active) => "Active",
Some(CssPathPseudoSelector::Focus) => "Focus",
_ => "Normal",
};
for declaration in css_block.block.declarations.as_ref().iter() {
let prop = match declaration {
CssDeclaration::Static(s) => s,
CssDeclaration::Dynamic(d) => &d.default_value,
};
extra_blocks.insert_from_css_property(prop);
}
let formatted = css_block
.block
.declarations
.as_ref()
.iter()
.rev()
.map(|s| match &s {
CssDeclaration::Static(s) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(s, 1)
),
CssDeclaration::Dynamic(d) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(&d.default_value, 1)
),
})
.collect::<Vec<String>>();
format!("// {}\r\n{}", css_block.block.path, formatted.join(",\r\n"))
})
.collect::<Vec<_>>()
.join(",\r\n");
css_blocks.insert(format!("CSS_MATCH_{:09}", matcher_hash), css_strings);
dom_string.push_str(&format!(
"\r\n{}.with_inline_css_props(CSS_MATCH_{:09})",
t2, matcher_hash
));
}
set_stringified_attributes(
&mut dom_string,
&node.attributes,
&ComponentArgumentVec::new(),
tabs,
);
let mut children_string = node
.children
.as_ref()
.iter()
.enumerate()
.filter_map(|(child_idx, c)| match c {
XmlNodeChild::Element(child_node) => {
let mut matcher = matcher.clone();
matcher.path.push(CssPathSelector::Children);
matcher.indices_in_parent.push(child_idx);
matcher.children_length.push(node.children.len());
Some(compile_node_to_rust_code_inner(
child_node,
component_map,
tabs + 1,
extra_blocks,
css_blocks,
css,
matcher,
))
}
XmlNodeChild::Text(text) => {
let text = text.trim();
if text.is_empty() {
None
} else {
let t2 = String::from(" ").repeat(tabs);
let escaped = text.replace("\\", "\\\\").replace("\"", "\\\"");
Some(Ok(format!("{}Dom::create_text(\"{}\".into())", t2, escaped)))
}
}
})
.collect::<Result<Vec<_>, _>>()?
.join(&format!(",\r\n"));
if !children_string.is_empty() {
dom_string.push_str(&format!(
"\r\n{}.with_children(DomVec::from_vec(vec![\r\n{}\r\n{}]))",
t2, children_string, t2
));
}
Ok(dom_string)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dom::{Dom, NodeType};
#[test]
fn test_inline_span_parsing() {
let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
let expected_dom = Dom::create_p().with_children(
vec![
Dom::create_text("Text before "),
Dom::create_node(NodeType::Span)
.with_children(vec![Dom::create_text("inline text")].into()),
Dom::create_text(" text after."),
]
.into(),
);
assert_eq!(expected_dom.children.as_ref().len(), 3);
match &expected_dom.children.as_ref()[1].root.node_type {
NodeType::Span => {}
other => panic!("Expected Span, got {:?}", other),
}
assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
println!("Test passed: Inline span parsing structure is correct");
}
#[test]
fn test_xml_node_structure() {
let node = XmlNode {
node_type: "p".into(),
attributes: XmlAttributeMap {
inner: StringPairVec::from_const_slice(&[]),
},
children: vec![
XmlNodeChild::Text("Before ".into()),
XmlNodeChild::Element(XmlNode {
node_type: "span".into(),
children: vec![XmlNodeChild::Text("inline".into())].into(),
..Default::default()
}),
XmlNodeChild::Text(" after".into()),
]
.into(),
};
assert_eq!(node.children.as_ref().len(), 3);
assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
assert_eq!(
node.children.as_ref()[1]
.as_element()
.unwrap()
.node_type
.as_str(),
"span"
);
assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
let span = node.children.as_ref()[1].as_element().unwrap();
assert_eq!(span.children.as_ref().len(), 1);
assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
println!("Test passed: XmlNode structure preserves text nodes correctly");
}
}