mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
Add parsing of XML layout files into a ParsedLayoutNode tree
This commit is contained in:
parent
474b2b39f9
commit
f8025b15ea
13 changed files with 221 additions and 55 deletions
55
Cargo.lock
generated
55
Cargo.lock
generated
|
@ -434,9 +434,9 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
|
@ -538,9 +538,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -788,6 +788,7 @@ dependencies = [
|
|||
"rctree",
|
||||
"wgpu",
|
||||
"winit",
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1182,9 +1183,9 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b4b5f600e60dd3a147fb57b4547033d382d1979eb087af310e91cb45a63b1f4"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1226,9 +1227,9 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb44a25c5bba983be0fc8592dfaf3e6d0935ce8be0c6b15b2a39507af34a926"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
"synstructure",
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
@ -1292,9 +1293,9 @@ version = "0.4.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e58db2081ba5b4c93bd6be09c40fd36cb9193a8336c384f3b40012e531aa7e40"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1329,9 +1330,9 @@ checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.15"
|
||||
version = "0.5.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63"
|
||||
checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-nested"
|
||||
|
@ -1350,9 +1351,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.15"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a50b9351bfa8d65a7d93ce712dc63d2fd15ddbf2c36990fc7cac344859c04f"
|
||||
checksum = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101"
|
||||
dependencies = [
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
@ -1372,7 +1373,7 @@ version = "1.0.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1736,11 +1737,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.23"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269"
|
||||
checksum = "f87bc5b2815ebb664de0392fdf1b95b6d10e160f86d9f64ff65e5679841ca06a"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
@ -1751,9 +1752,9 @@ version = "0.12.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
||||
|
@ -1849,9 +1850,9 @@ dependencies = [
|
|||
"bumpalo",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
@ -1871,9 +1872,9 @@ version = "0.2.62"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.15",
|
||||
"proc-macro2 1.0.17",
|
||||
"quote 1.0.6",
|
||||
"syn 1.0.23",
|
||||
"syn 1.0.24",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
@ -2135,3 +2136,9 @@ name = "xml-rs"
|
|||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a"
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccb4240203dadf40be2de9369e5c6dec1bf427528115b030baca3334c18362d7"
|
||||
|
|
|
@ -17,3 +17,4 @@ palette = "0.5"
|
|||
futures = "0.3.4"
|
||||
bytemuck = "1.2.0"
|
||||
rctree = "0.3.3"
|
||||
xmlparser = "0.13.1"
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<header.file-menu>
|
||||
<header:file-menu>
|
||||
<box height="100%" x-padding="10px">
|
||||
<text height="100%" y-align="0.5" :color="mildwhite">፨</text>
|
||||
<text height="100%" y-align="0.5" :color="[mildwhite]">፨</text>
|
||||
</box>
|
||||
<box height="100%" x-padding="10px">
|
||||
<text height="100%" y-align="0.5" :color="mildwhite">File</text>
|
||||
<text height="100%" y-align="0.5" :color="[mildwhite]">File</text>
|
||||
</box>
|
||||
<box height="100%" x-padding="10px">
|
||||
<text height="100%" y-align="0.5" :color="mildwhite">Edit</text>
|
||||
<text height="100%" y-align="0.5" :color="[mildwhite]">Edit</text>
|
||||
</box>
|
||||
<box height="100%" x-padding="10px">
|
||||
<text height="100%" y-align="0.5" :color="mildwhite">Comp</text>
|
||||
<text height="100%" y-align="0.5" :color="[mildwhite]">Comp</text>
|
||||
</box>
|
||||
<box height="100%" x-padding="10px">
|
||||
<text height="100%" y-align="0.5" :color="mildwhite">View</text>
|
||||
<text height="100%" y-align="0.5" :color="[mildwhite]">View</text>
|
||||
</box>
|
||||
<box height="100%" x-padding="10px">
|
||||
<text height="100%" y-align="0.5" :color="mildwhite">Help</text>
|
||||
<text height="100%" y-align="0.5" :color="[mildwhite]">Help</text>
|
||||
</box>
|
||||
</header.file-menu>
|
||||
</header:file-menu>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<header.window-buttons :maximized="IS_MAXIMIZED">
|
||||
<header:window-buttons :maximized="IS_MAXIMIZED">
|
||||
<box height="100%" y-align="0.5" x-padding="18px">
|
||||
<icon :src="window_minimize.svg" />
|
||||
</box>
|
||||
|
@ -13,4 +13,4 @@
|
|||
<box height="100%" y-align="0.5" x-padding="18px">
|
||||
<icon :src="window_close.svg" />
|
||||
</box>
|
||||
</header.window-buttons>
|
||||
</header:window-buttons>
|
|
@ -1,12 +1,12 @@
|
|||
<input.checkbox-with-dropdown content="OPTION_LIST" :disabled="DISABLED = false" :checked="CHECKED = false" :default="DEFAULT = 0">
|
||||
<input:checkbox-with-dropdown content="OPTION_LIST" :disabled="DISABLED = false" :checked="CHECKED = false" :default="DEFAULT = 0">
|
||||
<!-- Checkbox -->
|
||||
<col width="height" height="100%">
|
||||
<box width="100%" height="100%" x-align="0.5" y-align="0.5" :fill="[accent]">
|
||||
<input.checkbox :checked="{{CHECKED}}" :disabled="{{DISABLED}}" :inverted="true" />
|
||||
<input:checkbox :checked="{{CHECKED}}" :disabled="{{DISABLED}}" :inverted="true" />
|
||||
</box>
|
||||
</col>
|
||||
<!-- Dropdown -->
|
||||
<col width="100%">
|
||||
<input.dropdown width="100%" :default="{{DEFAULT}}" :disabled="{{DISABLED}}">{{OPTION_LIST}}</input.dropdown>
|
||||
<input:dropdown width="100%" :default="{{DEFAULT}}" :disabled="{{DISABLED}}">{{OPTION_LIST}}</input:dropdown>
|
||||
</col>
|
||||
</input.checkbox-with-dropdown>
|
||||
</input:checkbox-with-dropdown>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<input.checkbox :checked="CHECKED = false" :disabled="DISABLED = false" :inverted="INVERTED = false">
|
||||
<input:checkbox :checked="CHECKED = false" :disabled="DISABLED = false" :inverted="INVERTED = false">
|
||||
<box width="14px" height="14px" :fill="{{BOX_COLOR}}" :round="2px">
|
||||
<if :a="{{CHECKED}}">
|
||||
<if :a="{{INVERTED}}" :b="false">
|
||||
|
@ -17,4 +17,4 @@
|
|||
</if>
|
||||
</if>
|
||||
</box>
|
||||
</input.checkbox>
|
||||
</input:checkbox>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<input.dropdown content="OPTION_LIST" :default="DEFAULT = 0" :disabled="DISABLED = false">
|
||||
<input:dropdown content="OPTION_LIST" :default="DEFAULT = 0" :disabled="DISABLED = false">
|
||||
<box width="100%" :round="4px">
|
||||
<!-- Current selection text -->
|
||||
<col width="100%" height="24px">
|
||||
<text width="100%" height="100%" x-align="0.5" y-align="0.5" :color="mildwhite">{{CURRENT_TEXT}}</text>
|
||||
<text width="100%" height="100%" x-align="0.5" y-align="0.5" :color="[mildwhite]">{{CURRENT_TEXT}}</text>
|
||||
</col>
|
||||
<!-- Dropdown arrow icon -->
|
||||
<col width="8px" height="100%">
|
||||
|
@ -13,4 +13,4 @@
|
|||
</icon>
|
||||
</col>
|
||||
</box>
|
||||
</input.dropdown>
|
||||
</input:dropdown>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<viewport.panels>
|
||||
<viewport:panels>
|
||||
<checkbox-with-dropdown :checked="true" :default="2">
|
||||
<dropdown-option>Option A</dropdown-option>
|
||||
<dropdown-option>Option B</dropdown-option>
|
||||
<dropdown-option>Option C</dropdown-option>
|
||||
</checkbox-with-dropdown>
|
||||
</viewport.panels>
|
||||
</viewport:panels>
|
|
@ -1,18 +1,18 @@
|
|||
<!-- Called as <window.main width="1920px" height="1080px" /> -->
|
||||
<window.main>
|
||||
<!-- Called as <window:main width="1920px" height="1080px" /> -->
|
||||
<window:main>
|
||||
<!-- Header -->
|
||||
<row width="100%" height="28px">
|
||||
<header.file-menu height="100%" x-align="0" />
|
||||
<text height="100%" y-align="0.5" x-align="0.5" :color="mildwhite">Document 1* - Graphite</text>
|
||||
<header.window-buttons height="100%" x-align="1" />
|
||||
<header:file-menu height="100%" x-align="0" />
|
||||
<text height="100%" y-align="0.5" x-align="0.5" :color="[mildwhite]">Document 1* - Graphite</text>
|
||||
<header:window-buttons height="100%" x-align="1" />
|
||||
</row>
|
||||
<!-- Viewport -->
|
||||
<row width="100%" height="100@">
|
||||
<viewport.panels />
|
||||
<viewport:panels />
|
||||
</row>
|
||||
<!-- Footer -->
|
||||
<row width="100%" height="24px">
|
||||
<text height="100%" y-align="0.5" x-align="0" x-padding="10px" :color="mildwhite" :size="14px">File: 1.8 MB | Memory: 137 MB | Scratch: 0.7/12.3 GB</text>
|
||||
<text height="100%" y-align="0.5" x-align="1" x-padding="10px" :color="mildwhite" :size="14px">🖰 Box Select Objects | [⇧G] Move Selection | [⇧R] Rotate Selection | [⇧S] Scale Selection</text>
|
||||
<text height="100%" y-align="0.5" x-align="0" x-padding="10px" :color="[mildwhite]" :size="14px">File: 1.8 MB | Memory: 137 MB | Scratch: 0.7/12.3 GB</text>
|
||||
<text height="100%" y-align="0.5" x-align="1" x-padding="10px" :color="[mildwhite]" :size="14px">🖰 Box Select Objects | [⇧G] Move Selection | [⇧R] Rotate Selection | [⇧S] Scale Selection</text>
|
||||
</row>
|
||||
</window.main>
|
||||
</window:main>
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::window_events;
|
|||
use crate::pipeline::Pipeline;
|
||||
use crate::texture::Texture;
|
||||
use crate::resource_cache::ResourceCache;
|
||||
use crate::draw_command::DrawCommand;
|
||||
use crate::component_layout::ComponentLayout;
|
||||
use crate::gui_node::GuiNode;
|
||||
use winit::event::*;
|
||||
use winit::event_loop::*;
|
||||
|
@ -75,6 +75,8 @@ impl Application {
|
|||
let gui_root_data = GuiNode::new(swap_chain_descriptor.width, swap_chain_descriptor.height, ColorPalette::get_color_srgb(ColorPalette::Accent));
|
||||
let gui_root = rctree::Node::new(gui_root_data);
|
||||
|
||||
ComponentLayout::new();
|
||||
|
||||
Self {
|
||||
surface,
|
||||
adapter,
|
||||
|
|
116
src/component_layout.rs
Normal file
116
src/component_layout.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use std::fs;
|
||||
use std::io;
|
||||
use crate::parsed_layout_node::*;
|
||||
|
||||
pub struct ComponentLayout {
|
||||
|
||||
}
|
||||
|
||||
impl ComponentLayout {
|
||||
pub fn new() -> ComponentLayout {
|
||||
let parsed_layout_tree = Self::parse_xml_file("gui/window/main.xml").unwrap();
|
||||
for node in parsed_layout_tree.descendants() {
|
||||
println!("{:?}", node);
|
||||
}
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn parse_xml_file(path: &str) -> io::Result<rctree::Node<ParsedLayoutNode>> {
|
||||
let source = fs::read_to_string(path)?;
|
||||
let parsed = xmlparser::Tokenizer::from(&source[..]);
|
||||
|
||||
let mut stack: Vec<rctree::Node<ParsedLayoutNode>> = Vec::new();
|
||||
let mut current: Option<rctree::Node<ParsedLayoutNode>> = None;
|
||||
let mut result: Option<rctree::Node<ParsedLayoutNode>> = None;
|
||||
|
||||
for token in parsed {
|
||||
match token.unwrap() {
|
||||
xmlparser::Token::ElementStart { prefix, local, .. } => {
|
||||
let namespace = String::from(prefix.as_str());
|
||||
let tag_name = String::from(local.as_str());
|
||||
|
||||
let new_parsed_layout_node = ParsedLayoutNode::new_tag(namespace, tag_name);
|
||||
|
||||
let new_node = rctree::Node::new(new_parsed_layout_node);
|
||||
current = Some(new_node);
|
||||
}
|
||||
xmlparser::Token::Attribute { prefix, local, value, .. } => {
|
||||
let colon_prefixed = prefix.start() > 0 && (prefix.start() == prefix.end());
|
||||
let key = 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()) };
|
||||
let value = String::from(value.as_str());
|
||||
let attribute = (key, value);
|
||||
|
||||
match &mut current {
|
||||
Some(current_node) => {
|
||||
match &mut *current_node.borrow_mut() {
|
||||
ParsedLayoutNode::Tag(tag) => {
|
||||
// Add this attribute to the current node that has not yet reached its closing angle bracket
|
||||
tag.add_attribute(attribute);
|
||||
}
|
||||
ParsedLayoutNode::Text(_) => {
|
||||
panic!("Error adding attribute to tag when parsing XML layout in file: {}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
panic!("Error adding attribute to tag when parsing XML layout in file: {}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
xmlparser::Token::ElementEnd { end, .. } => {
|
||||
match end {
|
||||
// After adding any attributes, the opening tag ends
|
||||
xmlparser::ElementEnd::Open => {
|
||||
// After adding any attributes, we are now a layer deeper which the stack keeps track of
|
||||
let node_to_push = current.take().expect(&format!("Invalid syntax when parsing XML layout in file {}", path)[..]);
|
||||
stack.push(node_to_push);
|
||||
}
|
||||
// After adding any attributes, the self-closing tag ends
|
||||
xmlparser::ElementEnd::Empty => {
|
||||
let parent_node = stack.last_mut().expect(&format!("Invalid syntax when parsing XML layout in file: {}", path)[..]);
|
||||
let new_child = current.take().expect(&format!("Invalid syntax when parsing XML layout in file: {}", path)[..]);
|
||||
parent_node.append(new_child);
|
||||
}
|
||||
// The closing tag is reached
|
||||
xmlparser::ElementEnd::Close(..) => {
|
||||
let popped_node = stack.pop().expect(&format!("Encountered extra closing tag when parsing XML layout in file: {}", path)[..]);
|
||||
match stack.last_mut() {
|
||||
Some(parent_node) => {
|
||||
parent_node.append(popped_node);
|
||||
}
|
||||
None => {
|
||||
match result {
|
||||
None => result = Some(popped_node),
|
||||
Some(_) => panic!("Encountered multiple root-level tags when parsing XML layout in file: {}", path),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
xmlparser::Token::Text { text } => {
|
||||
let parent_node = stack.last_mut().expect(&format!("Encountered text outside the root tag when parsing XML layout in file: {}", path)[..]);
|
||||
let text_string = String::from(text.as_str());
|
||||
|
||||
if !text_string.trim().is_empty() {
|
||||
let text_node = ParsedLayoutNode::new_text(text_string);
|
||||
let new_node = rctree::Node::new(text_node);
|
||||
parent_node.append(new_node);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match result {
|
||||
None => panic!("Invalid syntax when parsing XML layout in file: {}", path),
|
||||
Some(tree) => Ok(tree)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ mod draw_command;
|
|||
mod gui_node;
|
||||
mod gui_attributes;
|
||||
mod window_events;
|
||||
mod component_layout;
|
||||
mod parsed_layout_node;
|
||||
|
||||
use application::Application;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
|
38
src/parsed_layout_node.rs
Normal file
38
src/parsed_layout_node.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
#[derive(Debug)]
|
||||
pub enum ParsedLayoutNode {
|
||||
Tag(ParsedLayoutTag),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl ParsedLayoutNode {
|
||||
pub fn new_tag(namespace: String, tag: String) -> Self {
|
||||
Self::Tag(ParsedLayoutTag::new(namespace, tag))
|
||||
}
|
||||
|
||||
pub fn new_text(text: String) -> Self {
|
||||
Self::Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedLayoutTag {
|
||||
pub namespace: Option<String>,
|
||||
pub tag: String,
|
||||
pub attributes: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl ParsedLayoutTag {
|
||||
pub fn new(namespace: String, tag: String) -> Self {
|
||||
let namespace = if namespace.is_empty() { None } else { Some(namespace) };
|
||||
|
||||
Self {
|
||||
namespace,
|
||||
tag,
|
||||
attributes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_attribute(&mut self, attribute: (String, String)) {
|
||||
self.attributes.push(attribute);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue