]> git.mdlowis.com Git - proto/labwc.git/commitdiff
rcxml: convert dotted properties into nested nodes before processing
authortokyo4j <hrak1529@gmail.com>
Wed, 9 Apr 2025 15:43:21 +0000 (00:43 +0900)
committerJohan Malm <johanmalm@users.noreply.github.com>
Wed, 30 Jul 2025 19:36:27 +0000 (20:36 +0100)
For example, the following node:

  <keybind name.action="ShowMenu" menu.action="root-menu"
            x.position.action="1" y.position.action="2" />

is converted to:

 <keybind>
   <action>
     <name>ShowMenu</name>
     <menu>root-menu</menu>
     <position>
       <x>1</x>
       <y>2</y>
     </position>
   </action>
 </keybind>

...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 [new file with mode: 0644]
src/common/meson.build
src/common/xml.c [new file with mode: 0644]
src/config/rcxml.c
t/meson.build
t/xml.c [new file with mode: 0644]

diff --git a/include/common/xml.h b/include/common/xml.h
new file mode 100644 (file)
index 0000000..7bc8eb2
--- /dev/null
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef LABWC_XML_H
+#define LABWC_XML_H
+
+#include <libxml/tree.h>
+
+/*
+ * Converts dotted attributes into nested nodes.
+ * For example, the following node:
+ *
+ *   <keybind name.action="ShowMenu" menu.action="root-menu"
+ *            x.position.action="1" y.position.action="2" />
+ *
+ * is converted to:
+ *
+ *   <keybind>
+ *     <action>
+ *       <name>ShowMenu</name>
+ *       <menu>root-menu</menu>
+ *       <position>
+ *         <x>1</x>
+ *         <y>2</y>
+ *       </position>
+ *     </action>
+ *   </keybind>
+ */
+void lab_xml_expand_dotted_attributes(xmlNode *root);
+
+#endif /* LABWC_XML_H */
index 1e226297a465e4965a47d8a60993f713085678a9..aa0fc41331f46aae2806fbf885fd142eff4c7f3a 100644 (file)
@@ -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 (file)
index 0000000..08ec18b
--- /dev/null
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <assert.h>
+#include <glib.h>
+#include <stdbool.h>
+#include <strings.h>
+#include "common/xml.h"
+
+/*
+ * Converts an attribute A.B.C="X" into <C><B><A>X</A></B></C>
+ */
+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 <keybind name.action="ShowMenu" x.position.action="1" y.position="2" />.
+ * 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);
+       }
+}
index e8c4185e74c9cb874f57e30790ffcb07275a8efc..c3abbf930eb066a0245043f6f99a7fce3cb03c01 100644 (file)
@@ -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();
 }
index eb997fe299ac71db499a07b47fa94f2e646c4497..66534a2c56020219a97e7d936f5160041ea2c2d1 100644 (file)
@@ -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 (file)
index 0000000..003632a
--- /dev/null
+++ b/t/xml.c
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define _POSIX_C_SOURCE 200809L
+#include <setjmp.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <cmocka.h>
+#include "common/macros.h"
+#include "common/xml.h"
+
+struct test_case {
+       const char *before, *after;
+} test_cases[] = {{
+       "<keybind name.action='ShowMenu' menu.action='root-menu' "
+               "x.position.action='1' y.position.action='2'/>",
+
+       "<keybind>"
+               "<action>"
+                       "<name>ShowMenu</name>"
+                       "<menu>root-menu</menu>"
+                       "<position>"
+                               "<x>1</x>"
+                               "<y>2</y>"
+                       "</position>"
+               "</action>"
+       "</keybind>"
+}, {
+       "<AAA aaa='111' bbb='222'/>",
+
+       "<AAA>"
+               "<aaa>111</aaa>"
+               "<bbb>222</bbb>"
+       "</AAA>"
+}, {
+       "<AAA aaa.bbb.ccc='111' ddd.ccc='222' eee.bbb.ccc='333'/>",
+
+       "<AAA><ccc>"
+               "<bbb><aaa>111</aaa></bbb>"
+               "<ddd>222</ddd>"
+               "<bbb><eee>333</eee></bbb>"
+       "</ccc></AAA>"
+}, {
+       "<AAA aaa.bbb.ccc='111' bbb.ccc='222' ddd.bbb.ccc='333'/>",
+
+       "<AAA><ccc><bbb>"
+               "<aaa>111</aaa>"
+               "222"
+               "<ddd>333</ddd>"
+       "</bbb></ccc></AAA>"
+}, {
+       "<AAA aaa.bbb='111' aaa.ddd='222'/>",
+
+       "<AAA>"
+               "<bbb><aaa>111</aaa></bbb>"
+               "<ddd><aaa>222</aaa></ddd>"
+       "</AAA>"
+}, {
+       "<AAA aaa.bbb='111' bbb='222' ccc.bbb='333'/>",
+
+       "<AAA><bbb>"
+               "<aaa>111</aaa>"
+               "222"
+               "<ccc>333</ccc>"
+       "</bbb></AAA>",
+}, {
+       "<AAA>"
+               "<BBB aaa.bbb='111'/>"
+               "<BBB aaa.bbb='111'/>"
+       "</AAA>",
+
+       "<AAA>"
+               "<BBB><bbb><aaa>111</aaa></bbb></BBB>"
+               "<BBB><bbb><aaa>111</aaa></bbb></BBB>"
+       "</AAA>",
+}, {
+       "<AAA bbb.ccc='111'>"
+               "<BBB>222</BBB>"
+       "</AAA>",
+
+       "<AAA>"
+               "<ccc><bbb>111</bbb></ccc>"
+               "<BBB>222</BBB>"
+       "</AAA>",
+}, {
+       "<AAA>"
+               "<BBB><CCC>111</CCC></BBB>"
+               "<BBB><CCC>111</CCC></BBB>"
+       "</AAA>",
+
+       "<AAA>"
+               "<BBB><CCC>111</CCC></BBB>"
+               "<BBB><CCC>111</CCC></BBB>"
+       "</AAA>",
+}, {
+       "<AAA aaa..bbb.ccc.='111' />",
+
+       "<AAA><ccc><bbb><aaa>111</aaa></bbb></ccc></AAA>"
+}};
+
+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);
+}