use alloc::{
boxed::Box,
collections::BTreeMap,
string::{String, ToString},
vec::Vec,
};
use core::{fmt, fmt::Write, hash::Hash};
use azul_css::{
codegen::format::VecContents,
css::{
Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
NodeTypeTag,
},
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)]
#[derive(Debug, Copy, Clone)]
pub enum c_void {}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub enum XmlNodeType {
Root,
Element,
PI,
Comment,
Text,
}
#[repr(C)]
#[derive(Debug)]
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 {
#[must_use]
pub fn new(s: &str) -> Self {
Self {
inner: AzString::from(s),
}
}
#[must_use]
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" | "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
);
const MAX_XML_NESTING_DEPTH: usize = 512;
const MAX_TYPE_PARSE_DEPTH: usize = 64;
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct Xml {
pub root: XmlNodeChildVec,
}
impl Xml {
#[must_use]
pub fn scan_external_resources(&self) -> ExternalResourceVec {
let mut resources = Vec::new();
let mut stack: Vec<(&XmlNodeChild, usize)> = Vec::new();
for child in self.root.as_ref() {
stack.push((child, 0));
}
while let Some((child, depth)) = stack.pop() {
match child {
XmlNodeChild::Text(text) => {
Self::extract_css_urls(text.as_str(), &mut resources);
}
XmlNodeChild::Element(node) => {
if depth > MAX_XML_NESTING_DEPTH {
continue;
}
Self::scan_node(node, &mut resources);
for c in node.children.as_ref() {
stack.push((c, depth + 1));
}
}
}
}
resources.into()
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] 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().is_some_and(|t| t.starts_with("audio")) {
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() {
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"),
});
}
}
fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
let lower = css.to_ascii_lowercase();
let mut search_from = 0;
while let Some(rel) = lower[search_from..].find("url(") {
let url_start = search_from + rel;
let after = url_start + 4;
if lower[..url_start].trim_end().ends_with("@import") {
search_from = after;
continue;
}
let after_url = &css[after..];
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()"),
});
}
search_from = after;
}
let mut search_from = 0;
while let Some(rel) = lower[search_from..].find("@import") {
let after = search_from + rel + 7;
let after_import = &css[after..];
let trimmed = after_import.trim_start();
let import_url = if trimmed
.get(..4)
.is_some_and(|p| p.eq_ignore_ascii_case("url("))
{
Self::extract_url_value(&trimmed[4..])
} else {
Self::extract_quoted_string(trimmed)
};
if let Some(url) = import_url {
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"),
});
}
search_from = after;
}
}
fn extract_url_value(s: &str) -> Option<String> {
let trimmed = s.trim_start();
if trimmed.starts_with('"') {
Self::extract_quoted_string(trimmed)
} else if let Some(rest) = trimmed.strip_prefix('\'') {
let end = rest.find('\'')?;
Some(rest[..end].to_string())
} else {
let end = trimmed.find(')')?;
Some(trimmed[..end].trim().to_string())
}
}
fn extract_quoted_string(s: &str) -> Option<String> {
if let Some(rest) = s.strip_prefix('"') {
let end = rest.find('"')?;
Some(rest[..end].to_string())
} else if let Some(rest) = s.strip_prefix('\'') {
let end = rest.find('\'')?;
Some(rest[..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(alloc::string::ToString::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))
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
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, Eq, PartialOrd, Clone, Copy)]
#[repr(C)]
pub struct NonXmlCharError {
pub ch: u32,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
#[repr(C)]
pub struct InvalidCharError {
pub expected: u8,
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidCharMultipleError {
pub expected: u8,
pub got: U8Vec,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
#[repr(C)]
pub struct InvalidQuoteError {
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)]
#[repr(C)]
pub struct InvalidSpaceError {
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidStringError {
pub got: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, 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::{
InvalidChar, InvalidCharMultiple, InvalidCharacterData, InvalidCommentData,
InvalidCommentEnd, InvalidExternalID, InvalidName, InvalidQuote, InvalidReference,
InvalidSpace, InvalidString, NonXmlChar, UnexpectedEndOfStream,
};
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, Copy, 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, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct XmlTextError {
pub stream_error: XmlStreamError,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, 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::{
InvalidAttribute, InvalidCdata, InvalidCharData, InvalidComment, InvalidDeclaration,
InvalidDoctype, InvalidElement, InvalidEntity, InvalidPI, UnknownToken,
};
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, Eq, PartialOrd, Clone]
);
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedNamespaceError {
pub ns: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownNamespaceError {
pub ns: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnexpectedCloseTagError {
pub expected: AzString,
pub actual: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownEntityReferenceError {
pub entity: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedAttributeError {
pub attribute: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
#[repr(C)]
pub struct MalformedHierarchyError {
pub expected: AzString,
pub got: AzString,
}
#[derive(Debug, PartialEq, Eq, 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 {
#[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::XmlError::{
AttributesLimitReached, DtdDetected, DuplicatedAttribute, DuplicatedNamespace,
EntityReferenceLoop, InvalidAttributeValue, InvalidChar, InvalidChar2,
InvalidCharacterData, InvalidComment, InvalidElementNamePrefix, InvalidExternalID,
InvalidName, InvalidString, InvalidXmlPrefixUri, MalformedEntityReference,
MalformedHierarchy, NamespacesLimitReached, NoParserAvailable, NoRootNode,
NodesLimitReached, NonXmlChar, ParserError, SizeLimit, UnclosedRootNode,
UnexpectedCloseTag, UnexpectedDeclaration, UnexpectedEndOfStream,
UnexpectedEntityCloseTag, UnexpectedXmlUri, UnexpectedXmlnsUri, UnknownEntityReference,
UnknownNamespace, UnknownToken,
};
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 {
#[must_use]
pub fn builtin(name: &str) -> Self {
Self {
collection: AzString::from_const_str("builtin"),
name: AzString::from(name),
}
}
#[must_use]
pub fn new(collection: &str, name: &str) -> Self {
Self {
collection: AzString::from(collection),
name: AzString::from(name),
}
}
#[must_use]
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 {
#[must_use]
pub fn new(t: ComponentFieldType) -> Self {
Self {
ptr: Box::into_raw(Box::new(t)),
}
}
#[must_use]
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) {
let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
if !ptr.is_null() {
unsafe {
drop(Box::from_raw(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 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 {
#[must_use]
pub fn new(v: ComponentFieldValue) -> Self {
Self {
ptr: Box::into_raw(Box::new(v)),
}
}
#[must_use]
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) {
let ptr = core::mem::replace(&mut self.ptr, core::ptr::null_mut());
if !ptr.is_null() {
unsafe {
drop(Box::from_raw(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 {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
Self::parse_depth(s, 0)
}
fn parse_depth(s: &str, depth: usize) -> Option<Self> {
if depth > MAX_TYPE_PARSE_DEPTH {
return None;
}
let s = s.trim();
match s {
"String" | "string" => return Some(Self::String),
"Bool" | "bool" => return Some(Self::Bool),
"I32" | "i32" => return Some(Self::I32),
"I64" | "i64" => return Some(Self::I64),
"U32" | "u32" => return Some(Self::U32),
"U64" | "u64" => return Some(Self::U64),
"Usize" | "usize" => return Some(Self::Usize),
"F32" | "f32" => return Some(Self::F32),
"F64" | "f64" => return Some(Self::F64),
"ColorU" => return Some(Self::ColorU),
"CssProperty" => return Some(Self::CssProperty),
"ImageRef" => return Some(Self::ImageRef),
"FontRef" => return Some(Self::FontRef),
"StyledDom" => return Some(Self::StyledDom),
"RefAny" => return Some(Self::RefAny(AzString::from(""))),
_ => {}
}
if let Some(inner) = s.strip_prefix("Option<").and_then(|r| r.strip_suffix('>')) {
let inner_type = Self::parse_depth(inner, depth + 1)?;
return Some(Self::OptionType(ComponentFieldTypeBox::new(inner_type)));
}
if let Some(inner) = s.strip_prefix("Vec<").and_then(|r| r.strip_suffix('>')) {
let inner_type = Self::parse_depth(inner, depth + 1)?;
return Some(Self::VecType(ComponentFieldTypeBox::new(inner_type)));
}
if let Some(sig) = s
.strip_prefix("Callback(")
.and_then(|r| r.strip_suffix(')'))
{
return Some(Self::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(Self::RefAny(AzString::from(hint)));
}
if let Some(name) = s.strip_prefix("EnumRef(").and_then(|r| r.strip_suffix(')')) {
return Some(Self::EnumRef(AzString::from(name)));
}
if let Some(name) = s
.strip_prefix("StructRef(")
.and_then(|r| r.strip_suffix(')'))
{
return Some(Self::StructRef(AzString::from(name)));
}
if s.chars().next().is_some_and(char::is_uppercase) {
return Some(Self::StructRef(AzString::from(s)));
}
None
}
#[must_use]
pub fn format(&self) -> String {
match self {
Self::String => "String".to_string(),
Self::Bool => "Bool".to_string(),
Self::I32 => "I32".to_string(),
Self::I64 => "I64".to_string(),
Self::U32 => "U32".to_string(),
Self::U64 => "U64".to_string(),
Self::Usize => "Usize".to_string(),
Self::F32 => "F32".to_string(),
Self::F64 => "F64".to_string(),
Self::ColorU => "ColorU".to_string(),
Self::CssProperty => "CssProperty".to_string(),
Self::ImageRef => "ImageRef".to_string(),
Self::FontRef => "FontRef".to_string(),
Self::StyledDom => "StyledDom".to_string(),
Self::Callback(sig) => format!("Callback({})", sig.return_type.as_str()),
Self::RefAny(hint) => {
if hint.as_str().is_empty() {
"RefAny".to_string()
} else {
format!("RefAny({})", hint.as_str())
}
}
Self::OptionType(inner) => format!("Option<{}>", inner.as_ref().format()),
Self::VecType(inner) => format!("Vec<{}>", inner.as_ref().format()),
Self::StructRef(name) | Self::EnumRef(name) => name.as_str().to_string(),
}
}
}
impl fmt::Display for ComponentFieldType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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, Eq)]
#[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, Eq]
);
impl_vec_debug!(ComponentFieldOverride, ComponentFieldOverrideVec);
impl_vec_partialeq!(ComponentFieldOverride, ComponentFieldOverrideVec);
impl_vec_clone!(
ComponentFieldOverride,
ComponentFieldOverrideVec,
ComponentFieldOverrideVecDestructor
);
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum ComponentFieldValueSource {
Default,
Literal(AzString),
Binding(AzString),
}
#[allow(variant_size_differences)]
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
#[allow(clippy::large_enum_variant)] 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 {
#[must_use]
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
}
})
}
#[must_use]
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 {
#[must_use]
pub fn get_field(&self, name: &str) -> Option<&ComponentDataField> {
self.fields
.as_ref()
.iter()
.find(|f| f.name.as_str() == name)
}
#[must_use]
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,
})
}
#[must_use]
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 &mut fields_vec {
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 {
#[allow(clippy::wildcard_imports)] use super::*;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
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 = 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 = String::deserialize(deserializer)?;
Ok(string_to_field_type(&s))
}
}
fn field_type_to_string(ft: &ComponentFieldType) -> 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()),
}
}
#[allow(clippy::option_if_let_else)]
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 {
Self::None => serializer.serialize_none(),
Self::String(s) => serializer.serialize_str(s.as_str()),
Self::Bool(b) => serializer.serialize_bool(*b),
Self::I32(v) => serializer.serialize_i32(*v),
Self::I64(v) => serializer.serialize_i64(*v),
Self::U32(v) => serializer.serialize_u32(*v),
Self::U64(v) => serializer.serialize_u64(*v),
Self::Usize(v) => serializer.serialize_u64(*v as u64),
Self::F32(v) => serializer.serialize_f32(*v),
Self::F64(v) => serializer.serialize_f64(*v),
Self::ColorU(c) => serializer.serialize_str(&alloc::format!(
"#{:02x}{:02x}{:02x}{:02x}",
c.r,
c.g,
c.b,
c.a
)),
Self::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()
}
Self::CallbackFnPointer(name) => serializer.serialize_str(name.as_str()),
Self::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::Bool(b) => Self::Bool(b),
serde_json::Value::Number(n) => n.as_i64().map_or_else(
|| n.as_f64().map_or(Self::None, Self::F64),
|i| i32::try_from(i).map_or(Self::I64(i), Self::I32),
),
serde_json::Value::String(s) => Self::String(AzString::from(s.as_str())),
_ => Self::None,
})
}
}
impl Serialize for OptionComponentDefaultValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Some(v) => v.serialize(serializer),
Self::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(val.map_or(Self::None, Self::Some))
}
}
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: String,
#[serde(rename = "type", default = "default_type")]
field_type: ComponentFieldType,
#[serde(default)]
default: OptionComponentDefaultValue,
#[serde(default)]
required: bool,
#[serde(default)]
description: String,
}
const fn default_type() -> ComponentFieldType {
ComponentFieldType::String
}
let h = Helper::deserialize(deserializer)?;
Ok(Self {
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: 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> {
use serde::de::{IgnoredAny, MapAccess, Visitor};
struct ModelVisitor;
impl<'de> Visitor<'de> for ModelVisitor {
type Value = ComponentDataModel;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a data model object with `name`, `description` and `fields`")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut name: Option<String> = None;
let mut description: Option<String> = None;
let mut fields: Option<Vec<ComponentDataField>> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => name = Some(map.next_value()?),
"description" => description = Some(map.next_value()?),
"fields" => fields = Some(map.next_value()?),
_ => {
map.next_value::<IgnoredAny>()?;
}
}
}
Ok(ComponentDataModel {
name: AzString::from(name.unwrap_or_default().as_str()),
description: AzString::from(description.unwrap_or_default().as_str()),
fields: ComponentDataFieldVec::from_vec(fields.unwrap_or_default()),
})
}
}
deserializer.deserialize_map(ModelVisitor)
}
}
}
#[cfg(feature = "serde-json")]
impl ComponentDataModel {
pub fn to_json(&self) -> Result<String, String> {
serde_json::to_string_pretty(self).map_err(|e| alloc::format!("{e}"))
}
pub fn from_json(json: &str) -> Result<Self, String> {
serde_json::from_str(json).map_err(|e| alloc::format!("{e}"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum ComponentSource {
Builtin,
Compiled,
#[default]
UserDefined,
}
impl ComponentSource {
#[must_use]
pub fn create() -> Self {
Self::default()
}
}
#[allow(missing_copy_implementations)]
#[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_non_exhaustive()
}
}
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 {
#[must_use]
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))
}
#[must_use]
pub fn get_unqualified(&self, name: &str) -> Option<&ComponentDef> {
self.get("builtin", name)
}
#[must_use]
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)
}
}
#[must_use]
pub fn get_exportable_libraries(&self) -> Vec<&ComponentLibrary> {
self.libraries.iter().filter(|lib| lib.exportable).collect()
}
#[must_use]
pub fn all_components(&self) -> Vec<&ComponentDef> {
self.libraries
.iter()
.flat_map(|lib| lib.components.iter())
.collect()
}
}
macro_rules! html_tag_node_types {
($($tag:literal => $variant:ident),* $(,)?) => {
#[must_use] pub fn tag_to_node_type(tag: &str) -> NodeType {
match tag {
"img" => NodeType::Image(azul_css::css::BoxOrStatic::heap(
crate::resources::ImageRef::null_image(
0,
0,
crate::resources::RawImageFormat::RGBA8,
alloc::vec::Vec::new(),
),
)),
"icon" => NodeType::Icon(azul_css::css::BoxOrStatic::heap(
azul_css::AzString::from_const_str(""),
)),
"transient-window" => NodeType::TransientWindow(
crate::transient::TransientWindowConfig::closed(),
),
$($tag => NodeType::$variant,)*
_ => NodeType::Div,
}
}
fn tag_to_node_type_tag(tag: &str) -> NodeTypeTag {
match tag {
"img" | "image" => NodeTypeTag::Img,
"icon" => NodeTypeTag::Icon,
"transient-window" => NodeTypeTag::TransientWindow,
$($tag => NodeTypeTag::$variant,)*
_ => NodeTypeTag::Div,
}
}
};
}
html_tag_node_types! {
"html" => Html,
"head" => Head,
"title" => Title,
"body" => Body,
"div" => Div,
"header" => Header,
"footer" => Footer,
"section" => Section,
"article" => Article,
"aside" => Aside,
"nav" => Nav,
"main" => Main,
"figure" => Figure,
"figcaption" => FigCaption,
"address" => Address,
"details" => Details,
"summary" => Summary,
"dialog" => Dialog,
"h1" => H1,
"h2" => H2,
"h3" => H3,
"h4" => H4,
"h5" => H5,
"h6" => H6,
"p" => P,
"span" => Span,
"pre" => Pre,
"code" => Code,
"blockquote" => BlockQuote,
"br" => Br,
"hr" => Hr,
"pagebreak" => PageBreak,
"ul" => Ul,
"ol" => Ol,
"li" => Li,
"dl" => Dl,
"dt" => Dt,
"dd" => Dd,
"menu" => Menu,
"menuitem" => MenuItem,
"dir" => Dir,
"table" => Table,
"caption" => Caption,
"thead" => THead,
"tbody" => TBody,
"tfoot" => TFoot,
"tr" => Tr,
"th" => Th,
"td" => Td,
"colgroup" => ColGroup,
"col" => Col,
"form" => Form,
"fieldset" => FieldSet,
"legend" => Legend,
"label" => Label,
"input" => Input,
"button" => Button,
"select" => Select,
"optgroup" => OptGroup,
"option" => SelectOption,
"textarea" => TextArea,
"output" => Output,
"progress" => Progress,
"meter" => Meter,
"datalist" => DataList,
"a" => A,
"strong" => Strong,
"em" => Em,
"b" => B,
"i" => I,
"u" => U,
"s" => S,
"small" => Small,
"mark" => Mark,
"del" => Del,
"ins" => Ins,
"samp" => Samp,
"kbd" => Kbd,
"var" => Var,
"cite" => Cite,
"dfn" => Dfn,
"abbr" => Abbr,
"acronym" => Acronym,
"q" => Q,
"time" => Time,
"sub" => Sub,
"sup" => Sup,
"big" => Big,
"bdo" => Bdo,
"bdi" => Bdi,
"wbr" => Wbr,
"ruby" => Ruby,
"rt" => Rt,
"rtc" => Rtc,
"rp" => Rp,
"data" => Data,
"canvas" => Canvas,
"object" => Object,
"param" => Param,
"embed" => Embed,
"audio" => Audio,
"video" => Video,
"source" => Source,
"track" => Track,
"map" => Map,
"area" => Area,
"svg" => Svg,
"g" => SvgG,
"defs" => SvgDefs,
"symbol" => SvgSymbol,
"use" => SvgUse,
"switch" => SvgSwitch,
"path" => SvgPath,
"circle" => SvgCircle,
"rect" => SvgRect,
"ellipse" => SvgEllipse,
"line" => SvgLine,
"polygon" => SvgPolygon,
"polyline" => SvgPolyline,
"tspan" => SvgTspan,
"textpath" => SvgTextPath,
"lineargradient" => SvgLinearGradient,
"radialgradient" => SvgRadialGradient,
"stop" => SvgStop,
"pattern" => SvgPattern,
"clippath" => SvgClipPathElement,
"mask" => SvgMask,
"filter" => SvgFilter,
"feblend" => SvgFeBlend,
"fecolormatrix" => SvgFeColorMatrix,
"fecomponenttransfer" => SvgFeComponentTransfer,
"fecomposite" => SvgFeComposite,
"feconvolvematrix" => SvgFeConvolveMatrix,
"fediffuselighting" => SvgFeDiffuseLighting,
"fedisplacementmap" => SvgFeDisplacementMap,
"fedistantlight" => SvgFeDistantLight,
"fedropshadow" => SvgFeDropShadow,
"feflood" => SvgFeFlood,
"fefuncr" => SvgFeFuncR,
"fefuncg" => SvgFeFuncG,
"fefuncb" => SvgFeFuncB,
"fefunca" => SvgFeFuncA,
"fegaussianblur" => SvgFeGaussianBlur,
"feimage" => SvgFeImage,
"femerge" => SvgFeMerge,
"femergenode" => SvgFeMergeNode,
"femorphology" => SvgFeMorphology,
"feoffset" => SvgFeOffset,
"fepointlight" => SvgFePointLight,
"fespecularlighting" => SvgFeSpecularLighting,
"fespotlight" => SvgFeSpotLight,
"fetile" => SvgFeTile,
"feturbulence" => SvgFeTurbulence,
"foreignobject" => SvgForeignObject,
"desc" => SvgDesc,
"view" => SvgView,
"animate" => SvgAnimate,
"animatemotion" => SvgAnimateMotion,
"animatetransform" => SvgAnimateTransform,
"set" => SvgSet,
"mpath" => SvgMpath,
"meta" => Meta,
"link" => Link,
"script" => Script,
"style" => Style,
"base" => Base,
}
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_do_not_use_without_block_level_wrapper(
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 => {
text.map_or_else(|| Ok(format!("Dom::create_node(NodeType::{type_name})").into()), |text_str| Ok(format!(
"Dom::create_node(NodeType::{}).with_children(vec![Dom::create_text_do_not_use_without_block_level_wrapper(\"{}\")])",
type_name,
text_str.as_str().replace('\\', "\\\\").replace('"', "\\\"")
).into()))
}
CompileTarget::C => {
text.map_or_else(|| Ok(format!("AzDom_create{type_name}()").into()), |text_str| Ok(format!(
"AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{}\"))",
text_str
.as_str()
.replace('\\', "\\\\")
.replace('"', "\\\"")
)
.into()))
}
CompileTarget::Cpp => Ok(format!("Dom::create_{}()", type_name.to_lowercase()).into()),
CompileTarget::Python => Ok(format!("Dom.create_{}()", type_name.to_lowercase()).into()),
};
r.into()
}
fn push_scalar_field(children: &mut Vec<Dom>, field_name: &str, value: &dyn 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_do_not_use_without_block_level_wrapper(
text
)]
.into(),
),
);
}
#[allow(clippy::too_many_lines)] #[must_use]
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() {
let field_name = field.name.as_str();
match &field.default_value {
OptionComponentDefaultValue::None => {
}
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_do_not_use_without_block_level_wrapper(
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_do_not_use_without_block_level_wrapper(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_do_not_use_without_block_level_wrapper(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_do_not_use_without_block_level_wrapper(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_do_not_use_without_block_level_wrapper(
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_do_not_use_without_block_level_wrapper(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_do_not_use_without_block_level_wrapper(text)
]
.into(),
),
);
}
ComponentDefaultValue::None => {
}
}
}
}
}
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::empty()
} else {
Css::from_string(def.css.clone())
};
let r: Result<StyledDom, RenderDomError> = Ok(StyledDom::create(&mut wrapper, css));
r.into()
}
#[allow(clippy::too_many_lines)] #[must_use]
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!("{indent_str}// Component: {tag}"));
lines.push(alloc::format!(
"{indent_str}let mut children: Vec<Dom> = Vec::new();"
));
for field in data.fields.as_ref() {
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!(
"{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"));"
));
}
OptionComponentDefaultValue::Some(ComponentDefaultValue::Bool(b)) => {
lines.push(alloc::format!(
"{inner_indent}children.push(Dom::create_text_do_not_use_without_block_level_wrapper(format!(\"{{}}: {{}}\", \"{fname}\", {b}).as_str()));"
));
}
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!(
"{indent_str}Dom::create_node(NodeType::Div).with_children(children.into())"
));
Ok(lines.join("\n").into())
}
CompileTarget::C => {
let mut lines = Vec::new();
lines.push(alloc::format!("{indent_str}/* Component: {tag} */"));
lines.push(alloc::format!(
"{indent_str}AzDom root = AzDom_createDiv();"
));
for field in data.fields.as_ref() {
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!(
"{inner_indent}AzDom_addChild(&root, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{escaped}\")));"
));
}
OptionComponentDefaultValue::Some(
ComponentDefaultValue::ComponentInstance(ci),
) => {
let fn_name =
alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
lines.push(alloc::format!(
"{inner_indent}AzDom_addChild(&root, {fn_name}());"
));
}
_ => {
lines.push(alloc::format!("{inner_indent}/* field '{fname}' */"));
}
}
}
lines.push(alloc::format!("{indent_str}return root;"));
Ok(lines.join("\n").into())
}
CompileTarget::Cpp => {
let mut lines = Vec::new();
lines.push(alloc::format!("{indent_str}// Component: {tag}"));
lines.push(alloc::format!("{indent_str}auto root = Dom::create_div();"));
for field in data.fields.as_ref() {
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!(
"{inner_indent}root.add_child(Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{escaped}\")));"
));
}
OptionComponentDefaultValue::Some(
ComponentDefaultValue::ComponentInstance(ci),
) => {
let fn_name =
alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
lines.push(alloc::format!("{inner_indent}root.add_child({fn_name}());"));
}
_ => {
lines.push(alloc::format!("{inner_indent}// field '{fname}'"));
}
}
}
lines.push(alloc::format!("{indent_str}return root;"));
Ok(lines.join("\n").into())
}
CompileTarget::Python => {
let mut lines = Vec::new();
lines.push(alloc::format!("{indent_str}# Component: {tag}"));
lines.push(alloc::format!("{indent_str}root = Dom.create_div()"));
for field in data.fields.as_ref() {
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!(
"{inner_indent}root = root.with_child(Dom.create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"))"
));
}
OptionComponentDefaultValue::Some(
ComponentDefaultValue::ComponentInstance(ci),
) => {
let fn_name =
alloc::format!("render_{}", ci.component.as_str().replace('-', "_"));
lines.push(alloc::format!(
"{inner_indent}root = root.with_child({fn_name}())"
));
}
_ => {
lines.push(alloc::format!("{inner_indent}# field '{fname}'"));
}
}
}
lines.push(alloc::format!("{indent_str}return root"));
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!("{display_name}Data");
ComponentDef {
id: ComponentId::builtin(tag),
display_name: AzString::from(display_name),
description: AzString::from(format!("HTML <{tag}> element").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: default.map_or_else(
|| OptionComponentDefaultValue::None,
OptionComponentDefaultValue::Some,
),
required,
description: AzString::from(description),
}
}
#[allow(clippy::too_many_lines)] fn builtin_data_model(tag: &str) -> Vec<ComponentDataField> {
use ComponentDefaultValue as D;
use ComponentFieldType::{Bool, String, I32};
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 {
Self {
libraries: ComponentLibraryVec::from_const_slice(&[]),
}
}
}
impl ComponentMap {
#[must_use]
pub fn create() -> Self {
Self::default()
}
#[must_use]
pub fn with_builtin() -> Self {
Self {
libraries: alloc::vec![register_builtin_components()].into(),
}
}
#[must_use]
pub fn from_libraries(libs: &ComponentLibraryVec) -> Self {
Self {
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 &mut fields_vec {
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_do_not_use_without_block_level_wrapper(
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::create_div()\n} else {\n // else branch\n Dom::create_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::create_div();\n} else {\n // else branch\n Dom::create_div();\n}"
)),
CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
"if data.condition:\n # then branch\n Dom.create_div()\nelse:\n # else branch\n Dom.create_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: Vec<Dom> = Vec::new();
for i in 0..count {
items.push(
Dom::create_node(NodeType::Div).with_children(
alloc::vec![Dom::create_text_do_not_use_without_block_level_wrapper(
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::create_div());\n}\nDom::create_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::create_div();\nfor (uint32_t i = 0; i < data.count; i++) {\n container.add_child(Dom::create_div());\n}"
)),
CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
"container = Dom.create_div()\nfor i in range(data.count):\n container = container.with_child(Dom.create_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_do_not_use_without_block_level_wrapper(
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::create_div() // map template\n}).collect();\nDom::create_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::create_div();\n// TODO: iterate parsed JSON array"
)),
CompileTarget::Python => ResultStringCompileError::Ok(AzString::from(
"import json\nitems = json.loads(data.data_json)\ncontainer = Dom.create_div()\nfor item in items:\n container = container.with_child(Dom.create_div())"
)),
}
}
#[allow(clippy::too_many_lines)] #[must_use]
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("pagebreak", "Page Break", 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(Debug, 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);
assert!(
!(self.parsed_dom != fixed),
"\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)
);
}
#[must_use]
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 {
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s.as_str()),
Self::Element(_) => None,
}
}
#[must_use]
pub const fn as_element(&self) -> Option<&XmlNode> {
match self {
Self::Text(_) => None,
Self::Element(node) => Some(node),
}
}
pub const fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
match self {
Self::Text(_) => None,
Self::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 {
Self {
node_type: node_type.into(),
..Default::default()
}
}
#[must_use]
pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
Self {
children: v.into(),
..self
}
}
#[must_use]
pub fn get_text_content(&self) -> String {
self.children
.as_ref()
.iter()
.filter_map(|child| child.as_text())
.collect::<Vec<_>>()
.join("")
}
#[must_use]
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 {
Self::Dom(RenderDomError::Component(e))
}
}
impl From<CssParseErrorOwned> for CompileError {
fn from(e: CssParseErrorOwned) -> Self {
Self::Css(e)
}
}
impl fmt::Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::CompileError::{Css, Dom, Xml};
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 {
Self::Dom(e)
}
}
impl From<DomXmlParseError> for CompileError {
fn from(e: DomXmlParseError) -> Self {
Self::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, Eq)]
#[repr(C)]
pub struct MissingTypeError {
pub arg_pos: usize,
pub arg_name: AzString,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub struct WhiteSpaceInComponentNameError {
pub arg_pos: usize,
pub arg_name: AzString,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[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 fmt::Display for DomXmlParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::DomXmlParseError::{
Component, Css, MalformedHierarchy, MultipleBodyNodes, MultipleHtmlRootNodes,
NoBodyInHtml, NoHtmlNode, RenderDom, Xml,
};
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 fmt::Display for ComponentParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::ComponentParseError::{
CssError, MissingName, MissingType, NotAComponent, UnnamedComponent,
WhiteSpaceInComponentName, WhiteSpaceInComponentType,
};
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 {arg_pos} is either empty or has no name"
),
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::{UnknownComponent, UselessFunctionArgument};
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 fmt::Display for RenderDomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::RenderDomError::{Component, CssError};
match self {
Component(c) => write!(f, "{c}"),
CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
}
}
}
#[allow(clippy::result_large_err)] fn head_style_text(html_node: &XmlNode) -> String {
let Some(head) = find_node_by_type(html_node.children.as_ref(), "head") else {
return String::new();
};
let mut out = String::new();
for child in head.children.as_ref() {
let XmlNodeChild::Element(element) = child else {
continue;
};
if !element.node_type.as_str().eq_ignore_ascii_case("style") {
continue;
}
let text = element.get_text_content();
if !text.is_empty() {
if !out.is_empty() {
out.push('\n');
}
out.push_str(&text);
}
}
out
}
pub fn get_html_node(
root_nodes: &[XmlNodeChild],
) -> Result<alloc::borrow::Cow<'_, XmlNode>, DomXmlParseError> {
use alloc::borrow::Cow;
let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
if let XmlNodeChild::Element(node) = child {
if node.node_type.as_str().eq_ignore_ascii_case("html") {
Some(node)
} else {
None
}
} else {
None
}
});
if let Some(html_node) = html_node_iterator.next() {
return if html_node_iterator.next().is_some() {
Err(DomXmlParseError::MultipleHtmlRootNodes)
} else {
Ok(Cow::Borrowed(html_node))
};
}
let has_structural_root = root_nodes.iter().any(|child| match child {
XmlNodeChild::Element(node) => {
let tag = node.node_type.as_str();
tag.eq_ignore_ascii_case("body") || tag.eq_ignore_ascii_case("head")
}
XmlNodeChild::Text(_) => false,
});
if has_structural_root {
return Ok(Cow::Owned(
XmlNode::create("html").with_children(root_nodes.to_vec()),
));
}
let (head_children, body_children): (Vec<_>, Vec<_>) =
root_nodes.iter().cloned().partition(|child| match child {
XmlNodeChild::Element(node) => {
let tag = node.node_type.as_str();
tag.eq_ignore_ascii_case("style")
|| tag.eq_ignore_ascii_case("link")
|| tag.eq_ignore_ascii_case("meta")
|| tag.eq_ignore_ascii_case("title")
|| tag.eq_ignore_ascii_case("base")
}
XmlNodeChild::Text(_) => false,
});
let mut html_children = Vec::new();
if !head_children.is_empty() {
html_children.push(XmlNodeChild::Element(
XmlNode::create("head").with_children(head_children),
));
}
html_children.push(XmlNodeChild::Element(
XmlNode::create("body").with_children(body_children),
));
Ok(Cow::Owned(XmlNode::create("html").with_children(html_children)))
}
#[allow(clippy::result_large_err)] pub fn get_body_node(root_nodes: &[XmlNodeChild]) -> Result<&XmlNode, DomXmlParseError> {
fn find_body_recursive(nodes: &[XmlNodeChild], depth: usize) -> Option<&XmlNode> {
if depth > MAX_XML_NESTING_DEPTH {
return None;
}
for child in nodes {
if let XmlNodeChild::Element(node) = child {
if node.node_type.as_str().eq_ignore_ascii_case("body") {
return Some(node);
}
if let Some(found) = find_body_recursive(node.children.as_ref(), depth + 1) {
return Some(found);
}
}
}
None
}
let direct_body = root_nodes.iter().find_map(|child| {
if let XmlNodeChild::Element(node) = child {
if node.node_type.as_str().eq_ignore_ascii_case("body") {
Some(node)
} else {
None
}
} else {
None
}
});
if let Some(body) = direct_body {
return Ok(body);
}
find_body_recursive(root_nodes, 0).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 node.node_type.as_str().eq_ignore_ascii_case(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
}
#[must_use]
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)
}
#[must_use]
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 Some(item) = hierarchy.pop() else {
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 Some(cur_item) = hierarchy.pop() else {
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, }
}
#[allow(clippy::result_large_err)] 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)
}
#[allow(clippy::result_large_err)] 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 style_text = head_style_text(&html_node);
let global_style = if style_text.is_empty() {
None
} else {
Some(Css::from_string(style_text.into()))
};
render_dom_from_body_node_fast(body_node, global_style, component_map, max_width)
.map_err(Into::into)
}
#[allow(clippy::result_large_err)] 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 style_text = head_style_text(&html_node);
let global_style = if style_text.is_empty() {
None
} else {
Some(Css::from_string(style_text.into()))
};
let body_dom =
xml_node_to_dom_fast(body_node, component_map, false, 0).map_err(DomXmlParseError::from)?;
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 {
full_dom.css = alloc::vec![css].into();
}
Ok(full_dom)
}
#[allow(clippy::result_large_err)] 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 style_text = head_style_text(&html_node);
let mut global_style = if style_text.is_empty() {
Css::empty()
} else {
azul_css::parser2::new_from_str(&style_text).0
};
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 _ = (&css_blocks, &extra_blocks);
let main_func = "
use azul::{
app::{App, AppConfig},
dom::Dom,
callbacks::{RefAny, LayoutCallbackInfo},
window::WindowCreateOptions,
};
struct Data { }
extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> Dom {
crate::ui::render()
}
fn main() {
let config = AppConfig::create();
let app = App::create(RefAny::new(Data { }), config);
let window = WindowCreateOptions::create(render);
app.run(window);
}";
let ui_module = format!(
"#[allow(unused_imports)]\r\npub mod ui {{
use azul::prelude::*;
use azul::dom::{{NodeType, TabIndex, SmallAriaInfo}};
use azul::str::String as AzString;
pub fn render() -> Dom {{\r\n{app_source}\r\n }}\r\n}}"
);
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()), ui_module,
main_func,
);
Ok(source_code)
}
#[allow(clippy::needless_pass_by_value)] 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 {name} {{\r\n use azul::dom::Dom;\r\n use \
azul::str::String as AzString;\r\n{f}\r\n}}"
)
})
.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{cs}\r\n}}")
}
}
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(", ")
}
#[must_use]
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_length(attr: Option<&AzString>) -> Option<f32> {
let raw = attr?.as_str().trim();
let number = raw.strip_suffix("px").unwrap_or(raw).trim();
number.parse::<f32>().ok()
}
fn parse_svg_view_box(value: &str) -> Option<(f32, f32, f32, f32)> {
let nums: Vec<f32> = value
.split(|c: char| c == ',' || c.is_ascii_whitespace())
.filter(|s| !s.is_empty())
.map(str::parse::<f32>)
.collect::<Result<Vec<_>, _>>()
.ok()?;
match nums[..] {
[min_x, min_y, width, height] if width > 0.0 && height > 0.0 => {
Some((min_x, min_y, width, height))
}
_ => None,
}
}
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().is_multiple_of(2) {
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),
}]),
})
}
#[allow(clippy::too_many_lines)] fn apply_xml_node_attributes(
node: &mut crate::dom::NodeData,
xml_node: &XmlNode,
component_name: &str,
inside_svg: bool,
) {
use crate::dom::{IdOrClass, NodeType, TabIndex};
if component_name == "img" {
if let Some(src) = xml_node.attributes.get_key("src") {
let width = xml_node
.attributes
.get_key("width")
.and_then(|w| {
w.as_str()
.trim()
.trim_end_matches("px")
.trim()
.parse::<usize>()
.ok()
})
.unwrap_or(0);
let height = xml_node
.attributes
.get_key("height")
.and_then(|h| {
h.as_str()
.trim()
.trim_end_matches("px")
.trim()
.parse::<usize>()
.ok()
})
.unwrap_or(0);
let image_ref = crate::resources::ImageRef::null_image(
width,
height,
crate::resources::RawImageFormat::RGBA8,
src.as_str().as_bytes().to_vec(),
);
node.set_node_type(NodeType::Image(azul_css::css::BoxOrStatic::heap(image_ref)));
}
}
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.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()))
{
if focusable {
node.set_tab_index(TabIndex::Auto);
} else {
node.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.set_tab_index(TabIndex::Auto),
i if i > 0 => node.set_tab_index(TabIndex::OverrideInParent(
u32::try_from(i).unwrap_or(u32::MAX),
)),
_ => node.set_tab_index(TabIndex::NoKeyboardFocus),
}
}
apply_cell_span_attributes(node, xml_node);
let dir_prop = xml_node.attributes.get_key("dir").and_then(|d| {
let v = d.as_str().trim();
if v.eq_ignore_ascii_case("rtl") {
Some(azul_css::props::style::StyleDirection::Rtl)
} else if v.eq_ignore_ascii_case("ltr") {
Some(azul_css::props::style::StyleDirection::Ltr)
} else {
None
}
});
let mut intrinsic_props: Vec<azul_css::dynamic_selector::CssPropertyWithConditions> =
Vec::new();
if component_name == "svg" {
let view_box = xml_node
.attributes
.get_key("viewBox")
.or_else(|| xml_node.attributes.get_key("viewbox"))
.and_then(|v| parse_svg_view_box(v.as_str()));
if let Some((min_x, min_y, width, height)) = view_box {
node.set_svg_data(crate::dom::SvgNodeData::ViewBox {
min_x,
min_y,
width,
height,
});
}
let stated = |key: &str| parse_svg_length(xml_node.attributes.get_key(key));
let usable = |v: f32| v.is_finite() && v > 0.0;
if let Some(w) = stated("width")
.or(view_box.map(|(_, _, w, _)| w))
.filter(|w| usable(*w))
{
intrinsic_props.push(
azul_css::dynamic_selector::CssPropertyWithConditions::simple(
azul_css::props::property::CssProperty::width(
azul_css::props::layout::LayoutWidth::px(w),
),
),
);
}
if let Some(h) = stated("height")
.or(view_box.map(|(_, _, _, h)| h))
.filter(|h| usable(*h))
{
intrinsic_props.push(
azul_css::dynamic_selector::CssPropertyWithConditions::simple(
azul_css::props::property::CssProperty::height(
azul_css::props::layout::LayoutHeight::px(h),
),
),
);
}
}
if inside_svg
&& matches!(
component_name,
"path" | "circle" | "rect" | "ellipse" | "line" | "polygon" | "polyline"
)
{
use azul_css::props::{
layout::{
LayoutInsetBottom, LayoutLeft, LayoutPosition, LayoutRight, LayoutTop,
},
property::CssProperty,
};
let simple = azul_css::dynamic_selector::CssPropertyWithConditions::simple;
intrinsic_props.push(simple(CssProperty::const_position(LayoutPosition::Absolute)));
intrinsic_props.push(simple(CssProperty::const_left(LayoutLeft::const_px(0))));
intrinsic_props.push(simple(CssProperty::const_top(LayoutTop::const_px(0))));
intrinsic_props.push(simple(CssProperty::const_right(LayoutRight::const_px(0))));
intrinsic_props.push(simple(CssProperty::const_bottom(LayoutInsetBottom::const_px(
0,
))));
if let Some(fill) = xml_node.attributes.get_key("fill") {
let fill = fill.as_str().trim();
if fill != "none" {
if let Ok(color) = azul_css::props::basic::color::parse_css_color(fill) {
intrinsic_props.push(simple(CssProperty::const_background_content(
azul_css::props::style::StyleBackgroundContentVec::from_vec(vec![
azul_css::props::style::StyleBackgroundContent::Color(color),
]),
)));
}
}
}
if let Some(stroke) = xml_node.attributes.get_key("stroke") {
let stroke = stroke.as_str().trim();
if stroke != "none" {
if let Ok(color) = azul_css::props::basic::color::parse_css_color(stroke) {
use azul_css::props::style::{
StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor,
StyleBorderTopColor,
};
intrinsic_props.push(simple(CssProperty::const_border_top_color(
StyleBorderTopColor { inner: color },
)));
intrinsic_props.push(simple(CssProperty::const_border_right_color(
StyleBorderRightColor { inner: color },
)));
intrinsic_props.push(simple(CssProperty::const_border_bottom_color(
StyleBorderBottomColor { inner: color },
)));
intrinsic_props.push(simple(CssProperty::const_border_left_color(
StyleBorderLeftColor { inner: color },
)));
}
}
}
if let Some(width) = parse_svg_float(xml_node.attributes.get_key("stroke-width")) {
if width.is_finite() && width > 0.0 {
use azul_css::props::style::{
LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
LayoutBorderTopWidth,
};
let px = azul_css::props::basic::PixelValue::px(width);
intrinsic_props.push(simple(CssProperty::const_border_top_width(
LayoutBorderTopWidth { inner: px },
)));
intrinsic_props.push(simple(CssProperty::const_border_right_width(
LayoutBorderRightWidth { inner: px },
)));
intrinsic_props.push(simple(CssProperty::const_border_bottom_width(
LayoutBorderBottomWidth { inner: px },
)));
intrinsic_props.push(simple(CssProperty::const_border_left_width(
LayoutBorderLeftWidth { inner: px },
)));
}
}
}
if component_name == "svg" {
intrinsic_props.push(
azul_css::dynamic_selector::CssPropertyWithConditions::simple(
azul_css::props::property::CssProperty::const_position(
azul_css::props::layout::LayoutPosition::Relative,
),
),
);
}
let style_attr = xml_node.attributes.get_key("style");
if style_attr.is_some() || dir_prop.is_some() || !intrinsic_props.is_empty() {
use azul_css::dynamic_selector::CssPropertyWithConditions;
let css_key_map = azul_css::props::property::get_css_key_map();
let mut props: Vec<CssPropertyWithConditions> = intrinsic_props;
if let Some(dir) = dir_prop {
props.push(CssPropertyWithConditions::simple(
azul_css::props::property::CssProperty::Direction(
azul_css::css::CssPropertyValue::Exact(dir),
),
));
}
if let Some(style) = style_attr {
let mut attributes = Vec::new();
for s in style.as_str().split(';') {
let mut s = s.split(':');
let Some(key) = s.next() else {
continue;
};
let Some(value) = s.next() else {
continue;
};
drop(azul_css::parser2::parse_css_declaration(
key.trim(),
value.trim(),
azul_css::parser2::ErrorLocationRange::default(),
&css_key_map,
&mut Vec::new(),
&mut attributes,
));
}
props.extend(attributes.into_iter().filter_map(|s| match s {
CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
CssDeclaration::Dynamic(_) => None,
}));
}
if !props.is_empty() {
node.set_css_props(props.into());
}
}
let tag = component_name;
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::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::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::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::{SvgCubicCurve, SvgPoint};
const KAPPA: f32 = 0.552_284_8;
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.set_svg_data(crate::dom::SvgNodeData::Path(mp));
}
}
}
fn apply_cell_span_attributes(node: &mut crate::dom::NodeData, xml_node: &XmlNode) {
let mut spans = Vec::new();
if let Some(n) = xml_node
.attributes
.get_key("colspan")
.and_then(|v| v.as_str().trim().parse::<i32>().ok())
{
spans.push(crate::dom::AttributeType::ColSpan(n));
}
if let Some(n) = xml_node
.attributes
.get_key("rowspan")
.and_then(|v| v.as_str().trim().parse::<i32>().ok())
{
spans.push(crate::dom::AttributeType::RowSpan(n));
}
if !spans.is_empty() {
let mut v = node.attributes().clone().into_library_owned_vec();
v.extend(spans);
node.set_attributes(v.into());
}
}
#[allow(clippy::result_large_err)]
#[allow(clippy::only_used_in_recursion)]
fn collect_style_text(node: &XmlNode, out: &mut Vec<String>, depth: usize) {
if depth >= MAX_XML_NESTING_DEPTH {
return;
}
for child in node.children.as_ref() {
let XmlNodeChild::Element(element) = child else {
continue;
};
if normalize_casing(&element.node_type) == "style" {
let text = element.get_text_content();
if !text.is_empty() {
out.push(text);
}
} else {
collect_style_text(element, out, depth + 1);
}
}
}
fn xml_node_to_dom_fast<'a>(
xml_node: &'a XmlNode,
component_map: &'a ComponentMap,
inside_svg: bool,
depth: usize,
) -> Result<Dom, RenderDomError> {
use crate::dom::Dom;
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);
apply_xml_node_attributes(&mut dom.root, xml_node, &component_name, inside_svg);
let child_inside_svg = inside_svg || component_name == "svg";
if depth >= MAX_XML_NESTING_DEPTH {
return Ok(dom);
}
let mut children = Vec::new();
let mut scoped_css: Vec<Css> = Vec::new();
if component_name == "svg" {
let mut texts = Vec::new();
collect_style_text(xml_node, &mut texts, 0);
for text in texts {
scoped_css.push(Css::from_string(text.into()));
}
}
for child in xml_node.children.as_ref() {
match child {
XmlNodeChild::Element(child_node)
if normalize_casing(&child_node.node_type) == "style" =>
{
if component_name != "svg" {
let text = child_node.get_text_content();
if !text.is_empty() {
scoped_css.push(Css::from_string(text.into()));
}
}
}
XmlNodeChild::Element(child_node) => {
let child_dom =
xml_node_to_dom_fast(child_node, component_map, child_inside_svg, depth + 1)?;
children.push(child_dom);
}
XmlNodeChild::Text(text) => {
let text_dom = Dom::create_text_do_not_use_without_block_level_wrapper(
AzString::from(text.as_str()),
);
children.push(text_dom);
}
}
}
if !children.is_empty() {
dom = dom.with_children(children.into());
}
for css in scoped_css {
dom.add_component_css(css);
}
Ok(dom)
}
#[derive(Debug)]
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 Default for CompactDomBuilder {
fn default() -> Self {
Self::new()
}
}
impl CompactDomBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
hierarchy: Vec::new(),
node_data: Vec::new(),
css: Vec::new(),
stack: Vec::new(),
}
}
#[must_use]
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_or(0, |pi| NodeId::into_raw(&Some(NodeId::new(pi))))
} 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_or(0, |lc| NodeId::into_raw(&Some(NodeId::new(lc))));
}
}
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: Css) {
self.css.push(crate::dom::CssWithNodeId { node_id, css });
}
#[must_use]
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(),
}
}
}
#[allow(clippy::result_large_err)]
#[allow(clippy::only_used_in_recursion)]
fn xml_node_to_fast_dom<'a>(
xml_node: &'a XmlNode,
component_map: &'a ComponentMap,
inside_svg: bool,
builder: &mut CompactDomBuilder,
depth: usize,
) -> Result<(), RenderDomError> {
use crate::dom::NodeData;
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);
apply_xml_node_attributes(&mut node_data, xml_node, &component_name, inside_svg);
let child_inside_svg = inside_svg || component_name == "svg";
builder.open_node(node_data);
if depth < MAX_XML_NESTING_DEPTH {
for child in xml_node.children.as_ref() {
match child {
XmlNodeChild::Element(child_node) => {
xml_node_to_fast_dom(
child_node,
component_map,
child_inside_svg,
builder,
depth + 1,
)?;
}
XmlNodeChild::Text(text) => {
builder.add_leaf(
NodeData::create_text_do_not_use_without_block_level_wrapper(
AzString::from(text.as_str()),
),
);
}
}
}
}
builder.close_node();
Ok(())
}
#[allow(clippy::result_large_err)] 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, 0)?;
builder.close_node();
let mut combined_rules: Vec<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());
}
let mut combined_keyframes = Vec::new();
if let Some(css) = global_css.take() {
combined_rules.extend(css.rules.into_library_owned_vec());
combined_keyframes.extend(css.keyframes.into_library_owned_vec());
}
let mut combined_css = Css::new(combined_rules);
combined_css.keyframes = combined_keyframes.into();
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 _ = &t;
for id in xml_attributes
.get_key("id")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default()
{
let _ = write!(
dom_string,
"\r\n{}.with_id(\"{}\")",
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()
{
let _ = write!(
dom_string,
"\r\n{}.with_class(\"{}\")",
t0,
format_args_dynamic(class, filtered_xml_attributes)
);
}
if let Some(focusable) = xml_attributes
.get_key("focusable")
.map(|f| format_args_dynamic(f, filtered_xml_attributes))
.and_then(|f| parse_bool(&f))
{
if focusable {
let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)");
} else {
let _ = write!(
dom_string,
"\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
);
}
}
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 => {
let _ = write!(dom_string, "\r\n{t}.with_tab_index(TabIndex::Auto)");
}
i if i > 0 => {
let _ = write!(
dom_string,
"\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
t,
usize::try_from(i).unwrap_or(0)
);
}
_ => {
let _ = write!(
dom_string,
"\r\n{t}.with_tab_index(TabIndex::NoKeyboardFocus)"
);
}
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum DynamicItem {
Var {
name: String,
format_spec: Option<String>,
},
Str(String),
}
#[must_use]
pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
use self::DynamicItem::{Str, Var};
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 += 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());
if let Some(resolved_var) = variables
.iter()
.find(|s| s.name.as_str() == variable_name)
.map(|q| &q.arg_type)
{
s.push_str(resolved_var);
} else {
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
}
#[must_use]
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)
}
fn decode_numeric_entity(entity: &str) -> Option<char> {
let num = entity.strip_prefix('#')?;
let code = if let Some(hex) = num.strip_prefix(['x', 'X']) {
u32::from_str_radix(hex, 16).ok()?
} else {
num.parse::<u32>().ok()?
};
char::from_u32(code)
}
fn decode_entities(input: &str) -> String {
const MAX_ENTITY_BODY: usize = 12;
let mut out = String::with_capacity(input.len());
let bytes = input.as_bytes();
let mut i = 0;
while i < input.len() {
if bytes[i] == b'&' {
if let Some(semi_rel) = input[i + 1..].find(';') {
if semi_rel <= MAX_ENTITY_BODY {
let body = &input[i + 1..i + 1 + semi_rel];
let end = i + 1 + semi_rel; if body.eq_ignore_ascii_case("nbsp") {
out.push_str(&input[i..=end]);
i = end + 1;
continue;
}
let decoded = match body {
"lt" => Some('<'),
"gt" => Some('>'),
"amp" => Some('&'),
"quot" => Some('"'),
"apos" => Some('\''),
_ => decode_numeric_entity(body),
};
if let Some(c) = decoded {
out.push(c);
i = end + 1;
continue;
}
}
}
out.push('&');
i += 1;
} else {
let ch = input[i..].chars().next().unwrap_or('\u{FFFD}');
out.push(ch);
i += ch.len_utf8();
}
}
out
}
#[must_use]
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 = decode_entities(input);
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 mut target = String::with_capacity(input_len);
for (line_idx, line) in final_lines.iter().enumerate() {
if !(line.starts_with(RETURN) || line_idx == 0) {
target.push_str(SPACE);
}
target.push_str(line);
}
target
}
#[must_use]
pub fn parse_bool(input: &str) -> Option<bool> {
match input {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
#[derive(Debug, 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 core::hash::Hasher;
let mut hasher = crate::hash::DefaultHasher::new();
for p in &self.path {
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;
}
cur_pathgroup_scan += 1;
cur_selfgroup_scan += n;
path_group = path_groups[cur_pathgroup_scan].clone();
}
None => return false, }
}
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::{Class, Global, Id, PseudoSelector, Type},
};
for selector in a {
match selector {
Global
| PseudoSelector(
CssPathPseudoSelector::Hover
| CssPathPseudoSelector::Active
| CssPathPseudoSelector::Focus
| CssPathPseudoSelector::SeatFocus,
) => {}
Type(tag) => {
if !b.iter().any(|t| **t == Type(*tag)) {
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.is_multiple_of(2) {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
if idx_in_parent.is_multiple_of(2) {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
if !idx_in_parent
.saturating_sub(p.offset as usize)
.is_multiple_of(p.pattern_repeat as usize)
{
return false;
}
}
_ => return false, }
}
true
}
struct CssBlock {
ending: Option<CssPathPseudoSelector>,
block: CssRuleBlock,
}
#[allow(clippy::result_large_err)] 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() {
for css_block in &css_blocks_for_this_node {
for declaration in css_block.block.declarations.as_ref() {
let prop = match declaration {
CssDeclaration::Static(s) => s,
CssDeclaration::Dynamic(d) => &d.default_value,
};
extra_blocks.insert_from_css_property(prop);
}
}
let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
if !inline_css.is_empty() {
let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
}
let _ = (&mut *css_blocks, matcher_hash); }
if !body_node.children.as_ref().is_empty() {
use azul_css::codegen::format::GetHash;
let children_hash = body_node.children.as_ref().get_hash();
dom_string.push_str("\r\n.with_children(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());
let _ = write!(
dom_string,
"{}{},\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('"', "\\\"");
let _ = write!(dom_string,
"{t}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\"),\r\n"
);
}
}
}
}
let _ = write!(dom_string, "\r\n{t}])");
}
let dom_string = dom_string.trim();
Ok(dom_string.to_string())
}
fn css_blocks_to_inline_string(blocks: &[CssBlock]) -> String {
fn decls_of(block: &CssBlock) -> Vec<String> {
block
.block
.declarations
.as_ref()
.iter()
.map(|d| {
let prop = match d {
CssDeclaration::Static(s) => s,
CssDeclaration::Dynamic(dy) => &dy.default_value,
};
format!("{}: {};", prop.key(), prop.value())
})
.collect()
}
let mut normal: Vec<String> = Vec::new();
let mut pseudo: Vec<String> = Vec::new();
for block in blocks {
let pseudo_sel = match block.ending {
Some(CssPathPseudoSelector::Hover) => Some(":hover"),
Some(CssPathPseudoSelector::Active) => Some(":active"),
Some(CssPathPseudoSelector::Focus) => Some(":focus"),
Some(CssPathPseudoSelector::SeatFocus) => Some(":seat-focus"),
_ => None,
};
match pseudo_sel {
None => normal.extend(decls_of(block)),
Some(sel) => pseudo.push(format!("{} {{ {} }}", sel, decls_of(block).join(" "))),
}
}
let mut parts = normal;
parts.extend(pseudo);
parts.join(" ")
}
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 ending = match css_block.path.selectors.as_ref().last() {
Some(CssPathSelector::PseudoSelector(p)) => Some(p.clone()),
_ => None,
};
blocks.push(CssBlock {
ending,
block: css_block.clone(),
});
}
}
blocks
}
fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
use self::DynamicItem::{Str, Var};
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!(\"{{:{spec}}}\", {var_name}).into()")
} 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 {
let _ = write!(formatted_str, "{{{variable_name}:{spec}}}");
} else {
let _ = write!(formatted_str, "{{{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)
}
#[allow(clippy::result_large_err)]
#[allow(clippy::only_used_in_recursion)]
#[allow(clippy::too_many_lines)] fn compile_node_to_rust_code_inner(
node: &XmlNode,
component_map: &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 node_type_tag = tag_to_node_type_tag(&component_name);
let node_type = CssPathSelector::Type(node_type_tag);
let ctor = analyze_node_ctor(&component_name, node);
let mut dom_string = ctor.render_rust().map_or_else(
|| {
let tag = safe_container_tag(&format!("{:?}", tag_to_node_type(&component_name)));
format!("{t2}Dom::create_node(NodeType::{tag})")
},
|expr| format!("{t2}{expr}"),
);
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() {
for css_block in &css_blocks_for_this_node {
for declaration in css_block.block.declarations.as_ref() {
let prop = match declaration {
CssDeclaration::Static(s) => s,
CssDeclaration::Dynamic(d) => &d.default_value,
};
extra_blocks.insert_from_css_property(prop);
}
}
let inline_css = css_blocks_to_inline_string(&css_blocks_for_this_node);
if !inline_css.is_empty() {
let escaped = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
let _ = write!(dom_string, "\r\n{t2}.with_css(\"{escaped}\")");
}
let _ = (&mut *css_blocks, matcher_hash); }
set_stringified_attributes(
&mut dom_string,
&node.attributes,
&ComponentArgumentVec::new(),
tabs,
);
let mut caption_skipped = false;
let mut children_string = node
.children
.as_ref()
.iter()
.enumerate()
.filter_map(|(child_idx, c)| match c {
XmlNodeChild::Element(child_node) => {
if ctor.skip_caption()
&& !caption_skipped
&& child_node
.node_type
.as_str()
.eq_ignore_ascii_case("caption")
{
caption_skipped = true;
return None;
}
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) => {
if ctor.consumes_text() {
return None;
}
let text = text.trim();
if text.is_empty() {
None
} else {
let t2 = String::from(" ").repeat(tabs);
let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
Some(Ok(format!(
"{t2}Dom::create_text_do_not_use_without_block_level_wrapper(\"{escaped}\")"
)))
}
}
})
.collect::<Result<Vec<_>, _>>()?
.join(",\r\n");
if !children_string.is_empty() {
let _ = write!(
dom_string,
"\r\n{t2}.with_children(vec![\r\n{children_string}\r\n{t2}])"
);
}
Ok(dom_string)
}
const SAFE_CONTAINER_TAGS: &[&str] = &[
"Abbr",
"Acronym",
"Address",
"Article",
"Aside",
"B",
"Bdi",
"Bdo",
"Big",
"BlockQuote",
"Body",
"Br",
"Caption",
"Cite",
"Code",
"ColGroup",
"Dd",
"Del",
"Dfn",
"Dir",
"Div",
"Dl",
"Dt",
"Em",
"Embed",
"FigCaption",
"Figure",
"Footer",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"Head",
"Header",
"Hr",
"Html",
"I",
"Ins",
"Kbd",
"Li",
"Link",
"Main",
"Map",
"Mark",
"Meta",
"Nav",
"Object",
"Ol",
"P",
"Pre",
"Q",
"Rp",
"Rt",
"Rtc",
"Ruby",
"S",
"Samp",
"Script",
"Section",
"Small",
"Span",
"Strong",
"Style",
"Sub",
"Sup",
"Svg",
"TBody",
"Td",
"TFoot",
"Th",
"THead",
"Title",
"Tr",
"U",
"Ul",
"Var",
"Wbr",
];
fn safe_container_tag(tag_dbg: &str) -> &'static str {
SAFE_CONTAINER_TAGS
.iter()
.copied()
.find(|t| *t == tag_dbg)
.unwrap_or("Div")
}
#[derive(Debug, Clone)]
enum CtorArg {
Str(String),
Aria(String),
Float(f32),
OptSome(String),
OptNone,
}
enum NodeCtor {
Plain,
Semantic {
suffix: String,
args: Vec<CtorArg>,
consumes_text: bool,
skip_caption: bool,
},
}
fn cap_first(tag: &str) -> String {
let mut c = tag.chars();
c.next().map_or_else(String::new, |f| {
f.to_uppercase().collect::<String>() + c.as_str()
})
}
fn camel_to_snake(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::new();
for (i, &ch) in chars.iter().enumerate() {
if ch.is_ascii_uppercase() && i > 0 {
let prev = chars[i - 1];
let next_lower = chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
if prev.is_ascii_lowercase()
|| prev.is_ascii_digit()
|| (prev.is_ascii_uppercase() && next_lower)
{
out.push('_');
}
}
out.extend(ch.to_lowercase());
}
out
}
fn esc_lit(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn fmt_f32_lit(f: f32) -> String {
let s = format!("{f}");
if s.contains('.') || s.contains('e') || s.contains("inf") || s.contains("NaN") {
s
} else {
format!("{s}.0")
}
}
fn node_direct_text(node: &XmlNode) -> String {
node.children
.as_ref()
.iter()
.filter_map(|c| match c {
XmlNodeChild::Text(t) => {
let t = t.trim();
if t.is_empty() {
None
} else {
Some(t.to_string())
}
}
XmlNodeChild::Element(_) => None,
})
.collect::<Vec<_>>()
.join(" ")
}
fn node_aria_label(node: &XmlNode) -> Option<String> {
node.attributes.get_key("aria-label").and_then(|v| {
let v = v.as_str().trim();
if v.is_empty() {
None
} else {
Some(v.to_string())
}
})
}
fn node_attr_or(node: &XmlNode, key: &str, default: &str) -> String {
node.attributes
.get_key(key)
.map_or_else(|| default.to_string(), |v| v.as_str().to_string())
}
fn node_attr_f32(node: &XmlNode, key: &str, default: f32) -> f32 {
node.attributes
.get_key(key)
.and_then(|v| v.as_str().trim().parse::<f32>().ok())
.unwrap_or(default)
}
fn first_caption_text(node: &XmlNode) -> Option<String> {
node.children.as_ref().iter().find_map(|c| match c {
XmlNodeChild::Element(e) if e.node_type.as_str().eq_ignore_ascii_case("caption") => {
let t = e.get_text_content();
let t = t.trim();
if t.is_empty() {
None
} else {
Some(t.to_string())
}
}
_ => None,
})
}
const WITH_TEXT_TAGS: &[&str] = &[
"acronym",
"b",
"bdi",
"bdo",
"big",
"blockquote",
"cite",
"code",
"del",
"dfn",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"i",
"ins",
"kbd",
"li",
"mark",
"p",
"pre",
"rp",
"rt",
"s",
"samp",
"small",
"span",
"strong",
"style",
"sub",
"sup",
"td",
"th",
"title",
"u",
"var",
];
#[allow(clippy::too_many_lines)] fn analyze_node_ctor(tag: &str, node: &XmlNode) -> NodeCtor {
fn sem(suffix: impl Into<String>, args: Vec<CtorArg>, consumes_text: bool) -> NodeCtor {
NodeCtor::Semantic {
suffix: suffix.into(),
args,
consumes_text,
skip_caption: false,
}
}
let aria = node_aria_label(node);
let has_aria = aria.is_some();
let label = aria.unwrap_or_default();
let pure_text = node.has_only_text_children();
let text = node_direct_text(node);
let has_text = !text.is_empty();
let cap = cap_first(tag);
if WITH_TEXT_TAGS.contains(&tag) {
if pure_text && has_text {
return sem(format!("{cap}WithText"), vec![CtorArg::Str(text)], true);
}
return NodeCtor::Plain;
}
match tag {
"details" | "form" | "fieldset" | "legend" | "menu" | "output" | "datalist" | "canvas"
| "audio" | "video" | "area" => {
if has_aria {
sem(cap, vec![CtorArg::Aria(label)], false)
} else {
sem(format!("{cap}NoA11y"), vec![], false)
}
}
"summary" => {
if pure_text && has_text {
if has_aria {
sem(
"SummaryWithText",
vec![CtorArg::Str(text), CtorArg::Aria(label)],
true,
)
} else {
sem("SummaryWithTextNoA11y", vec![CtorArg::Str(text)], true)
}
} else if has_aria {
sem("Summary", vec![CtorArg::Aria(label)], false)
} else {
sem("SummaryNoA11y", vec![], false)
}
}
"button" => {
if has_aria {
sem(
"Button",
vec![CtorArg::Str(text), CtorArg::Aria(label)],
true,
)
} else {
sem("ButtonNoA11y", vec![CtorArg::Str(text)], true)
}
}
"a" => {
let href = node_attr_or(node, "href", "");
if has_aria {
sem(
"A",
vec![CtorArg::Str(href), CtorArg::Str(text), CtorArg::Aria(label)],
true,
)
} else {
let lbl = if has_text {
CtorArg::OptSome(text)
} else {
CtorArg::OptNone
};
sem("ANoA11y", vec![CtorArg::Str(href), lbl], true)
}
}
"label" => {
let for_id = node_attr_or(node, "for", "");
if has_aria {
sem(
"Label",
vec![
CtorArg::Str(for_id),
CtorArg::Str(text),
CtorArg::Aria(label),
],
true,
)
} else {
sem(
"LabelNoA11y",
vec![CtorArg::Str(for_id), CtorArg::Str(text)],
true,
)
}
}
"input" => {
let ty = node_attr_or(node, "type", "text");
let name = node_attr_or(node, "name", "");
if has_aria {
sem(
"Input",
vec![
CtorArg::Str(ty),
CtorArg::Str(name),
CtorArg::Str(label.clone()),
CtorArg::Aria(label),
],
false,
)
} else {
sem(
"InputNoA11y",
vec![CtorArg::Str(ty), CtorArg::Str(name), CtorArg::Str(label)],
false,
)
}
}
"textarea" => {
let name = node_attr_or(node, "name", "");
if has_aria {
sem(
"Textarea",
vec![
CtorArg::Str(name),
CtorArg::Str(label.clone()),
CtorArg::Aria(label),
],
false,
)
} else {
sem(
"TextareaNoA11y",
vec![CtorArg::Str(name), CtorArg::Str(label)],
false,
)
}
}
"select" => {
let name = node_attr_or(node, "name", "");
if has_aria {
sem(
"Select",
vec![
CtorArg::Str(name),
CtorArg::Str(label.clone()),
CtorArg::Aria(label),
],
false,
)
} else {
sem(
"SelectNoA11y",
vec![CtorArg::Str(name), CtorArg::Str(label)],
false,
)
}
}
"option" => {
let value = node_attr_or(node, "value", "");
if has_aria {
sem(
"Option",
vec![
CtorArg::Str(value),
CtorArg::Str(text),
CtorArg::Aria(label),
],
true,
)
} else {
sem(
"OptionNoA11y",
vec![CtorArg::Str(value), CtorArg::Str(text)],
true,
)
}
}
"optgroup" => {
let lbl = node_attr_or(node, "label", "");
if has_aria {
sem(
"Optgroup",
vec![CtorArg::Str(lbl), CtorArg::Aria(label)],
false,
)
} else {
sem("OptgroupNoA11y", vec![CtorArg::Str(lbl)], false)
}
}
"table" => {
if has_aria {
let caption = first_caption_text(node).unwrap_or_else(|| label.clone());
NodeCtor::Semantic {
suffix: "Table".to_string(),
args: vec![CtorArg::Str(caption), CtorArg::Aria(label)],
consumes_text: false,
skip_caption: true,
}
} else {
sem("TableNoA11y", vec![], false)
}
}
"progress" => sem(
"ProgressNoA11y",
vec![
CtorArg::Float(node_attr_f32(node, "value", 0.0)),
CtorArg::Float(node_attr_f32(node, "max", 1.0)),
],
false,
),
"meter" => sem(
"MeterNoA11y",
vec![
CtorArg::Float(node_attr_f32(node, "value", 0.0)),
CtorArg::Float(node_attr_f32(node, "min", 0.0)),
CtorArg::Float(node_attr_f32(node, "max", 1.0)),
],
false,
),
"dialog" => sem("DialogNoA11y", vec![], false),
_ => NodeCtor::Plain,
}
}
impl CtorArg {
fn render_rust(&self) -> String {
match self {
Self::Str(s) => format!("AzString::from(\"{}\")", esc_lit(s)),
Self::Aria(s) => format!("SmallAriaInfo::label(AzString::from(\"{}\"))", esc_lit(s)),
Self::Float(f) => fmt_f32_lit(*f),
Self::OptSome(s) => format!("OptionString::Some(AzString::from(\"{}\"))", esc_lit(s)),
Self::OptNone => "OptionString::None".to_string(),
}
}
fn render_c(&self) -> String {
match self {
Self::Str(s) => format!("AZ_STR(\"{}\")", esc_lit(s)),
Self::Aria(s) => format!("AzSmallAriaInfo_label(AZ_STR(\"{}\"))", esc_lit(s)),
Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
Self::OptSome(s) => format!("AzOptionString_some(AZ_STR(\"{}\"))", esc_lit(s)),
Self::OptNone => "AzOptionString_none()".to_string(),
}
}
fn render_cpp(&self) -> String {
match self {
Self::Str(s) => format!("String(\"{}\")", esc_lit(s)),
Self::Aria(s) => format!("SmallAriaInfo::label(String(\"{}\"))", esc_lit(s)),
Self::Float(f) => format!("{}f", fmt_f32_lit(*f)),
Self::OptSome(s) => format!("OptionString::some(String(\"{}\"))", esc_lit(s)),
Self::OptNone => "OptionString::none()".to_string(),
}
}
fn render_python(&self) -> String {
match self {
Self::Str(s) => format!("\"{}\"", esc_lit(s)),
Self::Aria(s) => format!("azul.SmallAriaInfo.label(\"{}\")", esc_lit(s)),
Self::Float(f) => fmt_f32_lit(*f),
Self::OptSome(s) => format!("azul.OptionString.some(\"{}\")", esc_lit(s)),
Self::OptNone => "azul.OptionString.none()".to_string(),
}
}
}
impl NodeCtor {
const fn consumes_text(&self) -> bool {
matches!(
self,
Self::Semantic {
consumes_text: true,
..
}
)
}
const fn skip_caption(&self) -> bool {
matches!(
self,
Self::Semantic {
skip_caption: true,
..
}
)
}
fn render_rust(&self) -> Option<String> {
match self {
Self::Plain => None,
Self::Semantic { suffix, args, .. } => Some(format!(
"Dom::create_{}({})",
camel_to_snake(suffix),
args.iter()
.map(CtorArg::render_rust)
.collect::<Vec<_>>()
.join(", ")
)),
}
}
fn render_c(&self) -> Option<String> {
match self {
Self::Plain => None,
Self::Semantic { suffix, args, .. } => Some(format!(
"AzDom_create{}({})",
suffix,
args.iter()
.map(CtorArg::render_c)
.collect::<Vec<_>>()
.join(", ")
)),
}
}
fn render_fluent(&self, target: &CompileTarget) -> Option<String> {
match self {
Self::Plain => None,
Self::Semantic { suffix, args, .. } => {
let snake = camel_to_snake(suffix);
let (prefix, rendered) = match target {
CompileTarget::Cpp => (
format!("Dom::create_{snake}"),
args.iter().map(CtorArg::render_cpp).collect::<Vec<_>>(),
),
CompileTarget::Python => (
format!("azul.Dom.create_{snake}"),
args.iter().map(CtorArg::render_python).collect::<Vec<_>>(),
),
_ => return None,
};
Some(format!("{}({})", prefix, rendered.join(", ")))
}
}
}
}
struct FluentSyntax {
target: CompileTarget,
create_node: fn(&str) -> String,
create_text: fn(&str) -> String,
with_css: fn(&str) -> String,
with_class: fn(&str) -> String,
with_id: fn(&str) -> String,
with_child: fn(&str) -> String,
}
const CPP_SYNTAX: FluentSyntax = FluentSyntax {
target: CompileTarget::Cpp,
create_node: |tag| alloc::format!("Dom::create_{}()", tag.to_lowercase()),
create_text: |s| {
alloc::format!("Dom::create_text_do_not_use_without_block_level_wrapper(String(\"{s}\"))")
},
with_css: |s| alloc::format!(".with_css(String(\"{s}\"))"),
with_class: |s| alloc::format!(".with_class(String(\"{s}\"))"),
with_id: |s| alloc::format!(".with_id(String(\"{s}\"))"),
with_child: |c| alloc::format!(".with_child({c})"),
};
const PYTHON_SYNTAX: FluentSyntax = FluentSyntax {
target: CompileTarget::Python,
create_node: |tag| alloc::format!("azul.Dom.create_{}()", tag.to_lowercase()),
create_text: |s| {
alloc::format!("azul.Dom.create_text_do_not_use_without_block_level_wrapper(\"{s}\")")
},
with_css: |s| alloc::format!(".with_css(\"{s}\")"),
with_class: |s| alloc::format!(".with_class(\"{s}\")"),
with_id: |s| alloc::format!(".with_id(\"{s}\")"),
with_child: |c| alloc::format!(".with_child({c})"),
};
#[allow(clippy::result_large_err)]
#[allow(clippy::only_used_in_recursion)]
fn compile_node_fluent(
node: &XmlNode,
syntax: &FluentSyntax,
component_map: &ComponentMap,
css: &Css,
mut matcher: CssMatcher,
) -> Result<String, CompileError> {
use azul_css::css::CssDeclaration;
let component_name = normalize_casing(&node.node_type);
let node_type_tag = tag_to_node_type_tag(&component_name);
let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
let ctor = analyze_node_ctor(&component_name, node);
let mut s = ctor.render_fluent(&syntax.target).map_or_else(
|| (syntax.create_node)(safe_container_tag(&tag_dbg)),
|expr| expr,
);
matcher.path.push(CssPathSelector::Type(node_type_tag));
let ids: Vec<String> = node
.attributes
.get_key("id")
.map(|v| {
v.split_whitespace()
.map(alloc::string::ToString::to_string)
.collect()
})
.unwrap_or_default();
matcher
.path
.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
let classes: Vec<String> = node
.attributes
.get_key("class")
.map(|v| {
v.split_whitespace()
.map(alloc::string::ToString::to_string)
.collect()
})
.unwrap_or_default();
matcher.path.extend(
classes
.iter()
.map(|c| CssPathSelector::Class(c.clone().into())),
);
let blocks = get_css_blocks(css, &matcher);
if !blocks.is_empty() {
let inline_css = css_blocks_to_inline_string(&blocks);
if !inline_css.is_empty() {
let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
s.push_str(&(syntax.with_css)(&esc));
}
}
for id in &ids {
s.push_str(&(syntax.with_id)(
&id.replace('\\', "\\\\").replace('"', "\\\""),
));
}
for class in &classes {
s.push_str(&(syntax.with_class)(
&class.replace('\\', "\\\\").replace('"', "\\\""),
));
}
let mut caption_skipped = false;
for (child_idx, child) in node.children.as_ref().iter().enumerate() {
match child {
XmlNodeChild::Element(child_node) => {
if ctor.skip_caption()
&& !caption_skipped
&& child_node
.node_type
.as_str()
.eq_ignore_ascii_case("caption")
{
caption_skipped = true;
continue;
}
let mut m = matcher.clone();
m.path.push(CssPathSelector::Children);
m.indices_in_parent.push(child_idx);
m.children_length.push(node.children.len());
let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
s.push_str(&(syntax.with_child)(&child_src));
}
XmlNodeChild::Text(text) => {
if ctor.consumes_text() {
continue;
}
let text = text.trim();
if !text.is_empty() {
let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
}
}
}
}
Ok(s)
}
#[allow(clippy::result_large_err)] fn compile_body_fluent<'a>(
body_node: &'a XmlNode,
syntax: &FluentSyntax,
component_map: &'a ComponentMap,
css: &Css,
mut matcher: CssMatcher,
) -> Result<String, CompileError> {
let mut s = (syntax.create_node)("Body");
matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
let classes: Vec<String> = body_node
.attributes
.get_key("class")
.map(|v| {
v.split_whitespace()
.map(alloc::string::ToString::to_string)
.collect()
})
.unwrap_or_default();
matcher.path.extend(
classes
.iter()
.map(|c| CssPathSelector::Class(c.clone().into())),
);
let blocks = get_css_blocks(css, &matcher);
if !blocks.is_empty() {
let inline_css = css_blocks_to_inline_string(&blocks);
if !inline_css.is_empty() {
let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
s.push_str(&(syntax.with_css)(&esc));
}
}
for class in &classes {
s.push_str(&(syntax.with_class)(
&class.replace('\\', "\\\\").replace('"', "\\\""),
));
}
for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
match child {
XmlNodeChild::Element(child_node) => {
let mut m = matcher.clone();
m.path.push(CssPathSelector::Children);
m.indices_in_parent.push(child_idx);
m.children_length.push(body_node.children.len());
let child_src = compile_node_fluent(child_node, syntax, component_map, css, m)?;
s.push_str(&(syntax.with_child)(&child_src));
}
XmlNodeChild::Text(text) => {
let text = text.trim();
if !text.is_empty() {
let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
s.push_str(&(syntax.with_child)(&(syntax.create_text)(&esc)));
}
}
}
}
Ok(s)
}
#[allow(clippy::result_large_err)] fn parse_page_style_and_body(root_nodes: &[XmlNodeChild]) -> Result<(Css, XmlNode), CompileError> {
let html_node = get_html_node(root_nodes)?;
let body_node = get_body_node(html_node.children.as_ref())?.clone();
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() {
global_style = azul_css::parser2::new_from_str(&text).0;
}
}
}
global_style.sort_by_specificity();
Ok((global_style, body_node))
}
fn body_matcher(body_node: &XmlNode) -> CssMatcher {
CssMatcher {
path: Vec::new(),
indices_in_parent: vec![0],
children_length: vec![body_node.children.as_ref().len()],
}
}
#[allow(clippy::result_large_err)] pub fn str_to_cpp_code<'a>(
root_nodes: &'a [XmlNodeChild],
component_map: &'a ComponentMap,
) -> Result<String, CompileError> {
let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
let body_node = &body_node;
let render = compile_body_fluent(
body_node,
&CPP_SYNTAX,
component_map,
&global_style,
body_matcher(body_node),
)?;
Ok(alloc::format!(
"// Auto-generated UI source code (C++). Build:\n\
// clang++ -std=c++20 -I <azul>/target/codegen main.cpp -lazul\n\
#include \"azul20.hpp\"\n\
using namespace azul;\n\n\
struct Data {{}};\n\n\
AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n \
return {render};\n}}\n\n\
int main() {{\n \
RefAny data = RefAny::create(Data{{}});\n \
WindowCreateOptions window = WindowCreateOptions::create(render);\n \
App app = App::create(std::move(data), AppConfig::default_());\n \
app.run(std::move(window));\n \
return 0;\n}}\n"
))
}
#[allow(clippy::result_large_err)] pub fn str_to_python_code<'a>(
root_nodes: &'a [XmlNodeChild],
component_map: &'a ComponentMap,
) -> Result<String, CompileError> {
let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
let body_node = &body_node;
let render = compile_body_fluent(
body_node,
&PYTHON_SYNTAX,
component_map,
&global_style,
body_matcher(body_node),
)?;
Ok(alloc::format!(
"# Auto-generated UI source code (Python). Run: python3 main.py\n\
import azul\n\n\
class Data:\n pass\n\n\
def render(data, info):\n return (\n {}\n )\n\n\
def main():\n \
app = azul.App.create(Data(), azul.AppConfig.create())\n \
window = azul.WindowCreateOptions.create(render)\n \
app.run(window)\n\n\
if __name__ == \"__main__\":\n main()\n",
render.replace("\r\n", "\n ")
))
}
fn c_creator_suffix(tag_dbg: &str) -> String {
let mut chars = tag_dbg.chars();
chars.next().map_or_else(
|| "Div".to_string(),
|first| {
let rest: String = chars.as_str().to_lowercase();
alloc::format!("{first}{rest}")
},
)
}
#[allow(clippy::result_large_err)] #[allow(clippy::too_many_lines)] fn compile_node_c(
node: &XmlNode,
component_map: &ComponentMap,
css: &Css,
mut matcher: CssMatcher,
counter: &mut usize,
out: &mut String,
) -> Result<String, CompileError> {
let _ = component_map;
let component_name = normalize_casing(&node.node_type);
let node_type_tag = tag_to_node_type_tag(&component_name);
let tag_dbg = alloc::format!("{:?}", tag_to_node_type(&component_name));
let var = alloc::format!("n{}", *counter);
*counter += 1;
let ctor = analyze_node_ctor(&component_name, node);
match ctor.render_c() {
Some(expr) => {
let _ = writeln!(out, " AzDom {var} = {expr};");
}
None => {
let _ = writeln!(
out,
" AzDom {} = AzDom_create{}();",
var,
c_creator_suffix(safe_container_tag(&tag_dbg))
);
}
}
matcher.path.push(CssPathSelector::Type(node_type_tag));
let ids: Vec<String> = node
.attributes
.get_key("id")
.map(|v| {
v.split_whitespace()
.map(alloc::string::ToString::to_string)
.collect()
})
.unwrap_or_default();
matcher
.path
.extend(ids.iter().map(|id| CssPathSelector::Id(id.clone().into())));
let classes: Vec<String> = node
.attributes
.get_key("class")
.map(|v| {
v.split_whitespace()
.map(alloc::string::ToString::to_string)
.collect()
})
.unwrap_or_default();
matcher.path.extend(
classes
.iter()
.map(|c| CssPathSelector::Class(c.clone().into())),
);
let blocks = get_css_blocks(css, &matcher);
if !blocks.is_empty() {
let inline_css = css_blocks_to_inline_string(&blocks);
if !inline_css.is_empty() {
let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(out, " {var} = AzDom_withCss({var}, AZ_STR(\"{esc}\"));");
}
}
for id in &ids {
let esc = id.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(out, " {var} = AzDom_withId({var}, AZ_STR(\"{esc}\"));");
}
for class in &classes {
let esc = class.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(
out,
" {var} = AzDom_withClass({var}, AZ_STR(\"{esc}\"));"
);
}
let mut caption_skipped = false;
for (child_idx, child) in node.children.as_ref().iter().enumerate() {
match child {
XmlNodeChild::Element(child_node) => {
if ctor.skip_caption()
&& !caption_skipped
&& child_node
.node_type
.as_str()
.eq_ignore_ascii_case("caption")
{
caption_skipped = true;
continue;
}
let mut m = matcher.clone();
m.path.push(CssPathSelector::Children);
m.indices_in_parent.push(child_idx);
m.children_length.push(node.children.len());
let child_var = compile_node_c(child_node, component_map, css, m, counter, out)?;
let _ = writeln!(out, " AzDom_addChild(&{var}, {child_var});");
}
XmlNodeChild::Text(text) => {
if ctor.consumes_text() {
continue;
}
let text = text.trim();
if !text.is_empty() {
let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(out,
" AzDom_addChild(&{var}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
);
}
}
}
}
Ok(var)
}
#[allow(clippy::result_large_err)] pub fn str_to_c_code<'a>(
root_nodes: &'a [XmlNodeChild],
component_map: &'a ComponentMap,
) -> Result<String, CompileError> {
let (global_style, body_node) = parse_page_style_and_body(root_nodes)?;
let body_node = &body_node;
let mut body = String::new();
let mut counter = 0usize;
let root = alloc::format!("n{counter}");
counter += 1;
let _ = writeln!(body, " AzDom {root} = AzDom_createBody();");
let mut matcher = body_matcher(body_node);
matcher.path.push(CssPathSelector::Type(NodeTypeTag::Body));
let classes: Vec<String> = body_node
.attributes
.get_key("class")
.map(|v| {
v.split_whitespace()
.map(alloc::string::ToString::to_string)
.collect()
})
.unwrap_or_default();
matcher.path.extend(
classes
.iter()
.map(|c| CssPathSelector::Class(c.clone().into())),
);
let blocks = get_css_blocks(&global_style, &matcher);
if !blocks.is_empty() {
let inline_css = css_blocks_to_inline_string(&blocks);
if !inline_css.is_empty() {
let esc = inline_css.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(
body,
" {root} = AzDom_withCss({root}, AZ_STR(\"{esc}\"));"
);
}
}
for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
match child {
XmlNodeChild::Element(child_node) => {
let mut m = matcher.clone();
m.path.push(CssPathSelector::Children);
m.indices_in_parent.push(child_idx);
m.children_length.push(body_node.children.len());
let child_var = compile_node_c(
child_node,
component_map,
&global_style,
m,
&mut counter,
&mut body,
)?;
let _ = writeln!(body, " AzDom_addChild(&{root}, {child_var});");
}
XmlNodeChild::Text(text) => {
let text = text.trim();
if !text.is_empty() {
let esc = text.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(body,
" AzDom_addChild(&{root}, AzDom_createTextDoNotUseWithoutBlockLevelWrapper(AZ_STR(\"{esc}\")));"
);
}
}
}
}
Ok(alloc::format!(
"/* Auto-generated UI source code (C). Build:\n\
* clang -I <azul>/target/codegen main.c -lazul\n */\n\
#include \"azul.h\"\n\
#include <string.h>\n\
#define AZ_STR(s) AzString_copyFromBytes((const uint8_t*)(s), 0, strlen(s))\n\n\
AzDom render(AzRefAny data, AzLayoutCallbackInfo info) {{\n\
{body} return {root};\n}}\n\n\
int main(void) {{\n \
AzString data_type = AZ_STR(\"Data\");\n \
AzRefAny data = AzRefAny_newC((AzGlVoidPtrConst){{ .ptr = NULL }}, 0, 1, 0, data_type, NULL, 0, 0);\n \
AzApp app = AzApp_create(data, AzAppConfig_create());\n \
AzWindowCreateOptions window = AzWindowCreateOptions_create(render);\n \
AzApp_run(&app, window);\n \
AzApp_delete(&app);\n \
return 0;\n}}\n"
))
}
#[cfg(test)]
#[path = "xml_test.rs"]
mod xml_test;