--- /dev/null
+/* 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 */
'surface-helpers.c',
'spawn.c',
'string-helpers.c',
+ 'xml.c',
)
--- /dev/null
+// 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);
+ }
+}
#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"
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();
}
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
sources: '@0@.c'.format(t),
include_directories: [labwc_inc],
link_with: [test_lib],
+ dependencies: [xml2],
),
is_parallel: false,
)
--- /dev/null
+// 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);
+}