libtbr/README.md
Pakin 21984bdfba feat: xml
- add new feature: xml parser

Signed-off-by: Pakin <pakin.t@forth.co.th>
2026-04-17 11:05:17 +07:00

10 KiB

libtbr

libtbr is swiss-knife toolbox for Taobin project.

cargo add --git https://gitlab.forthrd.io/Pakin/libtbr.git
cargo update

Examples

Initialize Config

use libtbr::recipe_functions::common;

// this read file `.tbcfg` in the current directory
let cfg = common::get_config();

let recipe_dir = cfg.get("RECIPE_DIR").unwrap();

Get recipe from specific country (latest)

...
let latest_versions = grep_latest_versions(recipe_dir).unwrap();

// try get malaysia recipe, may fail if country does not exist
let mys_version = latest_versions.get("mys");
// try to create malaysia recipe model
let mys_recipe_model = common::create_recipe_model_from_file(common::create_recipe_path(
    "mys",
    *mys_version.unwrap(),
));

...

Get list of material settings

// get current material settings including empty (0)
let mut used_in_mys = mys_recipe_model.list_material_settings();
used_in_mys.push("0".to_string());

Get all recipe of expected material id

let aus_latest = common::create_recipe_model_from_file(common::create_recipe_path(
    "aus",
    *aus_version.unwrap(),
));

let prod_list = aus_latest.find_recipe_by_material_path_id("511214");

Generate google sheet style table of recipe from file

// Experimental: expect current execution path to have `./test_result` folder first, and `config.RECIPE_DIR/{country}/{version_file_format}` must exist.
import::generate_recipe_sheet_table("mys", 626);

Notes

Simple Snippet Patterns

// =============================================
//  Get current material and convert to thai id
// =============================================
let curr_rpl_mat_id = rpl.materialPathId.as_i64().unwrap_or(0);

// strip off first 2
let pure_mat_id = if curr_rpl_mat_id > 300000 {
    //
    curr_rpl_mat_id.to_string()[2..]
        .parse::<i64>()
        .expect("not a number")
} else {
    curr_rpl_mat_id
};

XML Parser (Experimental)

This will parse xml file and create node structure type Vec<Node> where Node is from crate::xml::node

Xml-related functions

  • parse_xml_to_tree: this will parse raw xml string into node structure.
  • print_tree: printing node structure to xml
  • generate_nodes_from_xml: generate node index map from list of (catalog_name, catalog_path)

Node functions

  • find_by_child_value: search expected value from node (parent_node) which this value should be in node named child_name.
  • get_child: get child node from current node's children

Shortcut Macro

  • get_path: macro for accessing the node inner child by provided key key1.key2.key3...

Example of parsing xml layout v3 and generate into new-layout-v2 format

use libtbr::xml::*;


// ...

let taobin_dir = cfg
    .get("TAOBIN_REPO")
    .expect("Taobin directory path not provided");
let v3_dir = format!("{taobin_dir}/inter/ltu/xml/multi/v3");

// pre-defined paths configuration in format (catalog_name, catalog_path)
let v3_catalogs = vec![
    (
        "recommend",
        format!("{ltu_v3_dir}/event/event_v3/active_promotions.lxml"),
    ),
    (
        "coffee",
        format!("{ltu_v3_dir}/page_catalog_group_coffee.lxml"),
    ),
    ("milk", format!("{ltu_v3_dir}/page_catalog_group_milk.lxml")),
    ("tea", format!("{ltu_v3_dir}/page_catalog_group_tea.lxml")),
    (
        "health",
        format!("{ltu_v3_dir}/page_catalog_group_health.lxml"),
    ),
    (
        "other",
        format!("{ltu_v3_dir}/page_catalog_group_other.lxml"),
    ),
];

// input must be `Vec<(&str, String)>`
let mut v3_catalog_nodes: IndexMap<String, Vec<Node>> =
    generate_nodes_from_xml(v3_catalogs)?;

