Compose: add iterator API
authorRan Benita <ran@unusedvar.com>
Mon, 25 Sep 2023 09:41:48 +0000 (11:41 +0200)
committerWismill <dev@wismill.eu>
Tue, 26 Sep 2023 07:02:45 +0000 (09:02 +0200)
Allow users to iterate the entries in a compose table. This is useful
for other projects which want programmable access to the sequences,
without having to write their own parser.

- New API:
  - `xkb_compose_table_entry_sequence`;
  - `xkb_compose_table_entry_keysym`;
  - `xkb_compose_table_entry_utf8`;
  - `xkb_compose_table_iterator_new`;
  - `xkb_compose_table_iterator_free`;
  - `xkb_compose_table_iterator_next`.
- Add tests in `test/compose.c`.
- Add benchmark for compose traversal.
- `tools/compose.c`:
  - Print entries instead of just validating them.
  - Add `--file` option.
  - TODO: make this tool part of the xkbcli commands.

Co-authored-by: Pierre Le Marre <dev@wismill.eu>
Co-authored-by: Ran Benita <ran@unusedvar.com>
Signed-off-by: Ran Benita <ran@unusedvar.com>
bench/compose-traversal.c [new file with mode: 0644]
include/xkbcommon/xkbcommon-compose.h
meson.build
src/compose/parser.c
src/compose/parser.h
src/compose/table.c
src/compose/table.h
src/darray.h
test/compose.c
tools/compose.c
xkbcommon.map

diff --git a/bench/compose-traversal.c b/bench/compose-traversal.c
new file mode 100644 (file)
index 0000000..be24beb
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright © 2023 Pierre Le Marre
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <time.h>
+
+#include "xkbcommon/xkbcommon-compose.h"
+
+#include "../test/test.h"
+#include "bench.h"
+
+#define BENCHMARK_ITERATIONS 1000
+
+int
+main(void)
+{
+    struct xkb_context *ctx;
+    char *path;
+    FILE *file;
+    struct xkb_compose_table *table;
+    struct xkb_compose_table_iterator *iter;
+    struct xkb_compose_table_entry *entry;
+    struct bench bench;
+    char *elapsed;
+
+    ctx = test_get_context(CONTEXT_NO_FLAG);
+    assert(ctx);
+
+    path = test_get_path("locale/en_US.UTF-8/Compose");
+    file = fopen(path, "rb");
+    if (file == NULL) {
+        perror(path);
+        free(path);
+        xkb_context_unref(ctx);
+        return -1;
+    }
+    free(path);
+
+    xkb_context_set_log_level(ctx, XKB_LOG_LEVEL_CRITICAL);
+    xkb_context_set_log_verbosity(ctx, 0);
+
+    table = xkb_compose_table_new_from_file(ctx, file, "",
+                                            XKB_COMPOSE_FORMAT_TEXT_V1,
+                                            XKB_COMPOSE_COMPILE_NO_FLAGS);
+    fclose(file);
+    assert(table);
+
+    bench_start(&bench);
+    for (int i = 0; i < BENCHMARK_ITERATIONS; i++) {
+        iter = xkb_compose_table_iterator_new(table);
+        while ((entry = xkb_compose_table_iterator_next(iter))) {
+            assert (entry);
+        }
+        xkb_compose_table_iterator_free(iter);
+    }
+    bench_stop(&bench);
+
+    xkb_compose_table_unref(table);
+
+    elapsed = bench_elapsed_str(&bench);
+    fprintf(stderr, "traversed %d compose tables in %ss\n",
+            BENCHMARK_ITERATIONS, elapsed);
+    free(elapsed);
+
+    xkb_context_unref(ctx);
+    return 0;
+}
index 8b41b98..b28e4f8 100644 (file)
@@ -299,6 +299,143 @@ xkb_compose_table_ref(struct xkb_compose_table *table);
 void
 xkb_compose_table_unref(struct xkb_compose_table *table);
 
