feat: xml

- add new feature: xml parser

Signed-off-by: Pakin <pakin.t@forth.co.th>
This commit is contained in:
Pakin 2026-04-17 11:05:17 +07:00
parent 6219459e3e
commit 21984bdfba
7 changed files with 409 additions and 6 deletions

View file

@ -2,3 +2,4 @@
pub mod models;
pub mod previews;
pub mod recipe_functions;
pub mod xml;

View file

@ -339,3 +339,12 @@ pub fn grep_latest_versions(dir_path: &str) -> Result<HashMap<String, usize>, st
Ok(vs)
}
pub fn read_tsv_file(path: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content.lines().map(|x| x.to_string()).collect())
}

1
src/xml/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod node;

158
src/xml/node.rs Normal file
View file

@ -0,0 +1,158 @@
use indexmap::IndexMap;
use quick_xml::events::Event;
use std::fs::File;
use std::io::Read;
#[derive(Debug, Clone, Default)]
pub struct Node {
pub name: String,
pub children: Vec<Node>,
pub value: Option<String>,
}
pub fn parse_xml_to_tree(xml: &str) -> Vec<Node> {
let mut reader = quick_xml::Reader::from_str(xml);
let mut stack: Vec<Node> = Vec::new();
let mut roots: Vec<Node> = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
stack.push(Node {
name,
children: Vec::new(),
value: None,
});
}
Ok(Event::End(_)) => {
if let Some(finished_node) = stack.pop() {
if let Some(parent) = stack.last_mut() {
parent.children.push(finished_node);
} else {
roots.push(finished_node);
}
}
}
Ok(Event::Text(e)) => {
if let Some(current_node) = stack.last_mut() {
let curr_text = String::from_utf8(e.clone().into_inner().to_vec()).unwrap();
// println!("detect text: {curr_text} -- {e:?}");
current_node.value = Some(curr_text);
}
}
Ok(Event::Eof) => break,
Err(e) => {
println!("error: {e:?}");
}
_ => {}
}
buf.clear();
}
roots
}
pub fn print_tree(nodes: &[Node], depth: usize) {
for node in nodes {
let indent = " ".repeat(depth);
match (&node.value, node.children.is_empty()) {
(Some(val), true) => {
println!("{}<{}>{}</{}>", indent, node.name, val, node.name);
}
(_, false) => {
println!("{}<{}>", indent, node.name);
print_tree(&node.children, depth + 1);
println!("{}</{}>", indent, node.name);
}
(None, true) => {
println!("{}<{}></{}>", indent, node.name, node.name);
}
}
}
}
impl Node {
pub fn find_by_child_value(
&self,
parent_name: &str,
child_name: &str,
target_value: &str,
) -> Vec<Node> {
let mut matches = Vec::new();
if self.name == parent_name {
if self.children.iter().any(|c| {
c.name == child_name && c.clone().value.is_some_and(|x| x.contains(target_value))
}) {
matches.push(self.clone());
}
}
for child in &self.children {
matches.extend(child.find_by_child_value(parent_name, child_name, target_value));
}
matches
}
pub fn get_child(&self, name: &str) -> Option<&Node> {
self.children.iter().find(|c| c.name == name)
}
}
pub fn generate_nodes_from_xml(
catalog_map: Vec<(&str, String)>,
) -> Result<IndexMap<String, Vec<Node>>, Box<dyn std::error::Error>> {
let mut result = IndexMap::new();
for catalog_m in catalog_map {
let catalog_name = catalog_m.0;
let catalog_path = catalog_m.1.clone();
let mut file = File::open(catalog_path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
// clean comment
let mut new_file = String::new();
for line in content.lines() {
if line.contains(";") && !line.contains(";include") {
continue;
} else {
new_file.push_str(format!("{line}\n").replace("&", "[amp]").as_str());
}
}
let node = parse_xml_to_tree(&new_file);
result.insert(catalog_name.to_string(), node);
}
Ok(result)
}
/// get node from path of node vector
///
/// Example:
/// ```
/// let current_menus_result: Option<&Node> = get_path!(root_node, ScrollableCatalog.Menus);
///
/// // Possible results
///
/// //Some(Node { name: "Menus", children: [Node { name: "Menu", children: [Node { name: "State",....
///
/// //None
/// ```
#[macro_export]
macro_rules! get_path {
($node:expr, $last:ident) => {
$node.get_child(stringify!($last))
};
// recursive case
($node:expr, $next:ident . $($rest:ident).+) => {
$node
.get_child(stringify!($next))
.and_then(|child| get_path!(child, $($rest).+))
};
}