for (_, (catalog_name, catalog_nodes)) in v3_catalog_nodes.iter().enumerate() {
    if catalog_nodes.len() == 1
        && let Some(root_node) = catalog_nodes.first()
    {
        // get_path is a macro for accessing the node inner child
        //
        // usage: get_path!(root_node, key1.key2.key3...);
        //
        let current_menus_result: Option<&Node> = get_path!(root_node, ScrollableCatalog.Menus);
        if let Some(current_menus) = current_menus_result {
            println!(
                "Name={},file=page_catalog_group_{}.skt",
                catalog_name, catalog_name
            );

            let ccm = current_menus.clone();

            for menu_block in ccm.children.clone() {
                let mut name_row = String::from("\tname\t");
                let mut desc_row = String::from("\tdesc\t");
                let mut img_row = String::from("\timg\t");

                let tag_filter_option = get_path!(menu_block, TagFilter);
                let idle_image_tag = match get_path!(menu_block, IdleImage) {
                    Some(img_path) => {
                        let img_path = img_path.clone().value.unwrap_or("".to_string());
                        let img_path_spl: Vec<String> = img_path
                            .trim()
                            .replace("\"", "")
                            .split("/")
                            .map(|x| x.to_string())
                            .collect();
                        img_path_spl
                            .last()
                            .unwrap()
                            .replace("[amp]", "&")
                            .to_string()
                    }
                    None => "".to_string(),
                };

                img_row.push_str(format!("{idle_image_tag}\t-\t-\t-\t\t\t||||||||||||||||||||||||||\t||||||||||||||||||||||||||\t||||||||||||||||||||||||||\t\t\t\t\t\t\t\t-\t-\t-\t-\t-\t").as_str());

                let hot_state_val = match get_path!(menu_block, HotState) {
                    Some(state) => state
                        .clone()
                        .value
                        .and_then(|x| {
                            if x.to_string().contains("Disable2") {
                                return Some("-".to_string());
                            } else {
                                return Some(x.replace("$", "").replace(".Button", ""));
                            }
                        })
                        .unwrap()
                        .trim()
                        .to_string(),
                    None => "-".to_string(),
                };
                let ice_state_val = match get_path!(menu_block, IceState) {
                    Some(state) => state
                        .clone()
                        .value
                        .and_then(|x| {
                            if x.to_string().contains("Disable2") {
                                return Some("-".to_string());
                            } else {
                                return Some(x.replace("$", "").replace(".Button", ""));
                            }
                        })
                        .unwrap()
                        .trim()
                        .to_string(),
                    None => "-".to_string(),
                };
                let blend_state_val = match get_path!(menu_block, BlendState) {
                    Some(state) => state
                        .clone()
                        .value
                        .and_then(|x| {
                            if x.to_string().contains("Disable2") {
                                return Some("-".to_string());
                            } else {
                                return Some(x.replace("$", "").replace(".Button", ""));
                            }
                        })
                        .unwrap()
                        .trim()
                        .to_string(),
                    None => "-".to_string(),
                };

                let names = match get_path!(menu_block, Name.LanguageGroup) {
                    Some(names) => names.clone(),
                    None => Node::default(),
                };

                // Description
                let descs = match get_path!(menu_block, Description.LanguageGroup) {
                    Some(descs) => descs.clone(),
                    None => Node::default(),
                };

                for name in names.children.clone() {
                    if let Some(value) = name.value {
                        name_row.push_str(format!("{value}\t").replace("[amp]", "&").as_str());
                    } else {
                        name_row.push_str(format!("\t").as_str());
                    }
                }

                name_row.push_str(
                    format!(
                        "{},-\t{},-\t{},-\t\t\t\t\t\t\t\t-\t-\t-\t-\t{}",
                        hot_state_val,
                        ice_state_val,
                        blend_state_val,
                        tag_filter_option
                            .clone()
                            .unwrap_or(&Node::default())
                            .value
                            .clone()
                            .unwrap_or("-".to_string())
                            .trim()
                            .replace("\"", "")
                    )
                    .as_str(),
                );

                for desc in descs.children.clone() {
                    if let Some(value) = desc.value {
                        desc_row.push_str(format!("{value}\t").replace("[amp]", "&").as_str());
                    } else {
                        desc_row.push_str(format!("\t").as_str());
                    }
                }

                desc_row.push_str(
                    format!(
                        "||||||||||||||||||||||||||\t||||||||||||||||||||||||||\t||||||||||||||||||||||||||\t\t\t\t\t\t\t\t-\t-\t-\t-\t-\t"
                    )
                    .as_str(),
                );

                //||||||||||||||||||||||||||

                println!("{name_row}");
                println!("{desc_row}");
                println!("{img_row}");

                println!("");
            }

            // name	Americano	อเมริกาโน	Amerikano	Americano			59-01-01-0003,59-21-01-0003	59-01-02-0001,59-21-02-0001	-,-								-	-	Signature	-	CoffeeNoMilk,Recommend
            // desc	Espresso, Water	กาแฟ และน้ำ	Espresas, vanduo	Espresso, Apă			||||||||||||||||||||||||||	||||||||||||||||||||||||||	||||||||||||||||||||||||||								-	-	-	-	-
            // img	bn_hot_americano.png	-	bn_hot_america_no.png	bn_hot_america_no.png			posi1	||||||||||||||||||||||||||	||||||||||||||||||||||||||								-	-	-	-	-
        }
    }
}