+/**
+ * @struct xkb_compose_table_entry
+ * Opaque Compose table entry object.
+ *
+ * Represents a single entry in a Compose file in the iteration API.
+ * It is immutable.
+ *
+ * @sa xkb_compose_table_iterator_new
+ * @since 1.6.0
+ */
+struct xkb_compose_table_entry;
+
+/**
+ * Get the left-hand keysym sequence of a Compose table entry.
+ *
+ * For example, given the following entry:
+ *
+ * ```
+ * <dead_tilde> <space> : "~" asciitilde # TILDE
+ * ```
+ *
+ * it will return `{XKB_KEY_dead_tilde, XKB_KEY_space}`.
+ *
+ * @param[in]  entry The compose table entry object to process.
+ *
+ * @param[out] sequence_length Number of keysyms in the sequence.
+ *
+ * @returns The array of left-hand side keysyms.  The number of keysyms
+ * is returned in the @p sequence_length out-parameter.
+ *
+ * @memberof xkb_compose_table_entry
+ * @since 1.6.0
+ */
+const xkb_keysym_t *
+xkb_compose_table_entry_sequence(struct xkb_compose_table_entry *entry,
+                                 size_t *sequence_length);
+
+/**
+ * Get the right-hand result keysym of a Compose table entry.
+ *
+ * For example, given the following entry:
+ *
+ * ```
+ * <dead_tilde> <space> : "~" asciitilde # TILDE
+ * ```
+ *
+ * it will return `XKB_KEY_asciitilde`.
+ *
+ * The keysym is optional; if the entry does not specify a keysym,
+ * returns `XKB_KEY_NoSymbol`.
+ *
+ * @memberof xkb_compose_table_entry
+ * @since 1.6.0
+ */
+xkb_keysym_t
+xkb_compose_table_entry_keysym(struct xkb_compose_table_entry *entry);
+
+/**
+ * Get the right-hand result string of a Compose table entry.
+ *
+ * The string is UTF-8 encoded and NULL-terminated.
+ *
+ * For example, given the following entry:
+ *
+ * ```
+ * <dead_tilde> <space> : "~" asciitilde # TILDE
+ * ```
+ *
+ * it will return `"~"`.
+ *
+ * The string is optional; if the entry does not specify a string,
+ * returns the empty string.
+ *
+ * @memberof xkb_compose_table_entry
+ * @since 1.6.0
+ */
+const char *
+xkb_compose_table_entry_utf8(struct xkb_compose_table_entry *entry);
+
+/**
+ * @struct xkb_compose_table_iterator
+ * Iterator over a compose table’s entries.
+ *
+ * @sa xkb_compose_table_iterator_new()
+ * @since 1.6.0
+ */
+struct xkb_compose_table_iterator;
+
+/**
+ * Create a new iterator for a compose table.
+ *
+ * Intended use:
+ *
+ * ```c
+ * struct xkb_compose_table_iterator *iter = xkb_compose_table_iterator_new(compose_table);
+ * struct xkb_compose_table_entry *entry;
+ * while ((entry = xkb_compose_table_iterator_next(iter))) {
+ *     // ...
+ * }
+ * xkb_compose_table_iterator_free(iter);
+ * ```
+ *
+ * @returns A new compose table iterator, or `NULL` on failure.
+ *
+ * @memberof xkb_compose_table_iterator
+ * @sa xkb_compose_table_iterator_free()
+ * @since 1.6.0
+ */
+struct xkb_compose_table_iterator *
+xkb_compose_table_iterator_new(struct xkb_compose_table *table);
+
+/**
+ * Free a compose iterator.
+ *
+ * @memberof xkb_compose_table_iterator
+ * @since 1.6.0
+ */
+void
+xkb_compose_table_iterator_free(struct xkb_compose_table_iterator *iter);
+
+/**
+ * Get the next compose entry from a compose table iterator.
+ *
+ * The entries are returned in lexicographic order of the left-hand
+ * side of entries. This does not correspond to the order in which
+ * the entries appear in the Compose file.
+ *
+ * @attention The return value is valid until the next call to this function.
+ *
+ * Returns `NULL` in case there is no more entries.
+ *
+ * @memberof xkb_compose_table_iterator
+ * @since 1.6.0
+ */
+struct xkb_compose_table_entry *
+xkb_compose_table_iterator_next(struct xkb_compose_table_iterator *iter);
+
 /** Flags for compose state creation. */
 enum xkb_compose_state_flags {
     /** Do not apply any flags. */
index ce17b1d..91d0b36 100644 (file)
@@ -805,6 +805,11 @@ benchmark(
     env: bench_env,
 )
 benchmark(
+    'compose-traversal',
+    executable('bench-compose-traversal', 'bench/compose-traversal.c', dependencies: test_dep),
+    env: bench_env,
+)
+benchmark(
     'atom',
     executable('bench-atom', 'bench/atom.c', dependencies: test_dep),
     env: bench_env,
index 88105fa..e1b81de 100644 (file)
@@ -63,9 +63,6 @@ OR PERFORMANCE OF THIS SOFTWARE.
 #include "utf8.h"
 #include "parser.h"
 
-#define MAX_LHS_LEN 10
-#define MAX_INCLUDE_DEPTH 5
-
 /*
  * Grammar adapted from libX11/modules/im/ximcp/imLcPrs.c.
  * See also the XCompose(5) manpage.
index 3f64a07..487f1a9 100644 (file)
@@ -24,6 +24,9 @@
 #ifndef COMPOSE_PARSER_H
 #define COMPOSE_PARSER_H
 
+#define MAX_LHS_LEN 10
+#define MAX_INCLUDE_DEPTH 5
+
 bool
 parse_string(struct xkb_compose_table *table,
              const char *string, size_t len,
index 3abc649..04fa8cb 100644 (file)
@@ -27,6 +27,7 @@
 #include "table.h"
 #include "parser.h"
 #include "paths.h"
+#include "xkbcommon/xkbcommon.h"
 
 static struct xkb_compose_table *
 xkb_compose_table_new(struct xkb_context *ctx,
@@ -228,3 +229,158 @@ found_path:
     free(path);
     return table;
 }
+
+XKB_EXPORT const xkb_keysym_t *
+xkb_compose_table_entry_sequence(struct xkb_compose_table_entry *entry,
+                                 size_t *sequence_length)
+{
+    *sequence_length = entry->sequence_length;
+    return entry->sequence;
+}
+
+XKB_EXPORT xkb_keysym_t
+xkb_compose_table_entry_keysym(struct xkb_compose_table_entry *entry)
+{
+    return entry->keysym;
+}
+
+XKB_EXPORT const char *
+xkb_compose_table_entry_utf8(struct xkb_compose_table_entry *entry)
+{
+    return entry->utf8;
+}
+
+enum node_direction {
+    NODE_LEFT = 0,
+    NODE_DOWN,
+    NODE_RIGHT,
+    NODE_UP
+};
+
+struct xkb_compose_table_iterator_cursor {
+    uint32_t node_offset:30; /* WARNING: ensure it fits MAX_COMPOSE_NODES */
+    uint8_t direction:2;     /* enum node_direction: current direction
+                              * traversing the tree */
+};
+
+struct xkb_compose_table_iterator {
+    struct xkb_compose_table *table;
+    /* Current entry */
+    struct xkb_compose_table_entry entry;
+    /* Stack of pending nodes to process */
+    darray(struct xkb_compose_table_iterator_cursor) cursors;
+};
+
+XKB_EXPORT struct xkb_compose_table_iterator *
+xkb_compose_table_iterator_new(struct xkb_compose_table *table)
+{
+    struct xkb_compose_table_iterator *iter;
+    struct xkb_compose_table_iterator_cursor cursor;
+    xkb_keysym_t *sequence;
+
+    iter = calloc(1, sizeof(*iter));
+    if (!iter) {
+        return NULL;
+    }
+    iter->table = xkb_compose_table_ref(table);
+    sequence = calloc(MAX_LHS_LEN, sizeof(xkb_keysym_t));
+    if (!sequence) {
+        free(iter);
+        return NULL;
+    }
+    iter->entry.sequence = sequence;
+    iter->entry.sequence_length = 0;
+
+    darray_init(iter->cursors);
+    cursor.direction = NODE_LEFT;
+    /* Offset 0 is a dummy null entry, skip it. */
+    cursor.node_offset = 1;
+    darray_append(iter->cursors, cursor);
+
+    return iter;
+}
+
+XKB_EXPORT void
+xkb_compose_table_iterator_free(struct xkb_compose_table_iterator *iter)
+{
+    xkb_compose_table_unref(iter->table);
+    darray_free(iter->cursors);
+    free(iter->entry.sequence);
+    free(iter);
+}
+
+XKB_EXPORT struct xkb_compose_table_entry *
+xkb_compose_table_iterator_next(struct xkb_compose_table_iterator *iter)
+{
+    /*
+     * This function takes the following recursive traversal function,
+     * and makes it non-recursive and resumable. The iter->cursors stack
+     * is analogous to the call stack, and cursor->direction to the
+     * instruction pointer of a stack frame.
+     *
+     *    traverse(xkb_keysym_t *sequence, size_t sequence_length, uint16_t p) {
+     *        if (!p) return
+     *        // cursor->direction == NODE_LEFT
+     *        node = &darray_item(table->nodes, p)
+     *        traverse(sequence, sequence_length, node->lokid)
+     *        // cursor->direction == NODE_DOWN
+     *        sequence[sequence_length++] = node->keysym
+     *        if (node->is_leaf)
+     *            emit(sequence, sequence_length, node->leaf.keysym, table->utf[node->leaf.utf8])
+     *        else
+     *            traverse(sequence, sequence_length, node->internal.eqkid)
+     *        sequence_length--
+     *        // cursor->direction == NODE_RIGHT
+     *        traverse(sequence, sequence_length, node->hikid)
+     *        // cursor->direction == NODE_UP
+     *    }
+     */
+
+    struct xkb_compose_table_iterator_cursor *cursor;
+    const struct compose_node *node;
+
+    while (!darray_empty(iter->cursors)) {
+        cursor = &darray_item(iter->cursors, darray_size(iter->cursors) - 1);
+        node = &darray_item(iter->table->nodes, cursor->node_offset);
+
+        switch (cursor->direction) {
+        case NODE_LEFT:
+            cursor->direction = NODE_DOWN;
+            if (node->lokid) {
+                struct xkb_compose_table_iterator_cursor new_cursor = {node->lokid, NODE_LEFT};
+                darray_append(iter->cursors, new_cursor);
+            }
+            break;
+
+        case NODE_DOWN:
+            cursor->direction = NODE_RIGHT;
+            assert (iter->entry.sequence_length <= MAX_LHS_LEN);
+            iter->entry.sequence[iter->entry.sequence_length] = node->keysym;
+            iter->entry.sequence_length++;
+            if (node->is_leaf) {
+                iter->entry.keysym = node->leaf.keysym;
+                iter->entry.utf8 = &darray_item(iter->table->utf8, node->leaf.utf8);
+                return &iter->entry;
+            } else {
+                struct xkb_compose_table_iterator_cursor new_cursor = {node->internal.eqkid, NODE_LEFT};
+                darray_append(iter->cursors, new_cursor);
+            }
+            break;
+
+        case NODE_RIGHT:
+            cursor->direction = NODE_UP;
+            iter->entry.sequence_length--;
+            if (node->hikid) {
+                struct xkb_compose_table_iterator_cursor new_cursor = {node->hikid, NODE_LEFT};
+                darray_append(iter->cursors, new_cursor);
+            }
+            break;
+
+        case NODE_UP:
+            darray_remove_last(iter->cursors);
+            break;
+        }
+    }
+
+    return NULL;
+}
index 825a8e2..f6904a1 100644 (file)
@@ -119,4 +119,11 @@ struct xkb_compose_table {
     darray(struct compose_node) nodes;
 };
 
+struct xkb_compose_table_entry {
+    xkb_keysym_t *sequence;
+    size_t sequence_length;
+    xkb_keysym_t keysym;
+    const char *utf8;
+};
+
 #endif
index de659cc..b75d85f 100644 (file)
@@ -114,6 +114,11 @@ typedef darray (unsigned long)  darray_ulong;
 #define darray_concat(arr_to, arr_from) \
     darray_append_items((arr_to), (arr_from).item, (arr_from).size)
 
+/*** Removal ***/
+
+/* Warning: Do not call darray_remove_last on an empty darray. */
+#define darray_remove_last(arr) (--(arr).size)
+
 /*** String buffer ***/
 
 #define darray_append_string(arr, str) do { \
index 1e85cbd..5e7cba0 100644 (file)
@@ -580,6 +580,110 @@ test_override(struct xkb_context *ctx)
         XKB_KEY_NoSymbol));
 }
 
