mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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:
parent
00cd62d411
commit
3e12576b3c
17 changed files with 352 additions and 223 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<col content="INNER_XML: (None) = none">
|
||||
<col content="INNER_XML: (Layout) = [[]]">
|
||||
{{INNER_XML}}
|
||||
</col>
|
||||
</col>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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']">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<row content="INNER_XML: (None) = none">
|
||||
<row content="INNER_XML: (Layout) = [[]]">
|
||||
{{INNER_XML}}
|
||||
</row>
|
||||
</row>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
Option B
|
||||
Option C
|
||||
</input:checkbox-with-dropdown>
|
||||
</viewport:panels>
|
||||
</viewport:panels>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"")
|
||||
.replace("apos;", "'")
|
||||
.replace("'", "'")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(_) => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue