use alloc::{
boxed::Box,
collections::BTreeMap,
string::{String, ToString},
vec::Vec,
};
use core::{fmt, hash::Hash};
use azul_css::{
css::{
Css, CssDeclaration, CssPath, CssPathPseudoSelector, CssPathSelector, CssRuleBlock,
NodeTypeTag,
},
format_rust_code::VecContents,
parser2::{CssParseErrorOwned, ErrorLocation},
props::{
basic::StyleFontFamilyVec,
property::CssProperty,
style::{
NormalizedLinearColorStopVec, NormalizedRadialColorStopVec, StyleBackgroundContentVec,
StyleBackgroundPositionVec, StyleBackgroundRepeatVec, StyleBackgroundSizeVec,
StyleTransformVec,
},
},
AzString, OptionString, U8Vec,
};
use crate::{
dom::{Dom, NodeType},
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
}
}
pub type ComponentArgumentName = String;
pub type ComponentArgumentType = String;
pub type ComponentArgumentOrder = usize;
pub type ComponentArgumentTypes = Vec<(ComponentArgumentName, ComponentArgumentType)>;
pub type ComponentName = String;
pub type CompiledComponent = String;
pub const DEFAULT_ARGS: [&str; 8] = [
"id",
"class",
"tabindex",
"focusable",
"accepts_text",
"name",
"style",
"args",
];
#[allow(non_camel_case_types)]
pub enum c_void {}
#[repr(C)]
pub enum XmlNodeType {
Root,
Element,
PI,
Comment,
Text,
}
#[repr(C)]
pub struct XmlQualifiedName {
pub local_name: AzString,
pub namespace: OptionString,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum ExternalResourceKind {
Image,
Font,
Stylesheet,
Script,
Icon,
Video,
Audio,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct MimeTypeHint {
pub inner: AzString,
}
impl MimeTypeHint {
pub fn new(s: &str) -> Self {
Self { inner: AzString::from(s) }
}
pub fn from_extension(ext: &str) -> Self {
let mime = match ext.to_lowercase().as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"webp" => "image/webp",
"svg" => "image/svg+xml",
"ico" => "image/x-icon",
"bmp" => "image/bmp",
"avif" => "image/avif",
"ttf" => "font/ttf",
"otf" => "font/otf",
"woff" => "font/woff",
"woff2" => "font/woff2",
"eot" => "application/vnd.ms-fontobject",
"css" => "text/css",
"js" => "application/javascript",
"mjs" => "application/javascript",
"mp4" => "video/mp4",
"webm" => "video/webm",
"ogg" => "video/ogg",
"mp3" => "audio/mpeg",
"wav" => "audio/wav",
"flac" => "audio/flac",
_ => "application/octet-stream",
};
Self { inner: AzString::from(mime) }
}
}
impl_option!(
MimeTypeHint,
OptionMimeTypeHint,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct ExternalResource {
pub url: AzString,
pub kind: ExternalResourceKind,
pub mime_type: OptionMimeTypeHint,
pub source_element: AzString,
pub source_attribute: AzString,
}
impl_option!(
ExternalResource,
OptionExternalResource,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec!(ExternalResource, ExternalResourceVec, ExternalResourceVecDestructor, ExternalResourceVecDestructorType, ExternalResourceVecSlice, OptionExternalResource);
impl_vec_mut!(ExternalResource, ExternalResourceVec);
impl_vec_debug!(ExternalResource, ExternalResourceVec);
impl_vec_partialeq!(ExternalResource, ExternalResourceVec);
impl_vec_eq!(ExternalResource, ExternalResourceVec);
impl_vec_partialord!(ExternalResource, ExternalResourceVec);
impl_vec_ord!(ExternalResource, ExternalResourceVec);
impl_vec_hash!(ExternalResource, ExternalResourceVec);
impl_vec_clone!(ExternalResource, ExternalResourceVec, ExternalResourceVecDestructor);
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct Xml {
pub root: XmlNodeChildVec,
}
impl Xml {
pub fn scan_external_resources(&self) -> ExternalResourceVec {
let mut resources = Vec::new();
for child in self.root.as_ref().iter() {
Self::scan_node_child(child, &mut resources);
}
resources.into()
}
fn scan_node_child(child: &XmlNodeChild, resources: &mut Vec<ExternalResource>) {
match child {
XmlNodeChild::Text(text) => {
Self::extract_css_urls(text.as_str(), resources);
}
XmlNodeChild::Element(node) => {
Self::scan_node(node, resources);
}
}
}
fn scan_node(node: &XmlNode, resources: &mut Vec<ExternalResource>) {
let tag_name = node.node_type.inner.as_str().to_lowercase();
let get_attr = |name: &str| -> Option<String> {
node.attributes.inner.as_ref().iter()
.find(|pair| pair.key.as_str().eq_ignore_ascii_case(name))
.map(|pair| pair.value.as_str().to_string())
};
match tag_name.as_str() {
"img" => {
if let Some(src) = get_attr("src") {
let mime = Self::guess_mime_from_url(&src, "image");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("img"),
source_attribute: AzString::from("src"),
});
}
if let Some(srcset) = get_attr("srcset") {
for src in Self::parse_srcset(&srcset) {
let mime = Self::guess_mime_from_url(&src, "image");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("img"),
source_attribute: AzString::from("srcset"),
});
}
}
}
"link" => {
if let Some(href) = get_attr("href") {
let rel = get_attr("rel").unwrap_or_default().to_lowercase();
let type_attr = get_attr("type");
let as_attr = get_attr("as").unwrap_or_default().to_lowercase();
let (kind, category) = if rel.contains("stylesheet") {
(ExternalResourceKind::Stylesheet, "stylesheet")
} else if rel.contains("icon") || rel.contains("apple-touch-icon") {
(ExternalResourceKind::Icon, "image")
} else if as_attr == "font" || rel.contains("preload") && as_attr == "font" {
(ExternalResourceKind::Font, "font")
} else if as_attr == "script" {
(ExternalResourceKind::Script, "script")
} else if as_attr == "image" {
(ExternalResourceKind::Image, "image")
} else {
(ExternalResourceKind::Unknown, "")
};
let mime = type_attr.map(|t| MimeTypeHint::new(&t))
.or_else(|| Self::guess_mime_from_url(&href, category));
resources.push(ExternalResource {
url: AzString::from(href),
kind,
mime_type: mime.into(),
source_element: AzString::from("link"),
source_attribute: AzString::from("href"),
});
}
}
"script" => {
if let Some(src) = get_attr("src") {
let type_attr = get_attr("type");
let mime = type_attr.map(|t| MimeTypeHint::new(&t))
.or_else(|| Some(MimeTypeHint::new("application/javascript")));
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Script,
mime_type: mime.into(),
source_element: AzString::from("script"),
source_attribute: AzString::from("src"),
});
}
}
"video" => {
if let Some(src) = get_attr("src") {
let mime = Self::guess_mime_from_url(&src, "video");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Video,
mime_type: mime.into(),
source_element: AzString::from("video"),
source_attribute: AzString::from("src"),
});
}
if let Some(poster) = get_attr("poster") {
let mime = Self::guess_mime_from_url(&poster, "image");
resources.push(ExternalResource {
url: AzString::from(poster),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("video"),
source_attribute: AzString::from("poster"),
});
}
}
"audio" => {
if let Some(src) = get_attr("src") {
let mime = Self::guess_mime_from_url(&src, "audio");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Audio,
mime_type: mime.into(),
source_element: AzString::from("audio"),
source_attribute: AzString::from("src"),
});
}
}
"source" => {
if let Some(src) = get_attr("src") {
let type_attr = get_attr("type");
let kind = if type_attr.as_ref().map(|t| t.starts_with("audio")).unwrap_or(false) {
ExternalResourceKind::Audio
} else {
ExternalResourceKind::Video
};
let mime = type_attr.map(|t| MimeTypeHint::new(&t))
.or_else(|| Self::guess_mime_from_url(&src, if kind == ExternalResourceKind::Audio { "audio" } else { "video" }));
resources.push(ExternalResource {
url: AzString::from(src),
kind,
mime_type: mime.into(),
source_element: AzString::from("source"),
source_attribute: AzString::from("src"),
});
}
if let Some(srcset) = get_attr("srcset") {
for src in Self::parse_srcset(&srcset) {
let mime = Self::guess_mime_from_url(&src, "image");
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from("source"),
source_attribute: AzString::from("srcset"),
});
}
}
}
"a" => {
if let Some(href) = get_attr("href") {
if Self::looks_like_resource(&href) {
let mime = Self::guess_mime_from_url(&href, "");
resources.push(ExternalResource {
url: AzString::from(href),
kind: ExternalResourceKind::Unknown,
mime_type: mime.into(),
source_element: AzString::from("a"),
source_attribute: AzString::from("href"),
});
}
}
}
"iframe" | "embed" | "object" => {
let src_attr = if tag_name == "object" { "data" } else { "src" };
if let Some(src) = get_attr(src_attr) {
resources.push(ExternalResource {
url: AzString::from(src),
kind: ExternalResourceKind::Unknown,
mime_type: OptionMimeTypeHint::None,
source_element: AzString::from(tag_name.clone()),
source_attribute: AzString::from(src_attr),
});
}
}
"style" => {
for child in node.children.as_ref().iter() {
if let XmlNodeChild::Text(text) = child {
Self::extract_css_urls(text.as_str(), resources);
}
}
}
_ => {}
}
if let Some(style) = get_attr("style") {
Self::extract_css_urls(&style, resources);
}
if let Some(bg) = get_attr("background") {
let mime = Self::guess_mime_from_url(&bg, "image");
resources.push(ExternalResource {
url: AzString::from(bg),
kind: ExternalResourceKind::Image,
mime_type: mime.into(),
source_element: AzString::from(tag_name),
source_attribute: AzString::from("background"),
});
}
for child in node.children.as_ref().iter() {
Self::scan_node_child(child, resources);
}
}
fn extract_css_urls(css: &str, resources: &mut Vec<ExternalResource>) {
let mut remaining = css;
while let Some(pos) = remaining.find("url(") {
let after_url = &remaining[pos + 4..];
if let Some(url) = Self::extract_url_value(after_url) {
let mime = Self::guess_mime_from_url(&url, "");
let kind = Self::guess_kind_from_url(&url);
resources.push(ExternalResource {
url: AzString::from(url),
kind,
mime_type: mime.into(),
source_element: AzString::from("style"),
source_attribute: AzString::from("url()"),
});
}
remaining = after_url;
}
remaining = css;
while let Some(pos) = remaining.to_lowercase().find("@import") {
let after_import = &remaining[pos + 7..];
let trimmed = after_import.trim_start();
if trimmed.starts_with("url(") {
if let Some(url) = Self::extract_url_value(&trimmed[4..]) {
resources.push(ExternalResource {
url: AzString::from(url),
kind: ExternalResourceKind::Stylesheet,
mime_type: Some(MimeTypeHint::new("text/css")).into(),
source_element: AzString::from("style"),
source_attribute: AzString::from("@import"),
});
}
} else if let Some(url) = Self::extract_quoted_string(trimmed) {
resources.push(ExternalResource {
url: AzString::from(url),
kind: ExternalResourceKind::Stylesheet,
mime_type: Some(MimeTypeHint::new("text/css")).into(),
source_element: AzString::from("style"),
source_attribute: AzString::from("@import"),
});
}
remaining = after_import;
}
}
fn extract_url_value(s: &str) -> Option<String> {
let trimmed = s.trim_start();
if trimmed.starts_with('"') {
Self::extract_quoted_string(trimmed)
} else if trimmed.starts_with('\'') {
let end = trimmed[1..].find('\'')?;
Some(trimmed[1..1+end].to_string())
} else {
let end = trimmed.find(')')?;
Some(trimmed[..end].trim().to_string())
}
}
fn extract_quoted_string(s: &str) -> Option<String> {
if s.starts_with('"') {
let end = s[1..].find('"')?;
Some(s[1..1+end].to_string())
} else if s.starts_with('\'') {
let end = s[1..].find('\'')?;
Some(s[1..1+end].to_string())
} else {
None
}
}
fn parse_srcset(srcset: &str) -> Vec<String> {
srcset.split(',')
.filter_map(|entry| {
let trimmed = entry.trim();
trimmed.split_whitespace().next().map(|s| s.to_string())
})
.filter(|url| !url.is_empty())
.collect()
}
fn looks_like_resource(url: &str) -> bool {
let lower = url.to_lowercase();
let resource_exts = [
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp",
".ttf", ".otf", ".woff", ".woff2", ".eot",
".css", ".js",
".mp4", ".webm", ".ogg", ".mp3", ".wav",
".pdf", ".zip", ".tar", ".gz",
];
resource_exts.iter().any(|ext| lower.ends_with(ext))
}
fn guess_kind_from_url(url: &str) -> ExternalResourceKind {
let lower = url.to_lowercase();
if lower.contains(".png") || lower.contains(".jpg") || lower.contains(".jpeg")
|| lower.contains(".gif") || lower.contains(".webp") || lower.contains(".svg")
|| lower.contains(".bmp") || lower.contains(".avif") {
ExternalResourceKind::Image
} else if lower.contains(".ttf") || lower.contains(".otf") || lower.contains(".woff")
|| lower.contains(".eot") {
ExternalResourceKind::Font
} else if lower.contains(".css") {
ExternalResourceKind::Stylesheet
} else if lower.contains(".js") {
ExternalResourceKind::Script
} else if lower.contains(".mp4") || lower.contains(".webm") || lower.contains(".ogg") {
ExternalResourceKind::Video
} else if lower.contains(".mp3") || lower.contains(".wav") || lower.contains(".flac") {
ExternalResourceKind::Audio
} else if lower.contains(".ico") {
ExternalResourceKind::Icon
} else {
ExternalResourceKind::Unknown
}
}
fn guess_mime_from_url(url: &str, category: &str) -> Option<MimeTypeHint> {
let lower = url.to_lowercase();
let ext = lower.rsplit('.').next()?;
let ext = ext.split('?').next()?;
let valid_exts = [
"png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "avif",
"ttf", "otf", "woff", "woff2", "eot",
"css", "js", "mjs",
"mp4", "webm", "ogg", "mp3", "wav", "flac",
];
if valid_exts.contains(&ext) {
Some(MimeTypeHint::from_extension(ext))
} else if !category.is_empty() {
match category {
"image" => Some(MimeTypeHint::new("image/*")),
"font" => Some(MimeTypeHint::new("font/*")),
"stylesheet" => Some(MimeTypeHint::new("text/css")),
"script" => Some(MimeTypeHint::new("application/javascript")),
"video" => Some(MimeTypeHint::new("video/*")),
"audio" => Some(MimeTypeHint::new("audio/*")),
_ => None,
}
} else {
None
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct NonXmlCharError {
pub ch: u32,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidCharError {
pub expected: u8,
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidCharMultipleError {
pub expected: u8,
pub got: U8Vec,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidQuoteError {
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidSpaceError {
pub got: u8,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct InvalidStringError {
pub got: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum XmlStreamError {
UnexpectedEndOfStream,
InvalidName,
NonXmlChar(NonXmlCharError),
InvalidChar(InvalidCharError),
InvalidCharMultiple(InvalidCharMultipleError),
InvalidQuote(InvalidQuoteError),
InvalidSpace(InvalidSpaceError),
InvalidString(InvalidStringError),
InvalidReference,
InvalidExternalID,
InvalidCommentData,
InvalidCommentEnd,
InvalidCharacterData,
}
impl fmt::Display for XmlStreamError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::XmlStreamError::*;
match self {
UnexpectedEndOfStream => write!(f, "Unexpected end of stream"),
InvalidName => write!(f, "Invalid name"),
NonXmlChar(nx) => write!(
f,
"Non-XML character: {:?} at {}",
core::char::from_u32(nx.ch),
nx.pos
),
InvalidChar(ic) => write!(
f,
"Invalid character: expected: {}, got: {} at {}",
ic.expected as char, ic.got as char, ic.pos
),
InvalidCharMultiple(imc) => write!(
f,
"Multiple invalid characters: expected: {}, got: {:?} at {}",
imc.expected,
imc.got.as_ref(),
imc.pos
),
InvalidQuote(iq) => write!(f, "Invalid quote: got {} at {}", iq.got as char, iq.pos),
InvalidSpace(is) => write!(f, "Invalid space: got {} at {}", is.got as char, is.pos),
InvalidString(ise) => write!(
f,
"Invalid string: got \"{}\" at {}",
ise.got.as_str(),
ise.pos
),
InvalidReference => write!(f, "Invalid reference"),
InvalidExternalID => write!(f, "Invalid external ID"),
InvalidCommentData => write!(f, "Invalid comment data"),
InvalidCommentEnd => write!(f, "Invalid comment end"),
InvalidCharacterData => write!(f, "Invalid character data"),
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Ord, Hash, Eq)]
#[repr(C)]
pub struct XmlTextPos {
pub row: u32,
pub col: u32,
}
impl fmt::Display for XmlTextPos {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "line {}:{}", self.row, self.col)
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct XmlTextError {
pub stream_error: XmlStreamError,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum XmlParseError {
InvalidDeclaration(XmlTextError),
InvalidComment(XmlTextError),
InvalidPI(XmlTextError),
InvalidDoctype(XmlTextError),
InvalidEntity(XmlTextError),
InvalidElement(XmlTextError),
InvalidAttribute(XmlTextError),
InvalidCdata(XmlTextError),
InvalidCharData(XmlTextError),
UnknownToken(XmlTextPos),
}
impl fmt::Display for XmlParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::XmlParseError::*;
match self {
InvalidDeclaration(e) => {
write!(f, "Invalid declaraction: {} at {}", e.stream_error, e.pos)
}
InvalidComment(e) => write!(f, "Invalid comment: {} at {}", e.stream_error, e.pos),
InvalidPI(e) => write!(
f,
"Invalid processing instruction: {} at {}",
e.stream_error, e.pos
),
InvalidDoctype(e) => write!(f, "Invalid doctype: {} at {}", e.stream_error, e.pos),
InvalidEntity(e) => write!(f, "Invalid entity: {} at {}", e.stream_error, e.pos),
InvalidElement(e) => write!(f, "Invalid element: {} at {}", e.stream_error, e.pos),
InvalidAttribute(e) => write!(f, "Invalid attribute: {} at {}", e.stream_error, e.pos),
InvalidCdata(e) => write!(f, "Invalid CDATA: {} at {}", e.stream_error, e.pos),
InvalidCharData(e) => write!(f, "Invalid char data: {} at {}", e.stream_error, e.pos),
UnknownToken(e) => write!(f, "Unknown token at {}", e),
}
}
}
impl_result!(
Xml,
XmlError,
ResultXmlXmlError,
copy = false,
[Debug, PartialEq, PartialOrd, Clone]
);
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedNamespaceError {
pub ns: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownNamespaceError {
pub ns: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnexpectedCloseTagError {
pub expected: AzString,
pub actual: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct UnknownEntityReferenceError {
pub entity: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct DuplicatedAttributeError {
pub attribute: AzString,
pub pos: XmlTextPos,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C)]
pub struct MalformedHierarchyError {
pub expected: AzString,
pub got: AzString,
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
#[repr(C, u8)]
pub enum XmlError {
NoParserAvailable,
InvalidXmlPrefixUri(XmlTextPos),
UnexpectedXmlUri(XmlTextPos),
UnexpectedXmlnsUri(XmlTextPos),
InvalidElementNamePrefix(XmlTextPos),
DuplicatedNamespace(DuplicatedNamespaceError),
UnknownNamespace(UnknownNamespaceError),
UnexpectedCloseTag(UnexpectedCloseTagError),
UnexpectedEntityCloseTag(XmlTextPos),
UnknownEntityReference(UnknownEntityReferenceError),
MalformedEntityReference(XmlTextPos),
EntityReferenceLoop(XmlTextPos),
InvalidAttributeValue(XmlTextPos),
DuplicatedAttribute(DuplicatedAttributeError),
NoRootNode,
SizeLimit,
DtdDetected,
MalformedHierarchy(MalformedHierarchyError),
ParserError(XmlParseError),
UnclosedRootNode,
UnexpectedDeclaration(XmlTextPos),
NodesLimitReached,
AttributesLimitReached,
NamespacesLimitReached,
InvalidName(XmlTextPos),
NonXmlChar(XmlTextPos),
InvalidChar(XmlTextPos),
InvalidChar2(XmlTextPos),
InvalidString(XmlTextPos),
InvalidExternalID(XmlTextPos),
InvalidComment(XmlTextPos),
InvalidCharacterData(XmlTextPos),
UnknownToken(XmlTextPos),
UnexpectedEndOfStream,
}
impl fmt::Display for XmlError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::XmlError::*;
match self {
NoParserAvailable => write!(
f,
"Library was compiled without XML parser (XML parser not available)"
),
InvalidXmlPrefixUri(pos) => {
write!(f, "Invalid XML Prefix URI at line {}:{}", pos.row, pos.col)
}
UnexpectedXmlUri(pos) => {
write!(f, "Unexpected XML URI at 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)]
pub struct ComponentArguments {
pub args: ComponentArgumentTypes,
pub accepts_text: bool,
}
impl Default for ComponentArguments {
fn default() -> Self {
Self {
args: ComponentArgumentTypes::default(),
accepts_text: false,
}
}
}
impl ComponentArguments {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FilteredComponentArguments {
pub types: ComponentArgumentTypes,
pub values: BTreeMap<String, String>,
pub accepts_text: bool,
}
impl Default for FilteredComponentArguments {
fn default() -> Self {
Self {
types: Vec::new(),
values: BTreeMap::default(),
accepts_text: false,
}
}
}
impl FilteredComponentArguments {
fn new() -> Self {
Self::default()
}
}
pub trait XmlComponentTrait {
fn get_type_id(&self) -> String {
"div".to_string()
}
fn get_xml_node(&self) -> XmlNode {
XmlNode::create(self.get_type_id())
}
fn get_available_arguments(&self) -> ComponentArguments {
ComponentArguments::new()
}
fn render_dom(
&self,
components: &XmlComponentMap,
arguments: &FilteredComponentArguments,
content: &XmlTextContent,
) -> Result<StyledDom, RenderDomError>;
fn compile_to_rust_code(
&self,
components: &XmlComponentMap,
attributes: &ComponentArguments,
content: &XmlTextContent,
) -> Result<String, CompileError> {
Ok(String::new())
}
}
#[derive(Default)]
pub struct DomXml {
pub parsed_dom: StyledDom,
}
impl DomXml {
#[cfg(test)]
pub fn assert_eq(self, other: StyledDom) {
let mut fixed = Dom::create_body().style(Css::empty());
fixed.append_child(other);
if self.parsed_dom != fixed {
panic!(
"\r\nExpected DOM did not match:\r\n\r\nexpected: ----------\r\n{}\r\ngot: \
----------\r\n{}\r\n",
self.parsed_dom.get_html_string("", "", true),
fixed.get_html_string("", "", true)
);
}
}
pub fn into_styled_dom(self) -> StyledDom {
self.into()
}
}
impl Into<StyledDom> for DomXml {
fn into(self) -> StyledDom {
self.parsed_dom
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum XmlNodeChild {
Text(AzString),
Element(XmlNode),
}
impl_option!(
XmlNodeChild,
OptionXmlNodeChild,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl XmlNodeChild {
pub fn as_text(&self) -> Option<&str> {
match self {
XmlNodeChild::Text(s) => Some(s.as_str()),
XmlNodeChild::Element(_) => None,
}
}
pub fn as_element(&self) -> Option<&XmlNode> {
match self {
XmlNodeChild::Text(_) => None,
XmlNodeChild::Element(node) => Some(node),
}
}
pub fn as_element_mut(&mut self) -> Option<&mut XmlNode> {
match self {
XmlNodeChild::Text(_) => None,
XmlNodeChild::Element(node) => Some(node),
}
}
}
impl_vec!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor, XmlNodeChildVecDestructorType, XmlNodeChildVecSlice, OptionXmlNodeChild);
impl_vec_mut!(XmlNodeChild, XmlNodeChildVec);
impl_vec_debug!(XmlNodeChild, XmlNodeChildVec);
impl_vec_partialeq!(XmlNodeChild, XmlNodeChildVec);
impl_vec_eq!(XmlNodeChild, XmlNodeChildVec);
impl_vec_partialord!(XmlNodeChild, XmlNodeChildVec);
impl_vec_ord!(XmlNodeChild, XmlNodeChildVec);
impl_vec_hash!(XmlNodeChild, XmlNodeChildVec);
impl_vec_clone!(XmlNodeChild, XmlNodeChildVec, XmlNodeChildVecDestructor);
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct XmlNode {
pub node_type: XmlTagName,
pub attributes: XmlAttributeMap,
pub children: XmlNodeChildVec,
}
impl_option!(
XmlNode,
OptionXmlNode,
copy = false,
[Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl XmlNode {
pub fn create<I: Into<XmlTagName>>(node_type: I) -> Self {
XmlNode {
node_type: node_type.into(),
..Default::default()
}
}
pub fn with_children(mut self, v: Vec<XmlNodeChild>) -> Self {
Self {
children: v.into(),
..self
}
}
pub fn get_text_content(&self) -> String {
self.children
.as_ref()
.iter()
.filter_map(|child| child.as_text())
.collect::<Vec<_>>()
.join("")
}
pub fn has_only_text_children(&self) -> bool {
self.children
.as_ref()
.iter()
.all(|child| matches!(child, XmlNodeChild::Text(_)))
}
}
impl_vec!(XmlNode, XmlNodeVec, XmlNodeVecDestructor, XmlNodeVecDestructorType, XmlNodeVecSlice, OptionXmlNode);
impl_vec_mut!(XmlNode, XmlNodeVec);
impl_vec_debug!(XmlNode, XmlNodeVec);
impl_vec_partialeq!(XmlNode, XmlNodeVec);
impl_vec_eq!(XmlNode, XmlNodeVec);
impl_vec_partialord!(XmlNode, XmlNodeVec);
impl_vec_ord!(XmlNode, XmlNodeVec);
impl_vec_hash!(XmlNode, XmlNodeVec);
impl_vec_clone!(XmlNode, XmlNodeVec, XmlNodeVecDestructor);
pub struct XmlComponent {
pub id: String,
pub renderer: Box<dyn XmlComponentTrait>,
pub inherit_vars: bool,
}
impl core::fmt::Debug for XmlComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("XmlComponent")
.field("id", &self.id)
.field("args", &self.renderer.get_available_arguments())
.field("inherit_vars", &self.inherit_vars)
.finish()
}
}
pub struct XmlComponentMap {
pub components: BTreeMap<String, XmlComponent>,
}
impl Default for XmlComponentMap {
fn default() -> Self {
let mut map = Self {
components: BTreeMap::new(),
};
map.register_component(XmlComponent {
id: normalize_casing("html"),
renderer: Box::new(HtmlRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("head"),
renderer: Box::new(HeadRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("title"),
renderer: Box::new(TitleRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("body"),
renderer: Box::new(BodyRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("div"),
renderer: Box::new(DivRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("header"),
renderer: Box::new(HeaderRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("footer"),
renderer: Box::new(FooterRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("section"),
renderer: Box::new(SectionRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("article"),
renderer: Box::new(ArticleRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("aside"),
renderer: Box::new(AsideRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("nav"),
renderer: Box::new(NavRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("main"),
renderer: Box::new(MainRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("h1"),
renderer: Box::new(H1Renderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("h2"),
renderer: Box::new(H2Renderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("h3"),
renderer: Box::new(H3Renderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("h4"),
renderer: Box::new(H4Renderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("h5"),
renderer: Box::new(H5Renderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("h6"),
renderer: Box::new(H6Renderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("p"),
renderer: Box::new(TextRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("span"),
renderer: Box::new(SpanRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("pre"),
renderer: Box::new(PreRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("code"),
renderer: Box::new(CodeRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("blockquote"),
renderer: Box::new(BlockquoteRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("br"),
renderer: Box::new(BrRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("hr"),
renderer: Box::new(HrRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("icon"),
renderer: Box::new(IconRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("ul"),
renderer: Box::new(UlRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("ol"),
renderer: Box::new(OlRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("li"),
renderer: Box::new(LiRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("dl"),
renderer: Box::new(DlRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("dt"),
renderer: Box::new(DtRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("dd"),
renderer: Box::new(DdRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("table"),
renderer: Box::new(TableRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("thead"),
renderer: Box::new(TheadRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("tbody"),
renderer: Box::new(TbodyRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("tfoot"),
renderer: Box::new(TfootRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("tr"),
renderer: Box::new(TrRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("th"),
renderer: Box::new(ThRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("td"),
renderer: Box::new(TdRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("a"),
renderer: Box::new(ARenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("strong"),
renderer: Box::new(StrongRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("em"),
renderer: Box::new(EmRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("b"),
renderer: Box::new(BRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("i"),
renderer: Box::new(IRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("u"),
renderer: Box::new(URenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("small"),
renderer: Box::new(SmallRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("mark"),
renderer: Box::new(MarkRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("sub"),
renderer: Box::new(SubRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("sup"),
renderer: Box::new(SupRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("form"),
renderer: Box::new(FormRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("label"),
renderer: Box::new(LabelRenderer::new()),
inherit_vars: true,
});
map.register_component(XmlComponent {
id: normalize_casing("button"),
renderer: Box::new(ButtonRenderer::new()),
inherit_vars: true,
});
map
}
}
impl XmlComponentMap {
pub fn register_component(&mut self, comp: XmlComponent) {
self.components.insert(comp.id.clone(), comp);
}
pub fn get(&self, name: &str) -> Option<&XmlComponent> {
self.components.get(name)
}
}
#[derive(Debug, Clone, PartialEq)]
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)]
pub enum CompileError {
Dom(RenderDomError),
Xml(DomXmlParseError),
Css(CssParseErrorOwned),
}
impl From<ComponentError> for CompileError {
fn from(e: ComponentError) -> Self {
CompileError::Dom(RenderDomError::Component(e))
}
}
impl From<CssParseErrorOwned> for CompileError {
fn from(e: CssParseErrorOwned) -> Self {
CompileError::Css(e)
}
}
impl<'a> fmt::Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::CompileError::*;
match self {
Dom(d) => write!(f, "{}", d),
Xml(s) => write!(f, "{}", s),
Css(s) => write!(f, "{}", s.to_shared()),
}
}
}
impl From<RenderDomError> for CompileError {
fn from(e: RenderDomError) -> Self {
CompileError::Dom(e)
}
}
impl From<DomXmlParseError> for CompileError {
fn from(e: DomXmlParseError) -> Self {
CompileError::Xml(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ComponentError {
UselessFunctionArgument(AzString, AzString, Vec<String>),
UnknownComponent(AzString),
}
#[derive(Debug, Clone, PartialEq)]
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)]
pub enum ComponentParseError {
NotAComponent,
UnnamedComponent,
MissingName(usize),
MissingType(usize, AzString),
WhiteSpaceInComponentName(usize, AzString),
WhiteSpaceInComponentType(usize, AzString, AzString),
CssError(CssParseErrorOwned),
}
impl<'a> fmt::Display for DomXmlParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::DomXmlParseError::*;
match self {
NoHtmlNode => write!(
f,
"No <html> node found as the root of the file - empty file?"
),
MultipleHtmlRootNodes => write!(
f,
"Multiple <html> nodes found as the root of the file - only one root node allowed"
),
NoBodyInHtml => write!(
f,
"No <body> node found as a direct child of an <html> node - malformed DOM \
hierarchy?"
),
MultipleBodyNodes => write!(
f,
"Multiple <body> nodes present, only one <body> node is allowed"
),
Xml(e) => write!(f, "Error parsing XML: {}", e),
MalformedHierarchy(e) => write!(
f,
"Invalid </{}> tag: expected </{}>",
e.got.as_str(),
e.expected.as_str()
),
RenderDom(e) => write!(f, "Error rendering DOM: {}", e),
Component(c) => write!(f, "Error parsing component in <head> node:\r\n{}", c),
Css(c) => write!(f, "Error parsing CSS in <head> node:\r\n{}", c.to_shared()),
}
}
}
impl<'a> fmt::Display for ComponentParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ComponentParseError::*;
match self {
NotAComponent => write!(f, "Expected <component/> node, found no such node"),
UnnamedComponent => write!(
f,
"Found <component/> tag with out a \"name\" attribute, component must have a name"
),
MissingName(arg_pos) => write!(
f,
"Argument at position {} is either empty or has no name",
arg_pos
),
MissingType(arg_pos, arg_name) => write!(
f,
"Argument \"{}\" at position {} doesn't have a `: type`",
arg_pos, arg_name
),
WhiteSpaceInComponentName(arg_pos, arg_name_unparsed) => {
write!(
f,
"Missing `:` between the name and the type in argument {} (around \"{}\")",
arg_pos, arg_name_unparsed
)
}
WhiteSpaceInComponentType(arg_pos, arg_name, arg_type_unparsed) => {
write!(
f,
"Missing `,` between two arguments (in argument {}, position {}, around \
\"{}\")",
arg_name, arg_pos, arg_type_unparsed
)
}
CssError(lsf) => write!(f, "Error parsing <style> tag: {}", lsf.to_shared()),
}
}
}
impl fmt::Display for ComponentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ComponentError::*;
match self {
UselessFunctionArgument(k, v, available_args) => {
write!(
f,
"Useless component argument \"{}\": \"{}\" - available args are: {:#?}",
k, v, available_args
)
}
UnknownComponent(name) => write!(f, "Unknown component: \"{}\"", name),
}
}
}
impl<'a> fmt::Display for RenderDomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::RenderDomError::*;
match self {
Component(c) => write!(f, "{}", c),
CssError(e) => write!(f, "Error parsing CSS in component: {}", e.to_shared()),
}
}
}
macro_rules! html_component {
($name:ident, $tag:expr, $node_type:expr) => {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name {
node: XmlNode,
}
impl $name {
pub fn new() -> Self {
Self {
node: XmlNode::create($tag),
}
}
}
impl XmlComponentTrait for $name {
fn get_available_arguments(&self) -> ComponentArguments {
ComponentArguments {
args: ComponentArgumentTypes::default(),
accepts_text: true,
}
}
fn render_dom(
&self,
_: &XmlComponentMap,
_: &FilteredComponentArguments,
text: &XmlTextContent,
) -> Result<StyledDom, RenderDomError> {
let mut dom = Dom::create_node($node_type);
if let Some(text_str) = text.as_ref() {
let prepared = prepare_string(text_str);
if !prepared.is_empty() {
dom = dom.with_children(alloc::vec![Dom::create_text(prepared)].into());
}
}
Ok(dom.style(Css::empty()))
}
fn compile_to_rust_code(
&self,
_: &XmlComponentMap,
_: &ComponentArguments,
_: &XmlTextContent,
) -> Result<String, CompileError> {
Ok(format!(
"Dom::create_node(NodeType::{})",
stringify!($node_type)
))
}
fn get_xml_node(&self) -> XmlNode {
self.node.clone()
}
}
};
}
html_component!(HtmlRenderer, "html", NodeType::Html);
html_component!(HeadRenderer, "head", NodeType::Head);
html_component!(TitleRenderer, "title", NodeType::Title);
html_component!(HeaderRenderer, "header", NodeType::Header);
html_component!(FooterRenderer, "footer", NodeType::Footer);
html_component!(SectionRenderer, "section", NodeType::Section);
html_component!(ArticleRenderer, "article", NodeType::Article);
html_component!(AsideRenderer, "aside", NodeType::Aside);
html_component!(NavRenderer, "nav", NodeType::Nav);
html_component!(MainRenderer, "main", NodeType::Main);
html_component!(H1Renderer, "h1", NodeType::H1);
html_component!(H2Renderer, "h2", NodeType::H2);
html_component!(H3Renderer, "h3", NodeType::H3);
html_component!(H4Renderer, "h4", NodeType::H4);
html_component!(H5Renderer, "h5", NodeType::H5);
html_component!(H6Renderer, "h6", NodeType::H6);
html_component!(SpanRenderer, "span", NodeType::Span);
html_component!(PreRenderer, "pre", NodeType::Pre);
html_component!(CodeRenderer, "code", NodeType::Code);
html_component!(BlockquoteRenderer, "blockquote", NodeType::BlockQuote);
html_component!(UlRenderer, "ul", NodeType::Ul);
html_component!(OlRenderer, "ol", NodeType::Ol);
html_component!(LiRenderer, "li", NodeType::Li);
html_component!(DlRenderer, "dl", NodeType::Dl);
html_component!(DtRenderer, "dt", NodeType::Dt);
html_component!(DdRenderer, "dd", NodeType::Dd);
html_component!(TableRenderer, "table", NodeType::Table);
html_component!(TheadRenderer, "thead", NodeType::THead);
html_component!(TbodyRenderer, "tbody", NodeType::TBody);
html_component!(TfootRenderer, "tfoot", NodeType::TFoot);
html_component!(TrRenderer, "tr", NodeType::Tr);
html_component!(ThRenderer, "th", NodeType::Th);
html_component!(TdRenderer, "td", NodeType::Td);
html_component!(ARenderer, "a", NodeType::A);
html_component!(StrongRenderer, "strong", NodeType::Strong);
html_component!(EmRenderer, "em", NodeType::Em);
html_component!(BRenderer, "b", NodeType::B);
html_component!(IRenderer, "i", NodeType::I);
html_component!(URenderer, "u", NodeType::U);
html_component!(SmallRenderer, "small", NodeType::Small);
html_component!(MarkRenderer, "mark", NodeType::Mark);
html_component!(SubRenderer, "sub", NodeType::Sub);
html_component!(SupRenderer, "sup", NodeType::Sup);
html_component!(FormRenderer, "form", NodeType::Form);
html_component!(LabelRenderer, "label", NodeType::Label);
html_component!(ButtonRenderer, "button", NodeType::Button);
html_component!(HrRenderer, "hr", NodeType::Hr);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DivRenderer {
node: XmlNode,
}
impl DivRenderer {
pub fn new() -> Self {
Self {
node: XmlNode::create("div"),
}
}
}
impl XmlComponentTrait for DivRenderer {
fn get_available_arguments(&self) -> ComponentArguments {
ComponentArguments::new()
}
fn render_dom(
&self,
_: &XmlComponentMap,
_: &FilteredComponentArguments,
_: &XmlTextContent,
) -> Result<StyledDom, RenderDomError> {
Ok(Dom::create_div().style(Css::empty()))
}
fn compile_to_rust_code(
&self,
_: &XmlComponentMap,
_: &ComponentArguments,
_: &XmlTextContent,
) -> Result<String, CompileError> {
Ok("Dom::create_div()".into())
}
fn get_xml_node(&self) -> XmlNode {
self.node.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BodyRenderer {
node: XmlNode,
}
impl BodyRenderer {
pub fn new() -> Self {
Self {
node: XmlNode::create("body"),
}
}
}
impl XmlComponentTrait for BodyRenderer {
fn get_available_arguments(&self) -> ComponentArguments {
ComponentArguments::new()
}
fn render_dom(
&self,
_: &XmlComponentMap,
_: &FilteredComponentArguments,
_: &XmlTextContent,
) -> Result<StyledDom, RenderDomError> {
Ok(Dom::create_body().style(Css::empty()))
}
fn compile_to_rust_code(
&self,
_: &XmlComponentMap,
_: &ComponentArguments,
_: &XmlTextContent,
) -> Result<String, CompileError> {
Ok("Dom::create_body()".into())
}
fn get_xml_node(&self) -> XmlNode {
self.node.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BrRenderer {
node: XmlNode,
}
impl BrRenderer {
pub fn new() -> Self {
Self {
node: XmlNode::create("br"),
}
}
}
impl XmlComponentTrait for BrRenderer {
fn get_available_arguments(&self) -> ComponentArguments {
ComponentArguments::new()
}
fn render_dom(
&self,
_: &XmlComponentMap,
_: &FilteredComponentArguments,
_: &XmlTextContent,
) -> Result<StyledDom, RenderDomError> {
Ok(Dom::create_node(NodeType::Br).style(Css::empty()))
}
fn compile_to_rust_code(
&self,
_: &XmlComponentMap,
_: &ComponentArguments,
_: &XmlTextContent,
) -> Result<String, CompileError> {
Ok("Dom::create_node(NodeType::Br)".into())
}
fn get_xml_node(&self) -> XmlNode {
self.node.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IconRenderer {
node: XmlNode,
}
impl IconRenderer {
pub fn new() -> Self {
Self {
node: XmlNode::create("icon"),
}
}
}
impl XmlComponentTrait for IconRenderer {
fn get_available_arguments(&self) -> ComponentArguments {
let mut args = ComponentArgumentTypes::default();
args.push(("name".to_string(), "String".to_string()));
ComponentArguments {
args,
accepts_text: true, }
}
fn render_dom(
&self,
_: &XmlComponentMap,
args: &FilteredComponentArguments,
content: &XmlTextContent,
) -> Result<StyledDom, RenderDomError> {
let icon_name = args.values.get("name")
.map(|s| s.to_string())
.or_else(|| content.as_ref().map(|s| prepare_string(&s)))
.unwrap_or_else(|| "invalid-icon".to_string());
Ok(Dom::create_node(NodeType::Icon(AzString::from(icon_name))).style(Css::empty()))
}
fn compile_to_rust_code(
&self,
_: &XmlComponentMap,
args: &ComponentArguments,
content: &XmlTextContent,
) -> Result<String, CompileError> {
let icon_name = args.args.iter()
.find(|(name, _)| name == "name")
.map(|(_, value)| value.to_string())
.or_else(|| content.as_ref().map(|s| s.to_string()))
.unwrap_or_else(|| "invalid-icon".to_string());
Ok(format!("Dom::create_node(NodeType::Icon(AzString::from(\"{}\")))", icon_name))
}
fn get_xml_node(&self) -> XmlNode {
self.node.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextRenderer {
node: XmlNode,
}
impl TextRenderer {
pub fn new() -> Self {
Self {
node: XmlNode::create("p"),
}
}
}
impl XmlComponentTrait for TextRenderer {
fn get_available_arguments(&self) -> ComponentArguments {
ComponentArguments {
args: ComponentArgumentTypes::default(),
accepts_text: true, }
}
fn render_dom(
&self,
_: &XmlComponentMap,
_: &FilteredComponentArguments,
content: &XmlTextContent,
) -> Result<StyledDom, RenderDomError> {
let content = content
.as_ref()
.map(|s| prepare_string(&s))
.unwrap_or_default();
Ok(Dom::create_node(NodeType::P)
.with_children(vec![Dom::create_text(content)].into())
.style(Css::empty()))
}
fn compile_to_rust_code(
&self,
_: &XmlComponentMap,
args: &ComponentArguments,
content: &XmlTextContent,
) -> Result<String, CompileError> {
Ok(String::from(
"Dom::create_node(NodeType::P).with_children(vec![Dom::create_text(content)].into())",
))
}
fn get_xml_node(&self) -> XmlNode {
self.node.clone()
}
}
pub fn parse_component_arguments<'a>(
input: &'a str,
) -> Result<ComponentArgumentTypes, ComponentParseError> {
use self::ComponentParseError::*;
let mut args = ComponentArgumentTypes::default();
for (arg_idx, arg) in input.split(",").enumerate() {
let mut colon_iterator = arg.split(":");
let arg_name = colon_iterator.next().ok_or(MissingName(arg_idx))?;
let arg_name = arg_name.trim();
if arg_name.is_empty() {
return Err(MissingName(arg_idx));
}
if arg_name.chars().any(char::is_whitespace) {
return Err(WhiteSpaceInComponentName(arg_idx, arg_name.into()));
}
let arg_type = colon_iterator
.next()
.ok_or(MissingType(arg_idx, arg_name.into()))?;
let arg_type = arg_type.trim();
if arg_type.is_empty() {
return Err(MissingType(arg_idx, arg_name.into()));
}
if arg_type.chars().any(char::is_whitespace) {
return Err(WhiteSpaceInComponentType(
arg_idx,
arg_name.into(),
arg_type.into(),
));
}
let arg_name = normalize_casing(arg_name);
let arg_type = arg_type.to_string();
args.push((arg_name, arg_type));
}
Ok(args)
}
pub fn validate_and_filter_component_args(
xml_attributes: &XmlAttributeMap,
valid_args: &ComponentArguments,
) -> Result<FilteredComponentArguments, ComponentError> {
let mut map = FilteredComponentArguments {
types: ComponentArgumentTypes::default(),
values: BTreeMap::new(),
accepts_text: valid_args.accepts_text,
};
for AzStringPair { key, value } in xml_attributes.as_ref().iter() {
let xml_attribute_name = key;
let xml_attribute_value = value;
if let Some(valid_arg_type) = valid_args
.args
.iter()
.find(|s| s.0 == xml_attribute_name.as_str())
.map(|q| &q.1)
{
map.types.push((
xml_attribute_name.as_str().to_string(),
valid_arg_type.clone(),
));
map.values.insert(
xml_attribute_name.as_str().to_string(),
xml_attribute_value.as_str().to_string(),
);
} else if DEFAULT_ARGS.contains(&xml_attribute_name.as_str()) {
map.values.insert(
xml_attribute_name.as_str().to_string(),
xml_attribute_value.as_str().to_string(),
);
} else {
#[cfg(feature = "std")]
eprintln!(
"Warning: Useless component argument \"{}\": \"{}\" for component with args: {:?}",
xml_attribute_name,
xml_attribute_value,
valid_args.args.iter().map(|s| &s.0).collect::<Vec<_>>()
);
map.values.insert(
xml_attribute_name.as_str().to_string(),
xml_attribute_value.as_str().to_string(),
);
}
}
Ok(map)
}
pub fn get_html_node<'a>(root_nodes: &'a [XmlNodeChild]) -> Result<&'a XmlNode, DomXmlParseError> {
let mut html_node_iterator = root_nodes.iter().filter_map(|child| {
if let XmlNodeChild::Element(node) = child {
let node_type_normalized = normalize_casing(&node.node_type);
if &node_type_normalized == "html" {
Some(node)
} else {
None
}
} else {
None
}
});
let html_node = html_node_iterator
.next()
.ok_or(DomXmlParseError::NoHtmlNode)?;
if html_node_iterator.next().is_some() {
Err(DomXmlParseError::MultipleHtmlRootNodes)
} else {
Ok(html_node)
}
}
pub fn get_body_node<'a>(root_nodes: &'a [XmlNodeChild]) -> Result<&'a XmlNode, DomXmlParseError> {
let direct_body = root_nodes.iter().filter_map(|child| {
if let XmlNodeChild::Element(node) = child {
let node_type_normalized = normalize_casing(&node.node_type);
if &node_type_normalized == "body" {
Some(node)
} else {
None
}
} else {
None
}
}).next();
if let Some(body) = direct_body {
return Ok(body);
}
fn find_body_recursive<'a>(nodes: &'a [XmlNodeChild]) -> Option<&'a XmlNode> {
for child in nodes {
if let XmlNodeChild::Element(node) = child {
let node_type_normalized = normalize_casing(&node.node_type);
if &node_type_normalized == "body" {
return Some(node);
}
if let Some(found) = find_body_recursive(node.children.as_ref()) {
return Some(found);
}
}
}
None
}
find_body_recursive(root_nodes).ok_or(DomXmlParseError::NoBodyInHtml)
}
static DEFAULT_STR: &str = "";
pub fn find_node_by_type<'a>(
root_nodes: &'a [XmlNodeChild],
node_type: &str,
) -> Option<&'a XmlNode> {
for child in root_nodes {
if let XmlNodeChild::Element(node) = child {
if normalize_casing(&node.node_type).as_str() == node_type {
return Some(node);
}
}
}
for child in root_nodes {
if let XmlNodeChild::Element(node) = child {
if let Some(found) = find_node_by_type(node.children.as_ref(), node_type) {
return Some(found);
}
}
}
None
}
pub fn find_attribute<'a>(node: &'a XmlNode, attribute: &str) -> Option<&'a AzString> {
node.attributes
.iter()
.find(|n| normalize_casing(&n.key.as_str()).as_str() == attribute)
.map(|s| &s.value)
}
pub fn normalize_casing(input: &str) -> String {
let mut words: Vec<String> = Vec::new();
let mut cur_str = Vec::new();
for ch in input.chars() {
if ch.is_uppercase() || ch == '_' || ch == '-' {
if !cur_str.is_empty() {
words.push(cur_str.iter().collect());
cur_str.clear();
}
if ch.is_uppercase() {
cur_str.extend(ch.to_lowercase());
}
} else {
cur_str.extend(ch.to_lowercase());
}
}
if !cur_str.is_empty() {
words.push(cur_str.iter().collect());
cur_str.clear();
}
words.join("_")
}
#[allow(trivial_casts)]
pub fn get_item<'a>(hierarchy: &[usize], root_node: &'a mut XmlNode) -> Option<&'a mut XmlNode> {
let mut hierarchy = hierarchy.to_vec();
hierarchy.reverse();
let item = match hierarchy.pop() {
Some(s) => s,
None => return Some(root_node),
};
let child = root_node.children.as_mut().get_mut(item)?;
match child {
XmlNodeChild::Element(node) => get_item_internal(&mut hierarchy, node),
XmlNodeChild::Text(_) => None, }
}
fn get_item_internal<'a>(
hierarchy: &mut Vec<usize>,
root_node: &'a mut XmlNode,
) -> Option<&'a mut XmlNode> {
if hierarchy.is_empty() {
return Some(root_node);
}
let cur_item = match hierarchy.pop() {
Some(s) => s,
None => return Some(root_node),
};
let child = root_node.children.as_mut().get_mut(cur_item)?;
match child {
XmlNodeChild::Element(node) => get_item_internal(hierarchy, node),
XmlNodeChild::Text(_) => None, }
}
pub fn str_to_dom<'a>(
root_nodes: &'a [XmlNodeChild],
component_map: &'a mut XmlComponentMap,
max_width: Option<f32>,
) -> Result<StyledDom, DomXmlParseError> {
let html_node = get_html_node(root_nodes)?;
let body_node = get_body_node(html_node.children.as_ref())?;
let mut global_style = None;
if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
for child in head_node.children.as_ref() {
if let XmlNodeChild::Element(node) = child {
match DynamicXmlComponent::new(node) {
Ok(comp) => {
let node_name = comp.name.clone();
component_map.register_component(XmlComponent {
id: normalize_casing(&node_name),
renderer: Box::new(comp),
inherit_vars: false,
});
}
Err(ComponentParseError::NotAComponent) => {}
Err(e) => return Err(e.into()),
}
}
}
if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
let text = style_node.get_text_content();
if !text.is_empty() {
let parsed_css = Css::from_string(text.into());
global_style = Some(parsed_css);
}
}
}
render_dom_from_body_node(&body_node, global_style, component_map, max_width)
.map_err(|e| e.into())
}
pub fn str_to_rust_code<'a>(
root_nodes: &'a [XmlNodeChild],
imports: &str,
component_map: &'a mut XmlComponentMap,
) -> Result<String, CompileError> {
let html_node = get_html_node(&root_nodes)?;
let body_node = get_body_node(html_node.children.as_ref())?;
let mut global_style = Css::empty();
if let Some(head_node) = find_node_by_type(html_node.children.as_ref(), "head") {
for child in head_node.children.as_ref() {
if let XmlNodeChild::Element(node) = child {
match DynamicXmlComponent::new(node) {
Ok(node) => {
let node_name = node.name.clone();
component_map.register_component(XmlComponent {
id: normalize_casing(&node_name),
renderer: Box::new(node),
inherit_vars: false,
});
}
Err(ComponentParseError::NotAComponent) => {}
Err(e) => return Err(CompileError::Xml(e.into())),
}
}
}
if let Some(style_node) = find_node_by_type(head_node.children.as_ref(), "style") {
let text = style_node.get_text_content();
if !text.is_empty() {
let parsed_css = azul_css::parser2::new_from_str(&text).0;
global_style = parsed_css;
}
}
}
global_style.sort_by_specificity();
let mut css_blocks = BTreeMap::new();
let mut extra_blocks = VecContents::default();
let app_source = compile_body_node_to_rust_code(
&body_node,
component_map,
&mut extra_blocks,
&mut css_blocks,
&global_style,
CssMatcher {
path: Vec::new(),
indices_in_parent: vec![0],
children_length: vec![body_node.children.as_ref().len()],
},
)?;
let app_source = app_source
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
let t = " ";
let css_blocks = css_blocks
.iter()
.map(|(k, v)| {
let v = v
.lines()
.map(|l| format!("{}{}{}", t, t, l))
.collect::<Vec<String>>()
.join("\r\n");
format!(
" const {}_PROPERTIES: &[NodeDataInlineCssProperty] = \
&[\r\n{}\r\n{}];\r\n{}const {}: NodeDataInlineCssPropertyVec = \
NodeDataInlineCssPropertyVec::from_const_slice({}_PROPERTIES);",
k, v, t, t, k, k
)
})
.collect::<Vec<_>>()
.join(&format!("{}\r\n\r\n", t));
let mut extra_block_string = extra_blocks.format(1);
let main_func = "
use azul::{
app::{App, AppConfig, LayoutSolver},
css::Css,
style::StyledDom,
callbacks::{RefAny, LayoutCallbackInfo},
window::{WindowCreateOptions, WindowFrame},
};
struct Data { }
extern \"C\" fn render(_: RefAny, _: LayoutCallbackInfo) -> StyledDom {
crate::ui::render()
.style(Css::empty()) // styles are applied inline
}
fn main() {
let app = App::new(RefAny::new(Data { }), AppConfig::new(LayoutSolver::Default));
let mut window = WindowCreateOptions::new(render);
window.state.flags.frame = WindowFrame::Maximized;
app.run(window);
}";
let source_code = format!(
"#![windows_subsystem = \"windows\"]\r\n//! Auto-generated UI source \
code\r\n{}\r\n{}\r\n\r\n{}{}",
imports,
compile_components(compile_components_to_rust_code(component_map)?),
format!(
"#[allow(unused_imports)]\r\npub mod ui {{
pub use crate::components::*;
use azul::css::*;
use azul::str::String as AzString;
use azul::vec::{{
DomVec, IdOrClassVec, NodeDataInlineCssPropertyVec,
StyleBackgroundSizeVec, StyleBackgroundRepeatVec,
StyleBackgroundContentVec, StyleTransformVec,
StyleFontFamilyVec, StyleBackgroundPositionVec,
NormalizedLinearColorStopVec, NormalizedRadialColorStopVec,
}};
use azul::dom::{{
Dom, IdOrClass, TabIndex,
IdOrClass::{{Id, Class}},
NodeDataInlineCssProperty,
}};\r\n\r\n{}\r\n\r\n{}
pub fn render() -> Dom {{\r\n{}\r\n }}\r\n}}",
extra_block_string, css_blocks, app_source
),
main_func,
);
Ok(source_code)
}
pub fn compile_components(
components: Vec<(
ComponentName,
CompiledComponent,
ComponentArguments,
BTreeMap<String, String>,
)>,
) -> String {
let cs = components
.iter()
.map(|(name, function_body, function_args, css_blocks)| {
let name = &normalize_casing(&name);
let f = compile_component(name, function_args, function_body)
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
format!(
"#[allow(unused_imports)]\r\npub mod {} {{\r\n use azul::dom::Dom;\r\n use \
azul::str::String as AzString;\r\n{}\r\n}}",
name, f
)
})
.collect::<Vec<String>>()
.join("\r\n\r\n");
let cs = cs
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
if cs.is_empty() {
cs
} else {
format!("pub mod components {{\r\n{}\r\n}}", cs)
}
}
pub fn format_component_args(component_args: &ComponentArgumentTypes) -> String {
let mut args = component_args
.iter()
.map(|(arg_name, arg_type)| format!("{}: {}", arg_name, arg_type))
.collect::<Vec<String>>();
args.sort_by(|a, b| b.cmp(&a));
args.join(", ")
}
pub fn compile_component(
component_name: &str,
component_args: &ComponentArguments,
component_function_body: &str,
) -> String {
let component_name = &normalize_casing(&component_name);
let function_args = format_component_args(&component_args.args);
let component_function_body = component_function_body
.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\r\n");
let should_inline = component_function_body.lines().count() == 1;
format!(
"{}pub fn render({}{}{}) -> Dom {{\r\n{}\r\n}}",
if should_inline { "#[inline]\r\n" } else { "" },
if component_args.accepts_text {
"text: AzString"
} else {
""
},
if function_args.is_empty() || !component_args.accepts_text {
""
} else {
", "
},
function_args,
component_function_body,
)
}
fn xml_node_to_dom_fast<'a>(
xml_node: &'a XmlNode,
component_map: &'a XmlComponentMap,
) -> Result<Dom, RenderDomError> {
use crate::dom::{Dom, NodeType, IdOrClass};
let component_name = normalize_casing(&xml_node.node_type);
let xml_component = component_map
.get(&component_name)
.ok_or(ComponentError::UnknownComponent(component_name.clone().into()))?;
let node_type = get_node_type_for_component(&component_name);
let mut dom = Dom::create_node(node_type);
let mut ids_and_classes = Vec::new();
if let Some(id_str) = xml_node.attributes.get_key("id") {
for id in id_str.split_whitespace() {
ids_and_classes.push(IdOrClass::Id(id.into()));
}
}
if let Some(class_str) = xml_node.attributes.get_key("class") {
for class in class_str.split_whitespace() {
ids_and_classes.push(IdOrClass::Class(class.into()));
}
}
if !ids_and_classes.is_empty() {
dom.root.set_ids_and_classes(ids_and_classes.into());
}
let mut children = Vec::new();
for child in xml_node.children.as_ref().iter() {
match child {
XmlNodeChild::Element(child_node) => {
let child_dom = xml_node_to_dom_fast(child_node, component_map)?;
children.push(child_dom);
}
XmlNodeChild::Text(text) => {
let text_dom = Dom::create_text(AzString::from(text.as_str()));
children.push(text_dom);
}
}
}
if !children.is_empty() {
dom = dom.with_children(children.into());
}
Ok(dom)
}
fn get_node_type_for_component(name: &str) -> crate::dom::NodeType {
use crate::dom::NodeType;
match name {
"html" => NodeType::Html,
"head" => NodeType::Head,
"title" => NodeType::Title,
"body" => NodeType::Body,
"div" => NodeType::Div,
"p" => NodeType::P,
"span" => NodeType::Span,
"br" => NodeType::Br,
"h1" => NodeType::H1,
"h2" => NodeType::H2,
"h3" => NodeType::H3,
"h4" => NodeType::H4,
"h5" => NodeType::H5,
"h6" => NodeType::H6,
"header" => NodeType::Header,
"footer" => NodeType::Footer,
"section" => NodeType::Section,
"article" => NodeType::Article,
"aside" => NodeType::Aside,
"nav" => NodeType::Nav,
"main" => NodeType::Main,
"pre" => NodeType::Pre,
"code" => NodeType::Code,
"blockquote" => NodeType::BlockQuote,
"ul" => NodeType::Ul,
"ol" => NodeType::Ol,
"li" => NodeType::Li,
"dl" => NodeType::Dl,
"dt" => NodeType::Dt,
"dd" => NodeType::Dd,
"table" => NodeType::Table,
"thead" => NodeType::THead,
"tbody" => NodeType::TBody,
"tfoot" => NodeType::TFoot,
"tr" => NodeType::Tr,
"th" => NodeType::Th,
"td" => NodeType::Td,
"a" => NodeType::A,
"strong" => NodeType::Strong,
"em" => NodeType::Em,
"b" => NodeType::B,
"i" => NodeType::I,
"u" => NodeType::U,
"small" => NodeType::Small,
"mark" => NodeType::Mark,
"sub" => NodeType::Sub,
"sup" => NodeType::Sup,
"form" => NodeType::Form,
"label" => NodeType::Label,
"button" => NodeType::Button,
"hr" => NodeType::Hr,
_ => NodeType::Div, }
}
pub fn render_dom_from_body_node<'a>(
body_node: &'a XmlNode,
mut global_css: Option<Css>,
component_map: &'a XmlComponentMap,
max_width: Option<f32>,
) -> Result<StyledDom, RenderDomError> {
let body_dom = xml_node_to_dom_fast(body_node, component_map)?;
let mut combined_stylesheets = Vec::new();
if let Some(max_width) = max_width {
let max_width_css = Css::from_string(
format!("html {{ max-width: {max_width}px; }}").into(),
);
for s in max_width_css.stylesheets.as_ref().iter() {
combined_stylesheets.push(s.clone());
}
}
if let Some(css) = global_css.take() {
for s in css.stylesheets.as_ref().iter() {
combined_stylesheets.push(s.clone());
}
}
let combined_css = Css::new(combined_stylesheets);
use crate::dom::NodeType;
let root_node_type = body_dom.root.node_type.clone();
let mut full_dom = match root_node_type {
NodeType::Html => {
body_dom
}
NodeType::Body => {
Dom::create_html().with_child(body_dom)
}
_ => {
let body_wrapper = Dom::create_body().with_child(body_dom);
Dom::create_html().with_child(body_wrapper)
}
};
let styled = full_dom.style(combined_css);
Ok(styled)
}
pub fn render_dom_from_body_node_inner<'a>(
xml_node: &'a XmlNode,
component_map: &'a XmlComponentMap,
parent_xml_attributes: &FilteredComponentArguments,
) -> Result<StyledDom, RenderDomError> {
let component_name = normalize_casing(&xml_node.node_type);
let xml_component = component_map
.get(&component_name)
.ok_or(ComponentError::UnknownComponent(
component_name.clone().into(),
))?;
let available_function_args = xml_component.renderer.get_available_arguments();
let mut filtered_xml_attributes =
validate_and_filter_component_args(&xml_node.attributes, &available_function_args)?;
if xml_component.inherit_vars {
filtered_xml_attributes
.types
.extend(parent_xml_attributes.types.clone().into_iter());
}
for v in filtered_xml_attributes.types.iter_mut() {
v.1 = format_args_dynamic(&v.1, &parent_xml_attributes.types).to_string();
}
let mut dom = xml_component.renderer.render_dom(
component_map,
&filtered_xml_attributes,
&OptionString::None,
)?;
set_attributes(&mut dom, &xml_node.attributes, &filtered_xml_attributes);
let mut child_index = 0usize;
for child in xml_node.children.as_ref().iter() {
match child {
XmlNodeChild::Element(child_node) => {
let child_dom = render_dom_from_body_node_inner(
child_node,
component_map,
&filtered_xml_attributes,
)?;
dom.append_child_with_index(child_dom, child_index);
child_index += 1;
}
XmlNodeChild::Text(text) => {
let text_dom = Dom::create_text(AzString::from(text.as_str())).style(Css::empty());
dom.append_child_with_index(text_dom, child_index);
child_index += 1;
}
}
}
Ok(dom)
}
pub fn set_attributes(
dom: &mut StyledDom,
xml_attributes: &XmlAttributeMap,
filtered_xml_attributes: &FilteredComponentArguments,
) {
use crate::dom::{
IdOrClass::{Class, Id},
TabIndex,
};
let mut ids_and_classes = Vec::new();
let dom_root = match dom.root.into_crate_internal() {
Some(s) => s,
None => return,
};
let node_data = &mut dom.node_data.as_container_mut()[dom_root];
if let Some(ids) = xml_attributes.get_key("id") {
for id in ids.split_whitespace() {
ids_and_classes.push(Id(
format_args_dynamic(id, &filtered_xml_attributes.types).into()
));
}
}
if let Some(classes) = xml_attributes.get_key("class") {
for class in classes.split_whitespace() {
ids_and_classes.push(Class(
format_args_dynamic(class, &filtered_xml_attributes.types).into(),
));
}
}
node_data.set_ids_and_classes(ids_and_classes.into());
if let Some(focusable) = xml_attributes
.get_key("focusable")
.map(|f| format_args_dynamic(f.as_str(), &filtered_xml_attributes.types))
.and_then(|f| parse_bool(&f))
{
match focusable {
true => node_data.set_tab_index(TabIndex::Auto),
false => node_data.set_tab_index(TabIndex::NoKeyboardFocus.into()),
}
}
if let Some(tab_index) = xml_attributes
.get_key("tabindex")
.map(|val| format_args_dynamic(val, &filtered_xml_attributes.types))
.and_then(|val| val.parse::<isize>().ok())
{
match tab_index {
0 => node_data.set_tab_index(TabIndex::Auto),
i if i > 0 => node_data.set_tab_index(TabIndex::OverrideInParent(i as u32)),
_ => node_data.set_tab_index(TabIndex::NoKeyboardFocus),
}
}
if let Some(style) = xml_attributes.get_key("style") {
let css_key_map = azul_css::props::property::get_css_key_map();
let mut attributes = Vec::new();
for s in style.as_str().split(";") {
let mut s = s.split(":");
let key = match s.next() {
Some(s) => s,
None => continue,
};
let value = match s.next() {
Some(s) => s,
None => continue,
};
azul_css::parser2::parse_css_declaration(
key.trim(),
value.trim(),
(ErrorLocation::default(), ErrorLocation::default()),
&css_key_map,
&mut Vec::new(),
&mut attributes,
);
}
let props = attributes
.into_iter()
.filter_map(|s| {
use azul_css::dynamic_selector::CssPropertyWithConditions;
match s {
CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
_ => return None,
}
})
.collect::<Vec<_>>();
node_data.set_css_props(props.into());
}
}
pub fn set_stringified_attributes(
dom_string: &mut String,
xml_attributes: &XmlAttributeMap,
filtered_xml_attributes: &ComponentArgumentTypes,
tabs: usize,
) {
let t0 = String::from(" ").repeat(tabs);
let t = String::from(" ").repeat(tabs + 1);
let mut ids_and_classes = String::new();
for id in xml_attributes
.get_key("id")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default()
{
ids_and_classes.push_str(&format!(
"{} Id(AzString::from_const_str(\"{}\")),\r\n",
t0,
format_args_dynamic(id, &filtered_xml_attributes)
));
}
for class in xml_attributes
.get_key("class")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default()
{
ids_and_classes.push_str(&format!(
"{} Class(AzString::from_const_str(\"{}\")),\r\n",
t0,
format_args_dynamic(class, &filtered_xml_attributes)
));
}
if !ids_and_classes.is_empty() {
use azul_css::format_rust_code::GetHash;
let id = ids_and_classes.get_hash();
dom_string.push_str(&format!(
"\r\n{t0}.with_ids_and_classes({{\r\n{t}const IDS_AND_CLASSES_{id}: &[IdOrClass] = \
&[\r\n{t}{ids_and_classes}\r\n{t}];\r\\
n{t}IdOrClassVec::from_const_slice(IDS_AND_CLASSES_{id})\r\n{t0}}})",
t0 = t0,
t = t,
ids_and_classes = ids_and_classes,
id = id
));
}
if let Some(focusable) = xml_attributes
.get_key("focusable")
.map(|f| format_args_dynamic(f, &filtered_xml_attributes))
.and_then(|f| parse_bool(&f))
{
match focusable {
true => dom_string.push_str(&format!("\r\n{}.with_tab_index(TabIndex::Auto)", t)),
false => dom_string.push_str(&format!(
"\r\n{}.with_tab_index(TabIndex::NoKeyboardFocus)",
t
)),
}
}
if let Some(tab_index) = xml_attributes
.get_key("tabindex")
.map(|val| format_args_dynamic(val, &filtered_xml_attributes))
.and_then(|val| val.parse::<isize>().ok())
{
match tab_index {
0 => dom_string.push_str(&format!("\r\n{}.with_tab_index(TabIndex::Auto)", t)),
i if i > 0 => dom_string.push_str(&format!(
"\r\n{}.with_tab_index(TabIndex::OverrideInParent({}))",
t, i as usize
)),
_ => dom_string.push_str(&format!(
"\r\n{}.with_tab_index(TabIndex::NoKeyboardFocus)",
t
)),
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum DynamicItem {
Var(String),
Str(String),
}
pub fn split_dynamic_string(input: &str) -> Vec<DynamicItem> {
use self::DynamicItem::*;
let input: Vec<char> = input.chars().collect();
let input_chars_len = input.len();
let mut items = Vec::new();
let mut current_idx = 0;
let mut last_idx = 0;
while current_idx < input_chars_len {
let c = input[current_idx];
match c {
'{' if input.get(current_idx + 1).copied() != Some('{') => {
let mut start_offset = 1;
let mut has_found_variable = false;
while let Some(c) = input.get(current_idx + start_offset) {
if c.is_whitespace() {
break;
}
if *c == '}' && input.get(current_idx + start_offset + 1).copied() != Some('}')
{
start_offset += 1;
has_found_variable = true;
break;
}
start_offset += 1;
}
if has_found_variable {
if last_idx != current_idx {
items.push(Str(input[last_idx..current_idx].iter().collect()));
}
items.push(Var(input
[(current_idx + 1)..(current_idx + start_offset - 1)]
.iter()
.collect()));
current_idx = current_idx + start_offset;
last_idx = current_idx;
} else {
current_idx += start_offset;
}
}
_ => {
current_idx += 1;
}
}
}
if current_idx != last_idx {
items.push(Str(input[last_idx..].iter().collect()));
}
for item in &mut items {
if let Str(s) = item {
*s = s.replace("{{", "{").replace("}}", "}");
}
}
items
}
pub fn combine_and_replace_dynamic_items(
input: &[DynamicItem],
variables: &ComponentArgumentTypes,
) -> String {
let mut s = String::new();
for item in input {
match item {
DynamicItem::Var(v) => {
let variable_name = normalize_casing(v.trim());
match variables
.iter()
.find(|s| s.0 == variable_name)
.map(|q| &q.1)
{
Some(resolved_var) => {
s.push_str(&resolved_var);
}
None => {
s.push('{');
s.push_str(v);
s.push('}');
}
}
}
DynamicItem::Str(dynamic_str) => {
s.push_str(&dynamic_str);
}
}
}
s
}
pub fn format_args_dynamic(input: &str, variables: &ComponentArgumentTypes) -> String {
let dynamic_str_items = split_dynamic_string(input);
combine_and_replace_dynamic_items(&dynamic_str_items, variables)
}
pub fn prepare_string(input: &str) -> String {
const SPACE: &str = " ";
const RETURN: &str = "\n";
let input = input.trim();
if input.is_empty() {
return String::new();
}
let input = input.replace("<", "<");
let input = input.replace(">", ">");
let input_len = input.len();
let mut final_lines: Vec<String> = Vec::new();
let mut last_line_was_empty = false;
for line in input.lines() {
let line = line.trim();
let line = line.replace(" ", " ");
let current_line_is_empty = line.is_empty();
if !current_line_is_empty {
if last_line_was_empty {
final_lines.push(format!("{}{}", RETURN, line));
} else {
final_lines.push(line.to_string());
}
}
last_line_was_empty = current_line_is_empty;
}
let line_len = final_lines.len();
let mut target = String::with_capacity(input_len);
for (line_idx, line) in final_lines.iter().enumerate() {
if !(line.starts_with(RETURN) || line_idx == 0 || line_idx == line_len.saturating_sub(1)) {
target.push_str(SPACE);
}
target.push_str(line);
}
target
}
pub fn parse_bool(input: &str) -> Option<bool> {
match input {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
pub fn render_component_inner<'a>(
map: &mut Vec<(
ComponentName,
CompiledComponent,
ComponentArguments,
BTreeMap<String, String>,
)>,
component_name: String,
xml_component: &'a XmlComponent,
component_map: &'a XmlComponentMap,
parent_xml_attributes: &ComponentArguments,
tabs: usize,
) -> Result<(), CompileError> {
let t = String::from(" ").repeat(tabs - 1);
let t1 = String::from(" ").repeat(tabs);
let component_name = normalize_casing(&component_name);
let xml_node = xml_component.renderer.get_xml_node();
let mut css = match find_node_by_type(xml_node.children.as_ref(), "style") {
Some(style_node) => {
let text = style_node.get_text_content();
if !text.is_empty() {
Some(text)
} else {
None
}
}
None => None,
};
let mut css = match css {
Some(text) => azul_css::parser2::new_from_str(&text).0,
None => Css::empty(),
};
css.sort_by_specificity();
let available_function_arg_types = xml_component.renderer.get_available_arguments();
let mut filtered_xml_attributes = available_function_arg_types.clone();
if xml_component.inherit_vars {
filtered_xml_attributes
.args
.extend(parent_xml_attributes.args.clone().into_iter());
}
for v in filtered_xml_attributes.args.iter_mut() {
v.1 = format_args_dynamic(&v.1, &parent_xml_attributes.args).to_string();
}
let text_content = xml_node.get_text_content();
let text = if !text_content.is_empty() {
Some(AzString::from(format_args_dynamic(
&text_content,
&filtered_xml_attributes.args,
)))
} else {
None
};
let mut dom_string = xml_component.renderer.compile_to_rust_code(
component_map,
&filtered_xml_attributes,
&text.into(),
)?;
set_stringified_attributes(
&mut dom_string,
&xml_node.attributes,
&filtered_xml_attributes.args,
tabs,
);
let matcher = CssMatcher {
path: vec![CssPathSelector::Type(NodeTypeTag::Body)],
indices_in_parent: Vec::new(),
children_length: Vec::new(),
};
let mut css_blocks = BTreeMap::new();
let mut extra_blocks = VecContents::default();
if !xml_node.children.as_ref().is_empty() {
dom_string.push_str(&format!(
"\r\n{}.with_children(DomVec::from_vec(vec![\r\n",
t
));
for (child_idx, child) in xml_node.children.as_ref().iter().enumerate() {
if let XmlNodeChild::Element(child_node) = child {
let mut matcher = matcher.clone();
matcher.indices_in_parent.push(child_idx);
matcher
.children_length
.push(xml_node.children.as_ref().len());
dom_string.push_str(&format!(
"{}{},",
t1,
compile_node_to_rust_code_inner(
child_node,
component_map,
&filtered_xml_attributes,
tabs + 1,
&mut extra_blocks,
&mut css_blocks,
&css,
matcher,
)?
));
}
}
dom_string.push_str(&format!("\r\n{}]))", t));
}
map.push((
component_name,
dom_string,
filtered_xml_attributes,
css_blocks,
));
Ok(())
}
pub fn compile_components_to_rust_code(
components: &XmlComponentMap,
) -> Result<
Vec<(
ComponentName,
CompiledComponent,
ComponentArguments,
BTreeMap<String, String>,
)>,
CompileError,
> {
let mut map = Vec::new();
for (id, xml_component) in &components.components {
render_component_inner(
&mut map,
id.clone(),
xml_component,
&components,
&ComponentArguments::default(),
1,
)?;
}
Ok(map)
}
#[derive(Clone)]
pub struct CssMatcher {
path: Vec<CssPathSelector>,
indices_in_parent: Vec<usize>,
children_length: Vec<usize>,
}
impl CssMatcher {
fn get_hash(&self) -> u64 {
use core::hash::Hash;
use highway::{HighwayHash, HighwayHasher, Key};
let mut hasher = HighwayHasher::new(Key([0; 4]));
for p in self.path.iter() {
p.hash(&mut hasher);
}
hasher.finalize64()
}
}
impl CssMatcher {
fn matches(&self, path: &CssPath) -> bool {
use azul_css::css::CssPathSelector::*;
use crate::style::{CssGroupIterator, CssGroupSplitReason};
if self.path.is_empty() {
return false;
}
if path.selectors.as_ref().is_empty() {
return false;
}
let mut path_groups = CssGroupIterator::new(path.selectors.as_ref()).collect::<Vec<_>>();
path_groups.reverse();
if path_groups.is_empty() {
return false;
}
let mut self_groups = CssGroupIterator::new(self.path.as_ref()).collect::<Vec<_>>();
self_groups.reverse();
if self_groups.is_empty() {
return false;
}
if self.indices_in_parent.len() != self_groups.len() {
return false;
}
if self.children_length.len() != self_groups.len() {
return false;
}
let mut cur_selfgroup_scan = 0;
let mut cur_pathgroup_scan = 0;
let mut valid = false;
let mut path_group = path_groups[cur_pathgroup_scan].clone();
while cur_selfgroup_scan < self_groups.len() {
let mut advance = None;
for (id, cg) in self_groups[cur_selfgroup_scan..].iter().enumerate() {
let gm = group_matches(
&path_group.0,
&self_groups[cur_selfgroup_scan + id].0,
self.indices_in_parent[cur_selfgroup_scan + id],
self.children_length[cur_selfgroup_scan + id],
);
if gm {
advance = Some(id);
break;
}
}
match advance {
Some(n) => {
if cur_pathgroup_scan == path_groups.len() - 1 {
return cur_selfgroup_scan + n == self_groups.len() - 1;
} else {
cur_pathgroup_scan += 1;
cur_selfgroup_scan += n;
path_group = path_groups[cur_pathgroup_scan].clone();
}
}
None => return false, }
}
return cur_pathgroup_scan == path_groups.len() - 1;
}
}
fn group_matches(
a: &[&CssPathSelector],
b: &[&CssPathSelector],
idx_in_parent: usize,
parent_children: usize,
) -> bool {
use azul_css::css::{CssNthChildSelector, CssPathPseudoSelector, CssPathSelector::*};
for selector in a {
match selector {
Global => {}
PseudoSelector(CssPathPseudoSelector::Hover) => {}
PseudoSelector(CssPathPseudoSelector::Active) => {}
PseudoSelector(CssPathPseudoSelector::Focus) => {}
Type(tag) => {
if !b.iter().any(|t| **t == Type(tag.clone())) {
return false;
}
}
Class(class) => {
if !b.iter().any(|t| **t == Class(class.clone())) {
return false;
}
}
Id(id) => {
if !b.iter().any(|t| **t == Id(id.clone())) {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::First) => {
if idx_in_parent != 0 {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::Last) => {
if idx_in_parent != parent_children.saturating_sub(1) {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(i))) => {
if idx_in_parent != *i as usize {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even)) => {
if idx_in_parent % 2 != 0 {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)) => {
if idx_in_parent % 2 == 0 {
return false;
}
}
PseudoSelector(CssPathPseudoSelector::NthChild(CssNthChildSelector::Pattern(p))) => {
if idx_in_parent.saturating_sub(p.offset as usize) % p.pattern_repeat as usize != 0
{
return false;
}
}
_ => return false, }
}
true
}
struct CssBlock {
ending: Option<CssPathPseudoSelector>,
block: CssRuleBlock,
}
pub fn compile_body_node_to_rust_code<'a>(
body_node: &'a XmlNode,
component_map: &'a XmlComponentMap,
extra_blocks: &mut VecContents,
css_blocks: &mut BTreeMap<String, String>,
css: &Css,
mut matcher: CssMatcher,
) -> Result<String, CompileError> {
use azul_css::css::CssDeclaration;
let t = "";
let t2 = " ";
let mut dom_string = String::from("Dom::create_body()");
let node_type = CssPathSelector::Type(NodeTypeTag::Body);
matcher.path.push(node_type);
let ids = body_node
.attributes
.get_key("id")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
ids.into_iter()
.map(|id| CssPathSelector::Id(id.to_string().into())),
);
let classes = body_node
.attributes
.get_key("class")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
classes
.into_iter()
.map(|class| CssPathSelector::Class(class.to_string().into())),
);
let matcher_hash = matcher.get_hash();
let css_blocks_for_this_node = get_css_blocks(css, &matcher);
if !css_blocks_for_this_node.is_empty() {
use azul_css::props::property::format_static_css_prop;
let css_strings = css_blocks_for_this_node
.iter()
.rev()
.map(|css_block| {
let wrapper = match css_block.ending {
Some(CssPathPseudoSelector::Hover) => "Hover",
Some(CssPathPseudoSelector::Active) => "Active",
Some(CssPathPseudoSelector::Focus) => "Focus",
_ => "Normal",
};
for declaration in css_block.block.declarations.as_ref().iter() {
let prop = match declaration {
CssDeclaration::Static(s) => s,
CssDeclaration::Dynamic(d) => &d.default_value,
};
extra_blocks.insert_from_css_property(prop);
}
let formatted = css_block
.block
.declarations
.as_ref()
.iter()
.rev()
.map(|s| match &s {
CssDeclaration::Static(s) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(s, 1)
),
CssDeclaration::Dynamic(d) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(&d.default_value, 1)
),
})
.collect::<Vec<String>>();
format!("// {}\r\n{}", css_block.block.path, formatted.join(",\r\n"))
})
.collect::<Vec<_>>()
.join(",\r\n");
css_blocks.insert(format!("CSS_MATCH_{:09}", matcher_hash), css_strings);
dom_string.push_str(&format!(
"\r\n{}.with_inline_css_props(CSS_MATCH_{:09})",
t2, matcher_hash
));
}
if !body_node.children.as_ref().is_empty() {
use azul_css::format_rust_code::GetHash;
let children_hash = body_node.children.as_ref().get_hash();
dom_string.push_str(&format!("\r\n.with_children(DomVec::from_vec(vec![\r\n"));
for (child_idx, child) in body_node.children.as_ref().iter().enumerate() {
match child {
XmlNodeChild::Element(child_node) => {
let mut matcher = matcher.clone();
matcher.path.push(CssPathSelector::Children);
matcher.indices_in_parent.push(child_idx);
matcher.children_length.push(body_node.children.len());
dom_string.push_str(&format!(
"{}{},\r\n",
t,
compile_node_to_rust_code_inner(
child_node,
component_map,
&ComponentArguments::default(),
1,
extra_blocks,
css_blocks,
css,
matcher,
)?
));
}
XmlNodeChild::Text(text) => {
let text = text.trim();
if !text.is_empty() {
let escaped = text.replace("\\", "\\\\").replace("\"", "\\\"");
dom_string
.push_str(&format!("{}Dom::create_text(\"{}\".into()),\r\n", t, escaped));
}
}
}
}
dom_string.push_str(&format!("\r\n{}]))", t));
}
let dom_string = dom_string.trim();
Ok(dom_string.to_string())
}
fn get_css_blocks(css: &Css, matcher: &CssMatcher) -> Vec<CssBlock> {
let mut blocks = Vec::new();
for stylesheet in css.stylesheets.as_ref() {
for css_block in stylesheet.rules.as_ref() {
if matcher.matches(&css_block.path) {
let mut ending = None;
if let Some(CssPathSelector::PseudoSelector(p)) =
css_block.path.selectors.as_ref().last()
{
ending = Some(p.clone());
}
blocks.push(CssBlock {
ending,
block: css_block.clone(),
});
}
}
}
blocks
}
fn compile_and_format_dynamic_items(input: &[DynamicItem]) -> String {
use self::DynamicItem::*;
if input.is_empty() {
String::from("AzString::from_const_str(\"\")")
} else if input.len() == 1 {
match &input[0] {
Var(v) => normalize_casing(v.trim()),
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(v) => {
let variable_name = normalize_casing(v.trim());
formatted_str.push_str(&format!("{{{}}}", variable_name));
variables.push(variable_name.clone());
}
Str(s) => {
let s = s.replace("\"", "\\\"");
formatted_str.push_str(&s);
}
}
}
formatted_str.push('\"');
if !variables.is_empty() {
formatted_str.push_str(", ");
}
formatted_str.push_str(&variables.join(", "));
formatted_str.push_str(").into()");
formatted_str
}
}
fn format_args_for_rust_code(input: &str) -> String {
let dynamic_str_items = split_dynamic_string(input);
compile_and_format_dynamic_items(&dynamic_str_items)
}
pub fn compile_node_to_rust_code_inner<'a>(
node: &XmlNode,
component_map: &'a XmlComponentMap,
parent_xml_attributes: &ComponentArguments,
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 xml_component = component_map
.get(&component_name)
.ok_or(ComponentError::UnknownComponent(
component_name.clone().into(),
))?;
let available_function_args = xml_component.renderer.get_available_arguments();
let mut filtered_xml_attributes =
validate_and_filter_component_args(&node.attributes, &available_function_args)?;
if xml_component.inherit_vars {
filtered_xml_attributes
.types
.extend(parent_xml_attributes.args.clone().into_iter());
}
for v in filtered_xml_attributes.types.iter_mut() {
v.1 = format_args_dynamic(&v.1, &parent_xml_attributes.args).to_string();
}
let instantiated_function_arguments = {
let mut args = filtered_xml_attributes
.types
.iter()
.filter_map(|(xml_attribute_key, _xml_attribute_type)| {
match node.attributes.get_key(xml_attribute_key).cloned() {
Some(s) => Some(format_args_for_rust_code(&s)),
None => {
None
}
}
})
.collect::<Vec<String>>();
args.sort_by(|a, b| a.cmp(&b));
args.join(", ")
};
let text_as_first_arg = if filtered_xml_attributes.accepts_text {
let node_text = node.get_text_content();
let node_text = format_args_for_rust_code(node_text.trim());
let trailing_comma = if !instantiated_function_arguments.is_empty() {
", "
} else {
""
};
format!("{}{}", node_text, trailing_comma)
} else {
String::new()
};
let node_type = CssPathSelector::Type(match component_name.as_str() {
"body" => NodeTypeTag::Body,
"div" => NodeTypeTag::Div,
"br" => NodeTypeTag::Br,
"p" => NodeTypeTag::P,
"img" => NodeTypeTag::Img,
"br" => NodeTypeTag::Br,
other => {
return Err(CompileError::Dom(RenderDomError::Component(
ComponentError::UnknownComponent(other.to_string().into()),
)));
}
});
let mut dom_string = format!(
"{}{}::render({}{})",
t2, component_name, text_as_first_arg, instantiated_function_arguments
);
matcher.path.push(node_type);
let ids = node
.attributes
.get_key("id")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
ids.into_iter()
.map(|id| CssPathSelector::Id(id.to_string().into())),
);
let classes = node
.attributes
.get_key("class")
.map(|s| s.split_whitespace().collect::<Vec<_>>())
.unwrap_or_default();
matcher.path.extend(
classes
.into_iter()
.map(|class| CssPathSelector::Class(class.to_string().into())),
);
let matcher_hash = matcher.get_hash();
let css_blocks_for_this_node = get_css_blocks(css, &matcher);
if !css_blocks_for_this_node.is_empty() {
use azul_css::props::property::format_static_css_prop;
let css_strings = css_blocks_for_this_node
.iter()
.rev()
.map(|css_block| {
let wrapper = match css_block.ending {
Some(CssPathPseudoSelector::Hover) => "Hover",
Some(CssPathPseudoSelector::Active) => "Active",
Some(CssPathPseudoSelector::Focus) => "Focus",
_ => "Normal",
};
for declaration in css_block.block.declarations.as_ref().iter() {
let prop = match declaration {
CssDeclaration::Static(s) => s,
CssDeclaration::Dynamic(d) => &d.default_value,
};
extra_blocks.insert_from_css_property(prop);
}
let formatted = css_block
.block
.declarations
.as_ref()
.iter()
.rev()
.map(|s| match &s {
CssDeclaration::Static(s) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(s, 1)
),
CssDeclaration::Dynamic(d) => format!(
"NodeDataInlineCssProperty::{}({})",
wrapper,
format_static_css_prop(&d.default_value, 1)
),
})
.collect::<Vec<String>>();
format!("// {}\r\n{}", css_block.block.path, formatted.join(",\r\n"))
})
.collect::<Vec<_>>()
.join(",\r\n");
css_blocks.insert(format!("CSS_MATCH_{:09}", matcher_hash), css_strings);
dom_string.push_str(&format!(
"\r\n{}.with_inline_css_props(CSS_MATCH_{:09})",
t2, matcher_hash
));
}
set_stringified_attributes(
&mut dom_string,
&node.attributes,
&filtered_xml_attributes.types,
tabs,
);
let mut children_string = node
.children
.as_ref()
.iter()
.enumerate()
.filter_map(|(child_idx, c)| match c {
XmlNodeChild::Element(child_node) => {
let mut matcher = matcher.clone();
matcher.path.push(CssPathSelector::Children);
matcher.indices_in_parent.push(child_idx);
matcher.children_length.push(node.children.len());
Some(compile_node_to_rust_code_inner(
child_node,
component_map,
&ComponentArguments {
args: filtered_xml_attributes.types.clone(),
accepts_text: filtered_xml_attributes.accepts_text,
},
tabs + 1,
extra_blocks,
css_blocks,
css,
matcher,
))
}
XmlNodeChild::Text(text) => {
let text = text.trim();
if text.is_empty() {
None
} else {
let t2 = String::from(" ").repeat(tabs);
let escaped = text.replace("\\", "\\\\").replace("\"", "\\\"");
Some(Ok(format!("{}Dom::create_text(\"{}\".into())", t2, escaped)))
}
}
})
.collect::<Result<Vec<_>, _>>()?
.join(&format!(",\r\n"));
if !children_string.is_empty() {
dom_string.push_str(&format!(
"\r\n{}.with_children(DomVec::from_vec(vec![\r\n{}\r\n{}]))",
t2, children_string, t2
));
}
Ok(dom_string)
}
pub struct DynamicXmlComponent {
pub name: String,
pub arguments: ComponentArguments,
pub root: XmlNode,
}
impl DynamicXmlComponent {
pub fn new<'a>(root: &'a XmlNode) -> Result<Self, ComponentParseError> {
let node_type = normalize_casing(&root.node_type);
if node_type.as_str() != "component" {
return Err(ComponentParseError::NotAComponent);
}
let name = root
.attributes
.get_key("name")
.cloned()
.ok_or(ComponentParseError::NotAComponent)?;
let accepts_text = root
.attributes
.get_key("accepts_text")
.and_then(|p| parse_bool(p.as_str()))
.unwrap_or(false);
let args = match root.attributes.get_key("args") {
Some(s) => parse_component_arguments(s)?,
None => ComponentArgumentTypes::default(),
};
Ok(Self {
name: normalize_casing(&name),
arguments: ComponentArguments { args, accepts_text },
root: root.clone(),
})
}
}
impl XmlComponentTrait for DynamicXmlComponent {
fn get_available_arguments(&self) -> ComponentArguments {
self.arguments.clone()
}
fn get_xml_node(&self) -> XmlNode {
self.root.clone()
}
fn render_dom<'a>(
&'a self,
components: &'a XmlComponentMap,
arguments: &FilteredComponentArguments,
content: &XmlTextContent,
) -> Result<StyledDom, RenderDomError> {
let mut component_css = match find_node_by_type(self.root.children.as_ref(), "style") {
Some(style_node) => {
let text = style_node.get_text_content();
if !text.is_empty() {
let parsed_css = Css::from_string(text.into());
Some(parsed_css)
} else {
None
}
}
None => None,
};
let mut dom = StyledDom::default();
for child in self.root.children.as_ref() {
if let XmlNodeChild::Element(child_node) = child {
dom.append_child(render_dom_from_body_node_inner(
child_node, components, arguments,
)?);
}
}
if let Some(css) = component_css.clone() {
dom.restyle(css);
}
Ok(dom)
}
fn compile_to_rust_code(
&self,
components: &XmlComponentMap,
attributes: &ComponentArguments,
content: &XmlTextContent,
) -> Result<String, CompileError> {
Ok("Dom::create_div()".into()) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dom::{Dom, NodeType};
#[test]
fn test_inline_span_parsing() {
let html = r#"<p>Text before <span class="highlight">inline text</span> text after.</p>"#;
let expected_dom = Dom::create_p().with_children(
vec![
Dom::create_text("Text before "),
Dom::create_node(NodeType::Span)
.with_children(vec![Dom::create_text("inline text")].into()),
Dom::create_text(" text after."),
]
.into(),
);
assert_eq!(expected_dom.children.as_ref().len(), 3);
match &expected_dom.children.as_ref()[1].root.node_type {
NodeType::Span => {}
other => panic!("Expected Span, got {:?}", other),
}
assert_eq!(expected_dom.children.as_ref()[1].children.as_ref().len(), 1);
println!("Test passed: Inline span parsing structure is correct");
}
#[test]
fn test_xml_node_structure() {
let node = XmlNode {
node_type: "p".into(),
attributes: XmlAttributeMap {
inner: StringPairVec::from_const_slice(&[]),
},
children: vec![
XmlNodeChild::Text("Before ".into()),
XmlNodeChild::Element(XmlNode {
node_type: "span".into(),
children: vec![XmlNodeChild::Text("inline".into())].into(),
..Default::default()
}),
XmlNodeChild::Text(" after".into()),
]
.into(),
};
assert_eq!(node.children.as_ref().len(), 3);
assert_eq!(node.children.as_ref()[0].as_text(), Some("Before "));
assert_eq!(
node.children.as_ref()[1]
.as_element()
.unwrap()
.node_type
.as_str(),
"span"
);
assert_eq!(node.children.as_ref()[2].as_text(), Some(" after"));
let span = node.children.as_ref()[1].as_element().unwrap();
assert_eq!(span.children.as_ref().len(), 1);
assert_eq!(span.children.as_ref()[0].as_text(), Some("inline"));
println!("Test passed: XmlNode structure preserves text nodes correctly");
}
}