+static bool
+test_eq_entry_va(struct xkb_compose_table_entry *entry, xkb_keysym_t keysym_ref, const char *utf8_ref, va_list ap)
+{
+    assert (entry != NULL);
+
+    assert (xkb_compose_table_entry_keysym(entry) == keysym_ref);
+
+    const char *utf8 = xkb_compose_table_entry_utf8(entry);
+    assert (utf8 && utf8_ref && strcmp(utf8, utf8_ref) == 0);
+
+    size_t nsyms;
+    const xkb_keysym_t *sequence = xkb_compose_table_entry_sequence(entry, &nsyms);
+
+    xkb_keysym_t keysym;
+    for (unsigned k = 0; ; k++) {
+        keysym = va_arg(ap, xkb_keysym_t);
+        if (keysym == XKB_KEY_NoSymbol) {
+            return (k == nsyms - 1);
+        }
+        assert (k < nsyms);
+        assert (keysym == sequence[k]);
+    }
+}
+
+static bool
+test_eq_entry(struct xkb_compose_table_entry *entry, xkb_keysym_t keysym, const char *utf8, ...)
+{
+    va_list ap;
+    bool ok;
+    va_start(ap, utf8);
+    ok = test_eq_entry_va(entry, keysym, utf8, ap);
+    va_end(ap);
+    return ok;
+}
+
+static void
+test_traverse(struct xkb_context *ctx)
+{
+    struct xkb_compose_table *table;
+
+    const char *buffer = "<dead_circumflex> <dead_circumflex> : \"foo\" X\n"
+                         "<Ahook> <x> : \"foobar\"\n"
+                         "<Multi_key> <o> <e> : oe\n"
+                         "<dead_circumflex> <e> : \"bar\" Y\n"
+                         "<Multi_key> <a> <e> : \"æ\" ae\n"
+                         "<dead_circumflex> <a> : \"baz\" Z\n"
+                         "<dead_acute> <e> : \"é\" eacute\n"
+                         "<Multi_key> <a> <a> <c>: \"aac\"\n"
+                         "<Multi_key> <a> <a> <b>: \"aab\"\n"
+                         "<Multi_key> <a> <a> <a>: \"aaa\"\n";
+
+    table = xkb_compose_table_new_from_buffer(ctx, buffer, strlen(buffer), "",
+                                              XKB_COMPOSE_FORMAT_TEXT_V1,
+                                              XKB_COMPOSE_COMPILE_NO_FLAGS);
+    assert(table);
+
+    struct xkb_compose_table_iterator *iter = xkb_compose_table_iterator_new(table);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_eacute, "é",
+                  XKB_KEY_dead_acute, XKB_KEY_e, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_Z, "baz",
+                  XKB_KEY_dead_circumflex, XKB_KEY_a, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_Y, "bar",
+                  XKB_KEY_dead_circumflex, XKB_KEY_e, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_X, "foo",
+                  XKB_KEY_dead_circumflex, XKB_KEY_dead_circumflex, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_NoSymbol, "aaa",
+                  XKB_KEY_Multi_key, XKB_KEY_a, XKB_KEY_a, XKB_KEY_a, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_NoSymbol, "aab",
+                  XKB_KEY_Multi_key, XKB_KEY_a, XKB_KEY_a, XKB_KEY_b, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_NoSymbol, "aac",
+                  XKB_KEY_Multi_key, XKB_KEY_a, XKB_KEY_a, XKB_KEY_c, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_ae, "æ",
+                  XKB_KEY_Multi_key, XKB_KEY_a, XKB_KEY_e, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_oe, "",
+                  XKB_KEY_Multi_key, XKB_KEY_o, XKB_KEY_e, XKB_KEY_NoSymbol);
+
+    test_eq_entry(xkb_compose_table_iterator_next(iter),
+                  XKB_KEY_NoSymbol, "foobar",
+                  XKB_KEY_Ahook, XKB_KEY_x, XKB_KEY_NoSymbol);
+
+    assert (xkb_compose_table_iterator_next(iter) == NULL);
+
+    xkb_compose_table_iterator_free(iter);
+    xkb_compose_table_unref(table);
+}
+
 int
 main(int argc, char *argv[])
 {
@@ -612,6 +716,7 @@ main(int argc, char *argv[])
     test_modifier_syntax(ctx);
     test_include(ctx);
     test_override(ctx);
+    test_traverse(ctx);
 
     xkb_context_unref(ctx);
     return 0;
index 2b3ba64..3a77347 100644 (file)
 #include <stdlib.h>
 
 #include "xkbcommon/xkbcommon.h"
+#include "xkbcommon/xkbcommon-keysyms.h"
 #include "xkbcommon/xkbcommon-compose.h"
 
 static void
 usage(FILE *fp, char *progname)
 {
     fprintf(fp,
-            "Usage: %s [--locale LOCALE | --locale-from-env | --locale-from-setlocale]\n",
+            "Usage: %s [--file FILE] [--locale LOCALE | --locale-from-env | --locale-from-setlocale]\n",
             progname);
     fprintf(fp,
+            "   --file - specify a file to load\n"
             "   --locale - specify the locale directly\n"
             "   --locale-from-env - get the locale from the LC_ALL/LC_CTYPE/LANG environment variables (falling back to C)\n"
             "   --locale-from-setlocale - get the locale using setlocale(3)\n"
     );
 }
 
+static void
+print_compose_table_entry(struct xkb_compose_table_entry *entry)
+{
+    size_t nsyms;
+    const xkb_keysym_t *syms = xkb_compose_table_entry_sequence(entry, &nsyms);
+    char buf[128];
+    for (size_t i = 0; i < nsyms; i++) {
+        xkb_keysym_get_name(syms[i], buf, sizeof(buf));
+        printf("<%s>", buf);
+        if (i + 1 < nsyms) {
+            printf(" ");
+        }
+    }
+    printf(":");
+    const char *utf8 = xkb_compose_table_entry_utf8(entry);
+    if (*utf8 != '\0') {
+        printf(" \"%s\"", utf8);
+    }
+    const xkb_keysym_t keysym = xkb_compose_table_entry_keysym(entry);
+    if (keysym != XKB_KEY_NoSymbol) {
+        xkb_keysym_get_name(keysym, buf, sizeof(buf));
+        printf(" %s", buf);
+    }
+    printf("\n");
+}
+
 int
 main(int argc, char *argv[])
 {
@@ -51,12 +79,16 @@ main(int argc, char *argv[])
     struct xkb_context *ctx = NULL;
     struct xkb_compose_table *compose_table = NULL;
     const char *locale = NULL;
+    const char *path = NULL;
+    enum xkb_compose_format format = XKB_COMPOSE_FORMAT_TEXT_V1;
     enum options {
+        OPT_FILE,
         OPT_LOCALE,
         OPT_LOCALE_FROM_ENV,
         OPT_LOCALE_FROM_SETLOCALE,
     };
     static struct option opts[] = {
+        {"file",                  required_argument,      0, OPT_FILE},
         {"locale",                required_argument,      0, OPT_LOCALE},
         {"locale-from-env",       no_argument,            0, OPT_LOCALE_FROM_ENV},
         {"locale-from-setlocale", no_argument,            0, OPT_LOCALE_FROM_SETLOCALE},
@@ -74,6 +106,9 @@ main(int argc, char *argv[])
             break;
 
         switch (opt) {
+        case OPT_FILE:
+            path = optarg;
+            break;
         case OPT_LOCALE:
             locale = optarg;
             break;
@@ -108,18 +143,40 @@ main(int argc, char *argv[])
         goto out;
     }
 
-    compose_table =
-        xkb_compose_table_new_from_locale(ctx, locale,
-                                          XKB_COMPOSE_COMPILE_NO_FLAGS);
-    if (!compose_table) {
-        fprintf(stderr, "Couldn't create compose from locale\n");
-        goto out;
+    if (path != NULL) {
+        FILE *file = fopen(path, "rb");
+        if (file == NULL) {
+            perror(path);
+            goto file_error;
+        }
+        compose_table =
+            xkb_compose_table_new_from_file(ctx, file, locale, format,
+                                            XKB_COMPOSE_COMPILE_NO_FLAGS);
+        fclose(file);
+        if (!compose_table) {
+            fprintf(stderr, "Couldn't create compose from file: %s\n", path);
+            goto out;
+        }
+    } else {
+        compose_table =
+            xkb_compose_table_new_from_locale(ctx, locale,
+                                              XKB_COMPOSE_COMPILE_NO_FLAGS);
+        if (!compose_table) {
+            fprintf(stderr, "Couldn't create compose from locale\n");
+            goto out;
+        }
     }
 
-    printf("Compiled successfully from locale %s\n", locale);
+    struct xkb_compose_table_iterator *iter = xkb_compose_table_iterator_new(compose_table);
+    struct xkb_compose_table_entry *entry;
+    while ((entry = xkb_compose_table_iterator_next(iter))) {
+        print_compose_table_entry(entry);
+    }
+    xkb_compose_table_iterator_free(iter);
 
 out:
     xkb_compose_table_unref(compose_table);
+file_error:
     xkb_context_unref(ctx);
 
     return ret;
index 59ef971..b250727 100644 (file)
@@ -109,3 +109,13 @@ global:
     xkb_utf32_to_keysym;
     xkb_keymap_key_get_mods_for_level;
 } V_0.8.0;
+
+V_1.6.0 {
+global:
+    xkb_compose_table_entry_sequence;
+    xkb_compose_table_entry_keysym;
+    xkb_compose_table_entry_utf8;
+    xkb_compose_table_iterator_new;
+    xkb_compose_table_iterator_free;
+    xkb_compose_table_iterator_next;
+} V_1.0.0;