From 503af1050517683a7116070bb148c19a7035813d 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 refactor rcxml.c to use recursion instead of encoding nodes into dotted strings. --- include/common/xml.h | 29 +++++++++ src/common/meson.build | 1 + src/common/xml.c | 131 +++++++++++++++++++++++++++++++++++++++++ src/config/rcxml.c | 5 +- t/meson.build | 11 +++- t/xml.c | 128 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 302 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..7bc8eb26 --- /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 attributes into nested nodes. + * For example, the following node: + * + * + * + * is converted to: + * + * + * + * ShowMenu + * root-menu + * + * 1 + * 2 + * + * + * + */ +void lab_xml_expand_dotted_attributes(xmlNode *root); + +#endif /* LABWC_XML_H */ diff --git a/src/common/meson.build b/src/common/meson.build index 1e226297..aa0fc413 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..08ec18b6 --- /dev/null +++ b/src/common/xml.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include "common/xml.h" + +/* + * Converts an attribute A.B.C="X" into X + */ +static xmlNode * +create_attribute_tree(const xmlAttr *attr) +{ + gchar **parts = g_strsplit((char *)attr->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; + } + + /* + * Note: empty attributes or attributes with only dots are forbidden + * and root_node becomes never NULL here. + */ + assert(root_node); + + xmlChar *content = xmlNodeGetContent(attr->children); + xmlNodeSetContent(current_node, content); + xmlFree(content); + + g_strfreev(parts); + return root_node; +} + +/* + * Consider . + * These three attributes are represented by following trees. + * action(dst)---name + * action(src)---position---x + * action--------position---y + * When we call merge_two_trees(dst, src) for the first 2 trees above, 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_attributes(xmlNode *parent) +{ + xmlNode *old_first_child = parent->children; + xmlNode *prev_tree = NULL; + + if (parent->type != XML_ELEMENT_NODE) { + return; + } + + for (xmlAttr *attr = parent->properties; attr;) { + /* Convert the attribute with dots into an xml tree */ + xmlNode *tree = create_attribute_tree(attr); + if (!tree) { + /* The attribute doesn't contain dots */ + prev_tree = NULL; + attr = attr->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_attr = attr->next; + xmlRemoveProp(attr); + attr = next_attr; + } + + for (xmlNode *node = parent->children; node; node = node->next) { + lab_xml_expand_dotted_attributes(node); + } +} diff --git a/src/config/rcxml.c b/src/config/rcxml.c index e8c4185e..c3abbf93 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" @@ -1509,7 +1510,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_attributes(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..003632a6 --- /dev/null +++ b/t/xml.c @@ -0,0 +1,128 @@ +// 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" + "" +}, { + "", + + "" + "111" + "222" + "333" + "" +}, { + "", + + "" + "111" + "222" + "333" + "" +}, { + "", + + "" + "111" + "222" + "" +}, { + "", + + "" + "111" + "222" + "333" + "", +}, { + "" + "" + "" + "", + + "" + "111" + "111" + "", +}, { + "" + "222" + "", + + "" + "111" + "222" + "", +}, { + "" + "111" + "111" + "", + + "" + "111" + "111" + "", +}, { + "", + + "111" +}}; + +static void +test_lab_xml_expand_dotted_attributes(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_attributes(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_attributes), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}