Add conversion from component AST to flat component with content attribute (resolves #3), add XML attribute parsing, fix template string attribute parsing bug

This commit is contained in:
Keavon Chambers 2020-07-15 01:11:52 -07:00
parent 00cd62d411
commit 3e12576b3c
17 changed files with 352 additions and 223 deletions

View file

@ -19,11 +19,12 @@ Layout is controlled using predefined attributes, such as `width`, `height`, `x-
The children of a component are passed to it as a `content` attribute. For example, looking at the row component:
```xml
<row content="INNER_XML: (Layout | None) = none">
<row content="INNER_XML: (Layout) = [[]]">
{{INNER_XML}}
</row>
```
The `content` attribute defines a new variable `INNER_XML` of type either `Layout` or `None`, which can contain more XML or nothing at all. It has a default value of `none` (of type `None`).
The `content` attribute defines a new variable `INNER_XML` of type `Layout` which can contain more XML layout structure. It has a default value of `[[]]` which refers to an empty layout— XML syntax (for the `Layout` data type) written in a tag's attribute is wrapped in ``[[`` (opening) and `]]` (closing) symbols. In this case the `INNER_XML` variable defaults to empty XML, however it is not stricly useful here because the `content` attribute will always have its value replaced by whatever exists between opening and closing tags when this component is called from elsewhere.
This is then expanded in the body of the row: `{{INNER_XML}}`.
## Defining new components

View file

@ -1,3 +1,3 @@
<box content="INNER_XML: (None) = none" :fill="FILL: (Color | None) = none" :round="ROUND: (AbsolutePx | AbsolutePx, AbsolutePx, AbsolutePx, AbsolutePx) = 0px" :border-thickness="BORDER_THICKNESS: (AbsolutePx) = 0px" :border-color="BORDER_COLOR: (Color | None) = none">
<box content="INNER_XML: (Layout) = [[]]" :fill="FILL: (Color) = ['middlegray']" :round="ROUND: (AbsolutePx | AbsolutePx, AbsolutePx, AbsolutePx, AbsolutePx) = 0px" :border-thickness="BORDER_THICKNESS: (AbsolutePx) = 0px" :border-color="BORDER_COLOR: (Color) = ['black']">
{{INNER_XML}}
</box>
</box>

View file

@ -1,3 +1,3 @@
<col content="INNER_XML: (None) = none">
<col content="INNER_XML: (Layout) = [[]]">
{{INNER_XML}}
</col>
</col>

View file

@ -13,4 +13,4 @@
<box height="100%" y-align="50%" x-padding="18px">
<icon :svg="`window_close.svg`" />
</box>
</header:window-buttons>
</header:window-buttons>

View file

@ -1,3 +1,3 @@
<icon content="INNER_XML: (None) = none" :svg="SVG_SOURCE: (TemplateString) = ``" :style="SVG_STYLE: (TemplateString) = ``">
<icon content="INNER_XML: (Layout) = [[]]" :svg="SVG_SOURCE: (TemplateString) = ``" :style="SVG_STYLE: (TemplateString) = ``">
{{INNER_XML}}
</icon>
</icon>

View file

@ -1,3 +1,3 @@
<if content="INNER_XML: (None) = none" :a="CONDITION_A: (Integer | Decimal | AbsolutePx | Percent | PercentRemainder | Inner | Width | Height | TemplateString | Color | Bool | None) = true" :b="CONDITION_B: (Integer | Decimal | AbsolutePx | Percent | PercentRemainder | Inner | Width | Height | TemplateString | Color | Bool | None) = true">
<if content="INNER_XML: (Layout) = [[]]" :a="CONDITION_A: (Integer | Decimal | AbsolutePx | Percent | PercentRemainder | Inner | Width | Height | TemplateString | Color | Bool | None) = true" :b="CONDITION_B: (Integer | Decimal | AbsolutePx | Percent | PercentRemainder | Inner | Width | Height | TemplateString | Color | Bool | None) = true">
{{RESULT}}
</if>
</if>

View file

@ -1,4 +1,4 @@
<input:checkbox-with-dropdown content="OPTION_LIST: (None) = none" :disabled="DISABLED: (Bool) = false" :checked="CHECKED: (Bool) = false" :selected-index="SELECTED_INDEX: (Integer) = 0">
<input:checkbox-with-dropdown content="OPTION_LIST: (Layout) = [[]]" :disabled="DISABLED: (Bool) = false" :checked="CHECKED: (Bool) = false" :selected-index="SELECTED_INDEX: (Integer) = 0">
<!-- Checkbox -->
<col width="height" height="100%">
<box width="100%" height="100%" x-align="50%" y-align="50%" :round="4px, 0px, 0px, 4px" :fill="['accent']">

View file

@ -1,4 +1,4 @@
<input:dropdown content="OPTION_LIST: (TemplateString | None) = none" :selected-index="SELECTED_INDEX: (Integer) = 0" :disabled="DISABLED: (Bool) = false">
<input:dropdown content="OPTION_LIST: (TemplateString) = ``" :selected-index="SELECTED_INDEX: (Integer) = 0" :disabled="DISABLED: (Bool) = false">
<box width="100%" :round="4px">
<!-- Current selection text -->
<col width="100%" height="24px">

View file

@ -1,3 +1,3 @@
<row content="INNER_XML: (None) = none">
<row content="INNER_XML: (Layout) = [[]]">
{{INNER_XML}}
</row>
</row>

View file

@ -1 +1 @@
<text content="TEXT_STRING: (None) = none" :color="COLOR: (Color | None) = ['middlegray']" :size="SIZE: (AbsolutePx) = 12px"></text>
<text content="TEXT_STRING: (TemplateString) = `MISSING TEXT CONTENT`" :color="COLOR: (Color) = ['middlegray']" :size="SIZE: (AbsolutePx) = 12px"></text>

View file

@ -4,4 +4,4 @@
Option B
Option C
</input:checkbox-with-dropdown>
</viewport:panels>
</viewport:panels>

View file

@ -85,7 +85,7 @@ impl Application {
// Main window in the XML layout language
let mut main_window_layout = LayoutSystem::new();
main_window_layout.load_layout("window", "main");
main_window_layout.load_layout_component("window", "main");
Self {
surface,

View file

@ -1,5 +1,5 @@
#[repr(C, align(16))]
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,

View file

@ -1,6 +1,6 @@
use crate::layout_abstract_types::*;
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub enum LayoutAbstractNode {
Tag(LayoutAbstractTag),
Text(String),
@ -16,7 +16,7 @@ impl LayoutAbstractNode {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub struct LayoutAbstractTag {
pub namespace: String,
pub name: String,
@ -37,7 +37,7 @@ impl LayoutAbstractTag {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub struct Attribute {
pub name: String,
pub value: AttributeValue,
@ -49,7 +49,7 @@ impl Attribute {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub enum AttributeValue {
VariableParameter(VariableParameter),
TypeValue(Vec<TypeValueOrArgument>),

View file

@ -1,7 +1,7 @@
use crate::color::Color;
use crate::layout_abstract_syntax::*;
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub struct VariableParameter {
pub name: String,
pub type_sequence_options: Vec<Vec<TypeName>>,
@ -18,24 +18,13 @@ impl VariableParameter {
}
}
#[derive(Debug)]
pub struct VariableArgument {
pub name: String,
}
impl VariableArgument {
pub fn new(name: String) -> Self {
Self { name }
}
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub enum TypeValueOrArgument {
TypeValue(TypeValue),
VariableArgument(VariableArgument),
VariableArgument(String),
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub enum TypeName {
Layout,
Integer,
@ -52,9 +41,12 @@ pub enum TypeName {
None,
}
#[derive(Debug)]
pub type ComponentAst = rctree::Node<LayoutAbstractNode>;
pub type Component = Vec<LayoutAbstractNode>;
#[derive(Debug, Clone, PartialEq)]
pub enum TypeValue {
Layout(Vec<rctree::Node<LayoutAbstractNode>>),
Layout(Vec<ComponentAst>),
Integer(i64),
Decimal(f64),
AbsolutePx(f32),
@ -69,7 +61,7 @@ pub enum TypeValue {
None,
}
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
pub enum TemplateStringSegment {
String(String),
Argument(TypeValueOrArgument),

View file

@ -2,6 +2,7 @@ use crate::color::Color;
use crate::color_palette::ColorPalette;
use crate::layout_abstract_syntax::*;
use crate::layout_abstract_types::*;
use crate::layout_system::*;
pub struct AttributeParser {
capture_attribute_declaration_parameter_regex: regex::Regex,
@ -17,13 +18,15 @@ impl AttributeParser {
pub fn new() -> Self {
let capture_attribute_declaration_parameter_regex: regex::Regex = regex::Regex::new(
// Parameter: ?: (?, ... | ...) = ?
r"^\s*(\w*)\s*(:)\s*(\()\s*((?:(?:\w+)(?:\s*,\s*\w+)*)(?:\s*\|\s*(?:(?:\w+)(?:\s*,\s*\w+)*))*)\s*(\))\s*(=)\s*([\s\w'\[\]@%\-.`,]*?)\s*$",
r"^\s*(\w*)\s*(:)\s*(\()\s*((?:(?:\w+)(?:\s*,\s*\w+)*)(?:\s*\|\s*(?:(?:\w+)(?:\s*,\s*\w+)*))*)\s*(\))\s*(=)\s*([\s\w'\[\]@%\-.,]*?|\s*`[^`]*?`)\s*$",
)
.unwrap();
let capture_attribute_type_sequences_regex: regex::Regex = regex::Regex::new(concat!(
// Argument: {{?}}
r#"^\s*(\{\{)\s*(\w*)\s*(\}\})\s*$|"#,
// Layout: [[?]]
r#"^\s*(\[\[)\s*(.*)\s*(\]\])\s*$|"#,
// Integer: ?
r#"^\s*(-?\d+)\s*$|"#,
// Decimal: ?
@ -89,9 +92,24 @@ impl AttributeParser {
let tokens = captures.as_ref().map(|c| c.as_slice());
match tokens {
// Argument: {{?}}
Some(["{{", name, "}}"]) => {
let name = String::from(*name);
TypeValueOrArgument::VariableArgument(VariableArgument::new(name))
Some(["{{", name, "}}"]) => TypeValueOrArgument::VariableArgument(String::from(*name)),
// Layout: [[?]]
Some(["[[", xml_syntax, "]]"]) => {
// Remove any whitespace in order to test if any XML syntax is present
let trimmed = xml_syntax.trim();
// Build either an empty vector (for empty XML input) or a vector with the one parsed AST
let layout_entries = if trimmed.len() == 0 {
vec![]
}
else {
let unescaped = Self::unescape_xml(trimmed);
let component_ast = LayoutSystem::parse_xml_tree(&self, &unescaped[..], false, false).unwrap();
vec![component_ast]
};
// Return the `Layout` typed value with the empty vector or vector with the parsed AST
TypeValueOrArgument::TypeValue(TypeValue::Layout(layout_entries))
},
// Integer: ?
Some([value]) if self.match_integer_regex.is_match(value) => {
@ -139,12 +157,19 @@ impl AttributeParser {
let mut segments = Vec::<TemplateStringSegment>::new();
let mut is_template = false;
// Alternate between string and handlebars, always starting wtih string even if empty, and push abstract tokens of non-empty ones to the TemplateString sequence
for part in self.split_by_string_templates_regex.split(string) {
let segment = match is_template {
true => TemplateStringSegment::String(String::from(part)),
false => TemplateStringSegment::Argument(TypeValueOrArgument::VariableArgument(VariableArgument::new(String::from(part)))),
};
segments.push(segment);
// Push only non-empty template string segments (a String or Argument)
if !part.is_empty() {
// Based on whether we are alternating to a string or template, push the appropriate abstract token
let segment = match is_template {
false => TemplateStringSegment::String(String::from(part)),
true => TemplateStringSegment::Argument(TypeValueOrArgument::VariableArgument(String::from(part))),
};
segments.push(segment);
}
// The next iteration will switch from a template to a string or vice versa
is_template = !is_template;
}
@ -268,4 +293,15 @@ impl AttributeParser {
_ => panic!("Invalid attribute attribute declaration `{}` when parsing XML layout", attribute_declaration),
}
}
/// Replace escape characters in an XML string, only supports `&, <, >, ", '`
fn unescape_xml(xml: &str) -> String {
// Find and replace each escape character, starting with `&` to avoid unescaping other escape sequences
xml.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("apos;", "'")
.replace("&#39;", "'")
}
}

View file

@ -1,4 +1,5 @@
use crate::layout_abstract_syntax::*;
use crate::layout_abstract_types::*;
use crate::layout_attribute_parser::*;
use crate::resource_cache::ResourceCache;
use std::collections::HashSet;
@ -6,8 +7,7 @@ use std::fs;
use std::io;
pub struct LayoutSystem {
// pub dom_tree: rctree::Node<
pub loaded_layouts: ResourceCache<rctree::Node<LayoutAbstractNode>>,
loaded_layouts: ResourceCache<Component>,
attribute_parser: AttributeParser,
}
@ -19,49 +19,62 @@ impl LayoutSystem {
}
}
pub fn load_layout(&mut self, namespace: &str, name: &str) {
// Load and parse the requested XML layout
let xml_path = self.layout_xml_path(namespace, name);
let window_main = self.parse_xml_file(&xml_path[..]).unwrap();
Self::print_layout_tree(&window_main);
/// Preload and cache a component by its namespace and name, then recursively explore and repeat for its descendants
pub fn load_layout_component(&mut self, namespace: &str, name: &str) {
// Load and parse the XML file's AST for the visited tag
let xml_path = Self::layout_xml_path(namespace, name);
let xml_parsed = Self::parse_xml_tree(&self.attribute_parser, &xml_path[..], true, true);
let mut xml_ast = match xml_parsed {
Ok(result) => result,
Err(error) => panic!("Error parsing XML layout syntax: {}", error),
};
// Keep track of it being loaded to prevent duplicate work
let mut already_loaded_layouts = HashSet::new();
already_loaded_layouts.insert(format!("{}:{}", namespace, name));
already_loaded_layouts.insert(Self::component_name(namespace, name));
// Load XML files recursively for all tags referenced in window:main and within those layouts
self.explore_referenced_layouts(&window_main, &mut already_loaded_layouts);
let tag_name = self.layout_name(namespace, name);
self.loaded_layouts.set(&tag_name[..], window_main);
// Turn the entire XML AST into a component
let component = Self::component_ast_to_component(&mut xml_ast);
// Self::print_layout_component(&component);
// Parse and cache components recursively for all tags referenced within this root component
self.explore_referenced_components(&xml_ast, &mut already_loaded_layouts);
// Save the loaded component to the cache
let component_name = Self::component_name(namespace, name);
self.loaded_layouts.set(&component_name[..], component);
}
fn explore_referenced_layouts(&mut self, layout_tree_root: &rctree::Node<LayoutAbstractNode>, already_loaded_layouts: &mut HashSet<String>) {
/// Preload and cache every XML component file referenced by tags within a recursive traversal of descendants in the given component AST
fn explore_referenced_components(&mut self, layout_tree_root: &ComponentAst, already_loaded_layouts: &mut HashSet<String>) {
for child_tag in layout_tree_root.descendants() {
match &*child_tag.borrow() {
// Tags are references to other XML layouts that should be loaded and cached
LayoutAbstractNode::Tag(layout_abstract_tag) => {
// Cache key in form namespace:name
let key = self.layout_name(&layout_abstract_tag.namespace[..], &layout_abstract_tag.name[..]);
let key = Self::component_name(&layout_abstract_tag.namespace[..], &layout_abstract_tag.name[..]);
if !already_loaded_layouts.contains(&key[..]) {
// Check if the cache has the loaded layout and load it if not
// Check if the cache has the loaded component and load it if not
match self.loaded_layouts.get(&key[..]) {
// Tag has not been loaded, so load it now
None => {
// Load the layout for the visited tag
let xml_path = self.layout_xml_path(&layout_abstract_tag.namespace[..], &layout_abstract_tag.name[..]);
let new_loaded_layout = self.parse_xml_file(&xml_path[..]).unwrap();
// Load and parse the XML file's AST for the visited tag
let xml_path = Self::layout_xml_path(&layout_abstract_tag.namespace[..], &layout_abstract_tag.name[..]);
let mut xml_ast = Self::parse_xml_tree(&self.attribute_parser, &xml_path[..], true, true).unwrap();
// Keep track of it being loaded to prevent duplicate work
let key_copy = key.clone();
already_loaded_layouts.insert(key);
// Recursively explore the newly loaded layout's tags
self.explore_referenced_layouts(&new_loaded_layout, already_loaded_layouts);
// Turn the entire XML AST into a component
let component = Self::component_ast_to_component(&mut xml_ast);
// Save the loaded layout to the cache
self.loaded_layouts.set(&key_copy[..], new_loaded_layout);
// Recursively explore the newly loaded AST's tags
self.explore_referenced_components(&xml_ast, already_loaded_layouts);
// Save the loaded component to the cache
self.loaded_layouts.set(&key_copy[..], component);
},
// Tag has already been loaded
Some(_) => {},
@ -74,8 +87,206 @@ impl LayoutSystem {
}
}
// Get the "namespace:name" format of string given a namespace and layout name
fn layout_name(&self, namespace: &str, name: &str) -> String {
/// Flatten a full XML component AST into a vector of the immediate children and put the descendants of those nodes into `content` attributes
fn component_ast_to_component(tree: &mut ComponentAst) -> Component {
println!("====> Flattening the following component AST to a component\n{:#?}\n", tree);
let result = tree
.children()
.map(|mut child| {
// Clone the abstract syntax node for this child (excluding the tree)
let mut cloned_child = child.borrow_mut().clone();
// If this is a node, stick its descendants into a new `content` attribute
match &mut cloned_child {
// Deeply clone the children and attach the tree to a new `content` attribute
LayoutAbstractNode::Tag(ref mut tag) => {
let ast_vector_in_tag = child.children().map(|mut c| c.make_deep_copy()).collect::<Vec<_>>();
let layout_type_value = TypeValueOrArgument::TypeValue(TypeValue::Layout(ast_vector_in_tag));
let type_value_in_vec = AttributeValue::TypeValue(vec![layout_type_value]);
let content_attribute = Attribute::new(String::from("content"), type_value_in_vec);
tag.add_attribute(content_attribute);
},
// Text nodes have no children
LayoutAbstractNode::Text(_) => {},
}
cloned_child
})
.collect::<Vec<_>>();
Self::print_layout_component(&result);
result
}
/// Get an AST root node representing a parsed XML component file or XML source code
pub fn parse_xml_tree(attribute_parser: &AttributeParser, path_or_source: &str, is_path_not_source: bool, component_declaration: bool) -> io::Result<ComponentAst> {
// XML component file markup source code
let (path, source) = if is_path_not_source {
(path_or_source, fs::read_to_string(path_or_source)?)
}
else {
(&"[Inline Attribute XML]"[..], String::from(path_or_source))
};
// XML document parser that feeds token-by-token through the file
let parser = xmlparser::Tokenizer::from(&source[..]);
// Node stack used to collect descendant nodes while reading deeper into the tree until each reaches its closing tag
let mut stack: Vec<ComponentAst> = Vec::new();
// Opening XML tag used to collect the tag name and its various attributes
let mut current_opening_tag: Option<LayoutAbstractNode> = None;
// Top-level node that is popped from the stack when the closing tag is reached at the end of the XML document
let mut final_result: Option<ComponentAst> = None;
for token_result in parser {
match token_result {
Ok(token) => {
match token {
// Beginning of an opening tag (<NAMESPACE:NAME ...)
xmlparser::Token::ElementStart { prefix, local, .. } => {
// Get the supplied namespace and tag name as owned strings
let namespace = String::from(prefix.as_str());
let tag_name = String::from(local.as_str());
// Construct an AST tag node with the namespace and tag name
let abstract_tag_node = LayoutAbstractNode::new_tag(namespace, tag_name);
// Store the AST node while attributes are added until the opening (or self-closing) tag ends
current_opening_tag = Some(abstract_tag_node);
},
// Any attributes within the current opening tag (... ATTRIBUTE="VALUE" ...)
xmlparser::Token::Attribute { prefix, local, value, .. } => {
// Check if the attribute has an empty prefix (thus, only a colon)
let colon_prefixed = prefix.start() > 0 && (prefix.start() == prefix.end());
// Set the name to the given name, possibly with a prepended colon
let name = if colon_prefixed {
let slice = local.as_str();
let mut string = String::with_capacity(slice.len() + 1);
string.push(':');
string.push_str(slice);
string
}
else {
String::from(local.as_str())
};
// Set the value to an ordinary string slice of the given value
let value = value.as_str();
// Attributes on the root element are parameter declarations that list the names and types of permitted variables
let attribute = if stack.is_empty() && component_declaration {
let parameter_declaration = attribute_parser.parse_attribute_declaration(value);
Attribute::new(name, parameter_declaration)
}
// Attributes on elements inside the root are arguments to the layout engine (no colon prefix) or the child component (colon prefix)
else {
let parameter_types = attribute_parser.parse_attribute_types(value);
Attribute::new(name, parameter_types)
};
// Add the new attribute to the current yet-to-be-closed element
match &mut current_opening_tag {
// The opening tag is indeed a tag AST node
Some(LayoutAbstractNode::Tag(tag)) => {
tag.add_attribute(attribute);
},
// Somehow the current opening tag is actually a text node (probably impossible)
Some(LayoutAbstractNode::Text(text)) => {
panic!(
"Unexpected text attribute {} attemping to be added to tag when parsing XML layout in component: {}",
text, path
);
},
// Somehow there is no current opening tag to add this attribute to (probably impossible)
None => {
panic!("Error adding attribute to tag when parsing XML layout in component: {}", path);
},
}
},
// Either the end of the opening tag (...>) or the end of a self-closing tag (.../>) or an entire closing tag (</NAMESPACE:NAME>)
xmlparser::Token::ElementEnd { end, .. } => {
match end {
// After adding any attributes, this element's opening tag ends (...>)
xmlparser::ElementEnd::Open => {
// After adding any attributes, we are now a layer deeper in the stack of yet-to-be-closed descendants
let current_abstract_node = current_opening_tag
.take()
.expect(&format!("Invalid syntax when parsing XML layout in component {}", path)[..]);
let tree_node_with_descendants = rctree::Node::new(current_abstract_node);
stack.push(tree_node_with_descendants);
},
// After adding any attributes, this element's self-closing tag ends (.../>)
xmlparser::ElementEnd::Empty => {
// Because a self-closing element does not go deeper, attach this now-complete node directly to its parent
let parent_node = stack.last_mut().expect(&format!("Invalid syntax when parsing XML layout in component: {}", path)[..]);
let current_abstract_node = current_opening_tag
.take()
.expect(&format!("Invalid syntax when parsing XML layout in component: {}", path)[..]);
let tree_node = rctree::Node::new(current_abstract_node);
parent_node.append(tree_node);
},
// After visiting any descendants inside the opening tag, finally the closing tag is reached (</NAMESPACE:NAME>)
xmlparser::ElementEnd::Close(..) => {
// Pop the element now that descendants have been parsed and we make our way back up the tree one level
let closed_node_with_descendants = stack
.pop()
.expect(&format!("Encountered extra closing tag when parsing XML layout in component: {}", path)[..]);
// Append this now-complete node to its parent, unless there is no parent, in which case we save this root node as the final result
match stack.last_mut() {
// If a parent node exists
Some(parent_node) => {
parent_node.append(closed_node_with_descendants);
},
// If this is the root node
None => {
match final_result {
// Save the root element as the final result
None => final_result = Some(closed_node_with_descendants),
// There can only be one root element in the XML document, but this isn't the first one encountered
Some(_) => panic!("Encountered multiple root-level tags when parsing XML layout in component: {}", path),
}
},
}
},
}
},
// A text node in the space between sibling elements (... SOME TEXT ...)
xmlparser::Token::Text { text } => {
// Trim any whitespace from around the string
let text_string = String::from(text.as_str().trim());
// If the string isn't all whitespace, append a new text node to the parent
if !text_string.is_empty() {
// Get the tree node which contains this text
let parent_node = stack
.last_mut()
.expect(&format!("Encountered text outside the root tag when parsing XML layout in component: {}", path)[..]);
// Construct an AST text node with the provided text
let abstract_text_node = LayoutAbstractNode::new_text(text_string);
// Put the AST text node in a new tree node
let new_tree_node = rctree::Node::new(abstract_text_node);
// Attach the new text node on the parent in the tree which contains this text
parent_node.append(new_tree_node);
}
},
_ => {},
}
},
Err(error) => {
panic!("Failed parsing XML syntax with error: {}", error);
},
}
}
// Return the final result or throw an error
match final_result {
None => panic!("Invalid syntax when parsing XML layout in component: {}", path),
Some(tree) => Ok(tree),
}
}
/// Get a string in `namespace:name` format (or just `name` for primitives) given a namespace and component name
fn component_name(namespace: &str, name: &str) -> String {
if namespace.len() > 0 {
format!("{}:{}", namespace, name)
}
@ -84,8 +295,8 @@ impl LayoutSystem {
}
}
// Get the XML file path given a namespace and layout name
fn layout_xml_path(&self, namespace: &str, name: &str) -> String {
/// Get the XML file path given a namespace and component name
fn layout_xml_path(namespace: &str, name: &str) -> String {
if namespace.len() > 0 {
format!("gui/{}/{}.xml", namespace, name)
}
@ -94,155 +305,44 @@ impl LayoutSystem {
}
}
// Get an abstract syntax tree root node representing a parsed XML layout file
fn parse_xml_file(&self, path: &str) -> io::Result<rctree::Node<LayoutAbstractNode>> {
// XML layout file markup source code
let source = fs::read_to_string(path)?;
// XML document parser that feeds token-by-token through the file
let parser = xmlparser::Tokenizer::from(&source[..]);
// Node stack used to collect descendant nodes while reading deeper into the tree until each reaches its closing tag
let mut stack: Vec<rctree::Node<LayoutAbstractNode>> = Vec::new();
// Opening XML tag used to collect the tag name and its various attributes
let mut current_opening_tag: Option<LayoutAbstractNode> = None;
// Top-level node that is popped from the stack when the closing tag is reached at the end of the XML document
let mut final_result: Option<rctree::Node<LayoutAbstractNode>> = None;
for token in parser {
match token.unwrap() {
// Beginning of an opening tag (<NAMESPACE:NAME ...)
xmlparser::Token::ElementStart { prefix, local, .. } => {
// Get the supplied namespace and tag name as owned strings
let namespace = String::from(prefix.as_str());
let tag_name = String::from(local.as_str());
// Construct an AST tag node with the namespace and tag name
let abstract_tag_node = LayoutAbstractNode::new_tag(namespace, tag_name);
// Store the AST node while attributes are added until the opening (or self-closing) tag ends
current_opening_tag = Some(abstract_tag_node);
},
// Any attributes within the current opening tag (... ATTRIBUTE="VALUE" ...)
xmlparser::Token::Attribute { prefix, local, value, .. } => {
// Check if the attribute has an empty prefix (thus, only a colon)
let colon_prefixed = prefix.start() > 0 && (prefix.start() == prefix.end());
// Set the name to the given name, possibly with a prepended colon
let name = if colon_prefixed {
let slice = local.as_str();
let mut string = String::with_capacity(slice.len() + 1);
string.push(':');
string.push_str(slice);
string
}
else {
String::from(local.as_str())
};
// Set the value to an ordinary string slice of the given value
let value = value.as_str();
// Attributes on the root element are parameter declarations that list the names and types of permitted variables
let attribute = if stack.is_empty() {
let parameter_declaration = self.attribute_parser.parse_attribute_declaration(value);
Attribute::new(name, parameter_declaration)
}
// Attributes on elements inside the root are arguments to the layout engine (no colon prefix) or the child layout (colon prefix)
else {
let parameter_types = self.attribute_parser.parse_attribute_types(value);
Attribute::new(name, parameter_types)
};
// Add the new attribute to the current yet-to-be-closed element
match &mut current_opening_tag {
// The opening tag is indeed a tag AST node
Some(LayoutAbstractNode::Tag(tag)) => {
tag.add_attribute(attribute);
},
// Somehow the current opening tag is actually a text node (probably impossible)
Some(LayoutAbstractNode::Text(text)) => {
panic!("Unexpected text attribute {} attemping to be added to tag when parsing XML layout in file: {}", text, path);
},
// Somehow there is no current opening tag to add this attribute to (probably impossible)
None => {
panic!("Error adding attribute to tag when parsing XML layout in file: {}", path);
},
}
},
// Either the end of the opening tag (...>) or the end of a self-closing tag (.../>) or an entire closing tag (</NAMESPACE:NAME>)
xmlparser::Token::ElementEnd { end, .. } => {
match end {
// After adding any attributes, this element's opening tag ends (...>)
xmlparser::ElementEnd::Open => {
// After adding any attributes, we are now a layer deeper in the stack of yet-to-be-closed descendants
let current_abstract_node = current_opening_tag.take().expect(&format!("Invalid syntax when parsing XML layout in file {}", path)[..]);
let tree_node_with_descendants = rctree::Node::new(current_abstract_node);
stack.push(tree_node_with_descendants);
},
// After adding any attributes, this element's self-closing tag ends (.../>)
xmlparser::ElementEnd::Empty => {
// Because a self-closing element does not go deeper, attach this now-complete node directly to its parent
let parent_node = stack.last_mut().expect(&format!("Invalid syntax when parsing XML layout in file: {}", path)[..]);
let current_abstract_node = current_opening_tag.take().expect(&format!("Invalid syntax when parsing XML layout in file: {}", path)[..]);
let tree_node = rctree::Node::new(current_abstract_node);
parent_node.append(tree_node);
},
// After visiting any descendants inside the opening tag, finally the closing tag is reached (</NAMESPACE:NAME>)
xmlparser::ElementEnd::Close(..) => {
// Pop the element now that descendants have been parsed and we make our way back up the tree one level
let closed_node_with_descendants = stack.pop().expect(&format!("Encountered extra closing tag when parsing XML layout in file: {}", path)[..]);
// Append this now-complete node to its parent, unless there is no parent, in which case we save this root node as the final result
match stack.last_mut() {
// If a parent node exists
Some(parent_node) => {
parent_node.append(closed_node_with_descendants);
},
// If this is the root node
None => {
match final_result {
// Save the root element as the final result
None => final_result = Some(closed_node_with_descendants),
// There can only be one root element in the XML document, but this isn't the first one encountered
Some(_) => panic!("Encountered multiple root-level tags when parsing XML layout in file: {}", path),
}
},
}
},
}
},
// A text node in the space between sibling elements (... SOME TEXT ...)
xmlparser::Token::Text { text } => {
// Trim any whitespace from around the string
let text_string = String::from(text.as_str().trim());
// If the string isn't all whitespace, append a new text node to the parent
if !text_string.is_empty() {
// Get the tree node which contains this text
let parent_node = stack
.last_mut()
.expect(&format!("Encountered text outside the root tag when parsing XML layout in file: {}", path)[..]);
// Construct an AST text node with the provided text
let abstract_text_node = LayoutAbstractNode::new_text(text_string);
// Put the AST text node in a new tree node
let new_tree_node = rctree::Node::new(abstract_text_node);
// Attach the new text node on the parent in the tree which contains this text
parent_node.append(new_tree_node);
}
},
_ => {},
}
}
match final_result {
None => panic!("Invalid syntax when parsing XML layout in file: {}", path),
Some(tree) => Ok(tree),
/// Print a component AST (for debugging)
fn print_layout_tree(tree_root: &ComponentAst) {
for node in tree_root.descendants() {
println!("Printing Component AST:\n{:#?}\n", node);
}
}
fn print_layout_tree(tree_root: &rctree::Node<LayoutAbstractNode>) {
for node in tree_root.descendants() {
println!("{:?}", node);
/// Print a component (for debugging)
fn print_layout_component(component: &Component) {
for node in component {
println!("Printing Component:\n{:#?}\n\n", node);
match node {
LayoutAbstractNode::Tag(tag) => {
let content = tag.attributes.iter().find(|a| a.name == "content");
match content {
Some(attribute) => match attribute.value {
AttributeValue::TypeValue(ref type_value) => {
for type_value_or_argument in type_value {
match type_value_or_argument {
TypeValueOrArgument::TypeValue(type_value) => match type_value {
TypeValue::Layout(layout) => {
for component_ast in layout {
Self::print_layout_tree(&component_ast);
}
},
_ => {},
},
TypeValueOrArgument::VariableArgument(_) => {},
}
}
},
AttributeValue::VariableParameter(_) => {},
},
None => {},
}
},
LayoutAbstractNode::Text(_) => {},
}
}
}
}