From 5510d27904307e9fb029c8505ef11d4d87931a70 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Thu, 10 Apr 2025 00:43:21 +0900 Subject: [PATCH] rcxml: convert dotted properties into nested nodes before processing For example, the following node: is converted to: ShowMenu root-menu 1 2 ...before processing the entire xml tree. This is a preparation to prevent breaking changes when we allow parsing nested 'If' actions (#2427) or when we drastically refactor rcxml.c to use recursion instead of encoding. --- include/common/xml.h | 29 ++++++++++ src/common/meson.build | 1 + src/common/xml.c | 127 +++++++++++++++++++++++++++++++++++++++++ src/config/rcxml.c | 5 +- t/meson.build | 11 +++- t/xml.c | 120 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 include/common/xml.h create mode 100644 src/common/xml.c create mode 100644 t/xml.c diff --git a/include/common/xml.h b/include/common/xml.h new file mode 100644 index 00000000..a22699f7 --- /dev/null +++ b/include/common/xml.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_XML_H +#define LABWC_XML_H + +#include + +/* + * Converts dotted properties into nested nodes. + * For example, the following node: + * + * + * + * is converted to: + * + * + * + * ShowMenu + * root-menu + * + * 1 + * 2 + * + * + * + */ +void lab_xml_expand_dotted_props(xmlNode *root); + +#endif /* LABWC_XML_H */ diff --git a/src/common/meson.build b/src/common/meson.build index 15910f32..c57b41f2 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -23,4 +23,5 @@ labwc_sources += files( 'surface-helpers.c', 'spawn.c', 'string-helpers.c', + 'xml.c', ) diff --git a/src/common/xml.c b/src/common/xml.c new file mode 100644 index 00000000..203e56ba --- /dev/null +++ b/src/common/xml.c @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include "common/xml.h" + +/* + * Converts a property A.B.C="X" into X + */ +static xmlNode* +create_prop_tree(const xmlAttr *prop) +{ + if (!strchr((char *)prop->name, '.')) { + return NULL; + } + + gchar **parts = g_strsplit((char *)prop->name, ".", -1); + int length = g_strv_length(parts); + xmlNode *root_node = NULL; + xmlNode *parent_node = NULL; + xmlNode *current_node = NULL; + + for (int i = length - 1; i >= 0; i--) { + gchar *part = parts[i]; + if (!*part) { + /* Ignore empty string */ + continue; + } + current_node = xmlNewNode(NULL, (xmlChar *)part); + if (parent_node) { + xmlAddChild(parent_node, current_node); + } else { + root_node = current_node; + } + parent_node = current_node; + } + + xmlChar *content = xmlNodeGetContent(prop->children); + xmlNodeSetContent(current_node, content); + xmlFree(content); + + g_strfreev(parts); + return root_node; +} + +/* + * Consider . + * These three properties are represented by following trees. + * action(dst)---name + * action(src)---position---x + * action--------position---y + * When we call merge_two_trees(dst, src), we walk over the trees from their + * roots towards leaves, and merge the identical node 'action' like: + * action(dst)---name + * \--------position---x + * action(src)---position---y + * And when we call merge_two_trees(dst, src) again, we walk over the dst tree + * like 'action'->'position'->'x' and the src tree like 'action'->'position'->'y'. + * First, we merge the identical node 'action' again like: + * action---name + * \---position(dst)---x + * \--position(src)---y + * Next, we merge the identical node 'position' like: + * action---name + * \---position---x + * \----y + */ +static bool +merge_two_trees(xmlNode *dst, xmlNode *src) +{ + bool merged = false; + + while (dst && src && src->children + && !strcasecmp((char *)dst->name, (char *)src->name)) { + xmlNode *next_dst = dst->last; + xmlNode *next_src = src->children; + xmlAddChild(dst, src->children); + xmlUnlinkNode(src); + xmlFreeNode(src); + src = next_src; + dst = next_dst; + merged = true; + } + + return merged; +} + +void +lab_xml_expand_dotted_props(xmlNode *parent) +{ + xmlNode *old_first_child = parent->children; + xmlNode *prev_tree = NULL; + + if (parent->type != XML_ELEMENT_NODE) { + return; + } + + for (xmlAttr *prop = parent->properties; prop;) { + /* Convert the property with dots into an xml tree */ + xmlNode *tree = create_prop_tree(prop); + if (!tree) { + /* The property doesn't contain dots */ + prev_tree = NULL; + prop = prop->next; + continue; + } + + /* Try to merge the tree with the previous one */ + if (!merge_two_trees(prev_tree, tree)) { + /* If not merged, add the tree as a new child */ + if (old_first_child) { + xmlAddPrevSibling(old_first_child, tree); + } else { + xmlAddChild(parent, tree); + } + prev_tree = tree; + } + + xmlAttr *next_prop = prop->next; + xmlRemoveProp(prop); + prop = next_prop; + } + + for (xmlNode *node = parent->children; node; node = node->next) { + lab_xml_expand_dotted_props(node); + } +} diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 419e8daa..1a01739b 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -25,6 +25,7 @@ #include "common/parse-double.h" #include "common/string-helpers.h" #include "common/three-state.h" +#include "common/xml.h" #include "config/default-bindings.h" #include "config/keybind.h" #include "config/libinput.h" @@ -1414,7 +1415,9 @@ rcxml_parse_xml(struct buf *b) return; } struct parser_state init_state = {0}; - xml_tree_walk(xmlDocGetRootElement(d), &init_state); + xmlNode *root = xmlDocGetRootElement(d); + lab_xml_expand_dotted_props(root); + xml_tree_walk(root, &init_state); xmlFreeDoc(d); xmlCleanupParser(); } diff --git a/t/meson.build b/t/meson.build index eb997fe2..66534a2c 100644 --- a/t/meson.build +++ b/t/meson.build @@ -3,15 +3,21 @@ test_lib = static_library( sources: files( '../src/common/buf.c', '../src/common/mem.c', - '../src/common/string-helpers.c' + '../src/common/string-helpers.c', + '../src/common/xml.c', ), include_directories: [labwc_inc], - dependencies: [dep_cmocka], + dependencies: [ + dep_cmocka, + glib, + xml2, + ], ) tests = [ 'buf-simple', 'str', + 'xml', ] foreach t : tests @@ -22,6 +28,7 @@ foreach t : tests sources: '@0@.c'.format(t), include_directories: [labwc_inc], link_with: [test_lib], + dependencies: [xml2], ), is_parallel: false, ) diff --git a/t/xml.c b/t/xml.c new file mode 100644 index 00000000..be71581d --- /dev/null +++ b/t/xml.c @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include "common/macros.h" +#include "common/xml.h" + +struct test_case { + const char *before, *after; +} test_cases[] = {{ + "", + + "" + "" + "ShowMenu" + "root-menu" + "" + "1" + "2" + "" + "" + "" +}, { + "", + + "" + "111" + "222" + "333" + "" +}, { + "", + + "" + "111" + "222" + "333" + "" +}, { + "", + + "" + "111" + "222" + "" +}, { + "", + + "" + "111" + "333" + "", +}, { + "" + "" + "" + "", + + "" + "111" + "111" + "", +}, { + "" + "222" + "", + + "" + "111" + "222" + "", +}, { + "" + "111" + "111" + "", + + "" + "111" + "111" + "", +}, { + "", + + "111" +}}; + +static void +test_lab_xml_expand_dotted_props(void **state) +{ + (void)state; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + xmlDoc *doc = xmlReadDoc((xmlChar *)test_cases[i].before, + NULL, NULL, 0); + xmlNode *root = xmlDocGetRootElement(doc); + + lab_xml_expand_dotted_props(root); + + xmlBuffer *buf = xmlBufferCreate(); + xmlNodeDump(buf, root->doc, root, 0, 0); + assert_string_equal(test_cases[i].after, (char *)buf->content); + xmlBufferFree(buf); + + xmlFreeDoc(doc); + } +} + +int main(int argc, char **argv) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_lab_xml_expand_dotted_props), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}