rcxml: convert dotted properties into nested nodes before processing

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 allow parsing nested 'If' actions (#2427) or when
we drastically refactor rcxml.c to use recursion instead of encoding.
This commit is contained in:
tokyo4j 2025-04-10 00:43:21 +09:00
parent 44146487b2
commit 5510d27904
6 changed files with 290 additions and 3 deletions

29
include/common/xml.h Normal file
View file

@ -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 properties 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_props(xmlNode *root);
#endif /* LABWC_XML_H */

View file

@ -23,4 +23,5 @@ labwc_sources += files(
'surface-helpers.c',
'spawn.c',
'string-helpers.c',
'xml.c',
)

127
src/common/xml.c Normal file
View file

@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-2.0-only
#include <glib.h>
#include <stdbool.h>
#include <strings.h>
#include "common/xml.h"
/*
* Converts a property A.B.C="X" into <C><B><A>X</A></B></C>
*/
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 <keybind name.action="ShowMenu" x.position.action="1" y.position="2" />.
* 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);
}
}

View 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"
@ -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();
}

View 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,
)

120
t/xml.c Normal file
View file

@ -0,0 +1,120 @@
// 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.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=\"222\">"
"<bbb><aaa>111</aaa></bbb>"
"<bbb><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_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);
}