From 3e12576b3cb678d0b18a312563c6d1fc674bc99e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 15 Jul 2020 01:11:52 -0700 Subject: [PATCH] Add conversion from component AST to flat component with `content` attribute (resolves #3), add XML attribute parsing, fix template string attribute parsing bug --- gui/README.md | 5 +- gui/box.xml | 4 +- gui/col.xml | 4 +- gui/header/window-buttons.xml | 2 +- gui/icon.xml | 4 +- gui/if.xml | 4 +- gui/input/checkbox-with-dropdown.xml | 2 +- gui/input/dropdown.xml | 2 +- gui/row.xml | 4 +- gui/text.xml | 2 +- gui/viewport/panels.xml | 2 +- src/application.rs | 2 +- src/color.rs | 2 +- src/layout_abstract_syntax.rs | 8 +- src/layout_abstract_types.rs | 28 +- src/layout_attribute_parser.rs | 54 +++- src/layout_system.rs | 446 ++++++++++++++++----------- 17 files changed, 352 insertions(+), 223 deletions(-) diff --git a/gui/README.md b/gui/README.md index c82485e6..6969428a 100644 --- a/gui/README.md +++ b/gui/README.md @@ -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 - + {{INNER_XML}} ``` -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 diff --git a/gui/box.xml b/gui/box.xml index 9d313b70..29034b60 100644 --- a/gui/box.xml +++ b/gui/box.xml @@ -1,3 +1,3 @@ - + {{INNER_XML}} - \ No newline at end of file + diff --git a/gui/col.xml b/gui/col.xml index abdf5037..015a1f8c 100644 --- a/gui/col.xml +++ b/gui/col.xml @@ -1,3 +1,3 @@ - + {{INNER_XML}} - \ No newline at end of file + diff --git a/gui/header/window-buttons.xml b/gui/header/window-buttons.xml index 623578f1..f594b9ef 100644 --- a/gui/header/window-buttons.xml +++ b/gui/header/window-buttons.xml @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/gui/icon.xml b/gui/icon.xml index 4a7b173d..bce3573c 100644 --- a/gui/icon.xml +++ b/gui/icon.xml @@ -1,3 +1,3 @@ - + {{INNER_XML}} - \ No newline at end of file + diff --git a/gui/if.xml b/gui/if.xml index daa93141..8d05e0c2 100644 --- a/gui/if.xml +++ b/gui/if.xml @@ -1,3 +1,3 @@ - + {{RESULT}} - \ No newline at end of file + diff --git a/gui/input/checkbox-with-dropdown.xml b/gui/input/checkbox-with-dropdown.xml index 7ac4112d..0c3e1e13 100644 --- a/gui/input/checkbox-with-dropdown.xml +++ b/gui/input/checkbox-with-dropdown.xml @@ -1,4 +1,4 @@ - + diff --git a/gui/input/dropdown.xml b/gui/input/dropdown.xml index 6d17827f..add69cdf 100644 --- a/gui/input/dropdown.xml +++ b/gui/input/dropdown.xml @@ -1,4 +1,4 @@ - + diff --git a/gui/row.xml b/gui/row.xml index 02eb8574..00190568 100644 --- a/gui/row.xml +++ b/gui/row.xml @@ -1,3 +1,3 @@ - + {{INNER_XML}} - \ No newline at end of file + diff --git a/gui/text.xml b/gui/text.xml index 5630e0aa..341d1aa2 100644 --- a/gui/text.xml +++ b/gui/text.xml @@ -1 +1 @@ - \ No newline at end of file + diff --git a/gui/viewport/panels.xml b/gui/viewport/panels.xml index a7ce1f62..a3c7b48c 100644 --- a/gui/viewport/panels.xml +++ b/gui/viewport/panels.xml @@ -4,4 +4,4 @@ Option B Option C - \ No newline at end of file + diff --git a/src/application.rs b/src/application.rs index 097ab5f2..aa5d0c13 100644 --- a/src/application.rs +++ b/src/application.rs @@ -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, diff --git a/src/color.rs b/src/color.rs index fb349ed9..f7c17fd6 100644 --- a/src/color.rs +++ b/src/color.rs @@ -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, diff --git a/src/layout_abstract_syntax.rs b/src/layout_abstract_syntax.rs index d094da83..91e4073c 100644 --- a/src/layout_abstract_syntax.rs +++ b/src/layout_abstract_syntax.rs @@ -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), diff --git a/src/layout_abstract_types.rs b/src/layout_abstract_types.rs index e2b7331d..4ef00cdc 100644 --- a/src/layout_abstract_types.rs +++ b/src/layout_abstract_types.rs @@ -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>, @@ -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; +pub type Component = Vec; + +#[derive(Debug, Clone, PartialEq)] pub enum TypeValue { - Layout(Vec>), + Layout(Vec), 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), diff --git a/src/layout_attribute_parser.rs b/src/layout_attribute_parser.rs index 4ca4f165..45b545c6 100644 --- a/src/layout_attribute_parser.rs +++ b/src/layout_attribute_parser.rs @@ -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::::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("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("apos;", "'") + .replace("'", "'") + } } diff --git a/src/layout_system.rs b/src/layout_system.rs index b3759506..23b3278a 100644 --- a/src/layout_system.rs +++ b/src/layout_system.rs @@ -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>, + loaded_layouts: ResourceCache, 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, already_loaded_layouts: &mut HashSet) { + /// 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) { 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::>(); + 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::>(); + 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 { + // 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 = Vec::new(); + // Opening XML tag used to collect the tag name and its various attributes + let mut current_opening_tag: Option = 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 = None; + + for token_result in parser { + match token_result { + Ok(token) => { + match token { + // Beginning of an opening tag ( { + // 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 () + 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 () + 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> { - // 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> = Vec::new(); - // Opening XML tag used to collect the tag name and its various attributes - let mut current_opening_tag: Option = 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> = None; - - for token in parser { - match token.unwrap() { - // Beginning of an opening tag ( { - // 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 () - 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 () - 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) { - 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(_) => {}, + } } } }