Change UTF-8 combining to inspect the previous character at the cursor
authornicm <nicm@openbsd.org>
Fri, 15 Sep 2023 15:49:05 +0000 (15:49 +0000)
committernicm <nicm@openbsd.org>
Fri, 15 Sep 2023 15:49:05 +0000 (15:49 +0000)
position rather than keeping the last character from the input stream,
this is how most terminals work and fixes problems with displaying these
characters in vim. GitHub issue 3600.

usr.bin/tmux/screen-write.c
usr.bin/tmux/server.c
usr.bin/tmux/tmux.h
usr.bin/tmux/tty.c
usr.bin/tmux/utf8-combined.c
usr.bin/tmux/utf8.c

index 96a4916..08d6438 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: screen-write.c,v 1.221 2023/09/14 13:01:35 nicm Exp $ */
+/* $OpenBSD: screen-write.c,v 1.222 2023/09/15 15:49:05 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -32,8 +32,8 @@ static void   screen_write_collect_flush(struct screen_write_ctx *, int,
                    const char *);
 static int     screen_write_overwrite(struct screen_write_ctx *,
                    struct grid_cell *, u_int);
-static const struct grid_cell *screen_write_combine(struct screen_write_ctx *,
-                   const struct utf8_data *, u_int *, u_int *);
+static int     screen_write_combine(struct screen_write_ctx *,
+                   const struct grid_cell *);
 
 struct screen_write_citem {
        u_int                           x;
@@ -1742,7 +1742,6 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
 
        if (ci->used == 0)
                return;
-       ctx->flags &= ~SCREEN_WRITE_COMBINE;
 
        before = screen_write_collect_trim(ctx, s->cy, s->cx, ci->used,
            &wrapped);
@@ -1841,65 +1840,22 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
 {
        struct screen           *s = ctx->s;
        struct grid             *gd = s->grid;
-       struct grid_cell         copy;
-       const struct utf8_data  *ud = &gc->data, *previous = NULL, *combine;
+       const struct utf8_data  *ud = &gc->data;
        struct grid_line        *gl;
        struct grid_cell_entry  *gce;
        struct grid_cell         tmp_gc, now_gc;
        struct tty_ctx           ttyctx;
        u_int                    sx = screen_size_x(s), sy = screen_size_y(s);
-       u_int                    width = ud->width, xx, last, cx, cy;
+       u_int                    width = ud->width, xx, not_wrap;
        int                      selected, skip = 1;
 
        /* Ignore padding cells. */
        if (gc->flags & GRID_FLAG_PADDING)
                return;
 
-       /* Check if this cell needs to be combined with the previous cell. */
-       if (ctx->flags & SCREEN_WRITE_COMBINE)
-               previous = &ctx->previous;
-       switch (utf8_try_combined(ud, previous, &combine, &width)) {
-       case UTF8_DISCARD_NOW:
-               log_debug("%s: UTF8_DISCARD_NOW (width %u)", __func__, width);
-               ctx->flags &= ~SCREEN_WRITE_COMBINE;
+       /* Get the previous cell to check for combining. */
+       if (screen_write_combine(ctx, gc) != 0)
                return;
-       case UTF8_WRITE_NOW:
-               log_debug("%s: UTF8_WRITE_NOW (width %u)", __func__, width);
-               ctx->flags &= ~SCREEN_WRITE_COMBINE;
-               break;
-       case UTF8_COMBINE_NOW:
-               log_debug("%s: UTF8_COMBINE_NOW (width %u)", __func__, width);
-               screen_write_collect_flush(ctx, 0, __func__);
-               gc = screen_write_combine(ctx, combine, &xx, &cx);
-               if (gc != NULL) {
-                       cx = s->cx; cy = s->cy;
-                       screen_write_set_cursor(ctx, xx, s->cy);
-                       screen_write_initctx(ctx, &ttyctx, 0);
-                       ttyctx.cell = gc;
-                       tty_write(tty_cmd_cell, &ttyctx);
-                       s->cx = cx; s->cy = cy;
-               }
-               ctx->flags &= ~SCREEN_WRITE_COMBINE;
-               return;
-       case UTF8_WRITE_MAYBE_COMBINE:
-               log_debug("%s: UTF8_WRITE_MAYBE_COMBINE (width %u)", __func__,
-                   width);
-               utf8_copy(&ctx->previous, ud);
-               ctx->flags |= SCREEN_WRITE_COMBINE;
-               break;
-       case UTF8_DISCARD_MAYBE_COMBINE:
-               log_debug("%s: UTF8_DISCARD_MAYBE_COMBINE (width %u)", __func__,
-                   width);
-               utf8_copy(&ctx->previous, ud);
-               ctx->flags |= SCREEN_WRITE_COMBINE;
-               return;
-       }
-       if (width != ud->width) {
-               memcpy(&copy, gc, sizeof copy);
-               copy.data.width = width;
-               gc = &copy;
-       }
-       ud = NULL;
 
        /* Flush any existing scrolling. */
        screen_write_collect_flush(ctx, 1, __func__);
@@ -1991,11 +1947,11 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
         * Move the cursor. If not wrapping, stick at the last character and
         * replace it.
         */
-       last = !(s->mode & MODE_WRAP);
-       if (s->cx <= sx - last - width)
+       not_wrap = !(s->mode & MODE_WRAP);
+       if (s->cx <= sx - not_wrap - width)
                screen_write_set_cursor(ctx, s->cx + width, -1);
        else
-               screen_write_set_cursor(ctx,  sx - last, -1);
+               screen_write_set_cursor(ctx,  sx - not_wrap, -1);
 
        /* Create space for character in insert mode. */
        if (s->mode & MODE_INSERT) {
@@ -2015,65 +1971,98 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
        }
 }
 
-/* Combine a UTF-8 zero-width character onto the previous. */
-static const struct grid_cell *
-screen_write_combine(struct screen_write_ctx *ctx, const struct utf8_data *ud,
-    u_int *xx, u_int *cx)
+/* Combine a UTF-8 zero-width character onto the previous if necessary. */
+static int
+screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
 {
        struct screen           *s = ctx->s;
        struct grid             *gd = s->grid;
-       static struct grid_cell  gc;
-       u_int                    n, width;
+       const struct utf8_data  *ud = &gc->data;
+       u_int                    n, cx = s->cx, cy = s->cy;
+       struct grid_cell         last;
+       struct tty_ctx           ttyctx;
+       int                      force_wide = 0, zero_width = 0;
 
-       /* Can't combine if at 0. */
-       if (s->cx == 0) {
-               *xx = 0;
-               return (NULL);
+       /*
+        * Is this character which makes no sense without being combined? If
+        * this is true then flag it here and discard the character (return 1)
+        * if we cannot combine it.
+        */
+       if (utf8_is_zwj(ud))
+               zero_width = 1;
+       else if (utf8_is_vs(ud))
+               zero_width = force_wide = 1;
+       else if (ud->width == 0)
+               zero_width = 1;
+
+       /* Cannot combine empty character or at left. */
+       if (ud->size < 2 || cx == 0)
+               return (zero_width);
+       log_debug("%s: character %.*s at %u,%u (width %u)", __func__,
+           (int)ud->size, ud->data, cx, cy, ud->width);
+
+       /* Find the cell to combine with. */
+       n = 1;
+       grid_view_get_cell(gd, cx - n, cy, &last);
+       if (cx != 1 && (last.flags & GRID_FLAG_PADDING)) {
+               n = 2;
+               grid_view_get_cell(gd, cx - n, cy, &last);
        }
-       *xx = s->cx;
-
-       /* Empty data is out. */
-       if (ud->size == 0)
-               fatalx("UTF-8 data empty");
+       if (n != last.data.width || (last.flags & GRID_FLAG_PADDING))
+               return (zero_width);
 
-       /* Retrieve the previous cell. */
-       for (n = 1; n <= s->cx; n++) {
-               grid_view_get_cell(gd, s->cx - n, s->cy, &gc);
-               if (~gc.flags & GRID_FLAG_PADDING)
-                       break;
+       /*
+        * Check if we need to combine characters. This could be zero width
+        * (zet above), a modifier character (with an existing Unicode
+        * character) or a previous ZWJ.
+        */
+       if (!zero_width) {
+               if (utf8_is_modifier(ud)) {
+                       if (last.data.size < 2)
+                               return (0);
+                       force_wide = 1;
+               } else if (!utf8_has_zwj(&last.data))
+                       return (0);
        }
-       if (n > s->cx)
-               return (NULL);
 
-       /* Check there is enough space. */
-       if (gc.data.size + ud->size > sizeof gc.data.data)
-               return (NULL);
-       (*xx) -= n;
+       /* Combining; flush any pending output. */
+       screen_write_collect_flush(ctx, 0, __func__);
 
-       log_debug("%s: %.*s onto %.*s at %u,%u (width %u)", __func__,
-           (int)ud->size, ud->data, (int)gc.data.size, gc.data.data, *xx,
-           s->cy, gc.data.width);
+       log_debug("%s: %.*s -> %.*s at %u,%u (offset %u, width %u)", __func__,
+           (int)ud->size, ud->data, (int)last.data.size, last.data.data,
+           cx - n, cy, n, last.data.width);
 
        /* Append the data. */
-       memcpy(gc.data.data + gc.data.size, ud->data, ud->size);
-       gc.data.size += ud->size;
-       width = gc.data.width;
-
-       /* If this is U+FE0F VARIATION SELECTOR-16, force the width to 2. */
-       if (gc.data.width == 1 &&
-           ud->size == 3 &&
-           memcmp(ud->data, "\357\270\217", 3) == 0) {
-               grid_view_set_padding(gd, (*xx) + 1, s->cy);
-               gc.data.width = 2;
-               width += 2;
-       }
+       memcpy(last.data.data + last.data.size, ud->data, ud->size);
+       last.data.size += ud->size;
+
+       /* Force the width to 2 for modifiers and variation selector. */
+       if (last.data.width == 1 && force_wide) {
+               last.data.width = 2;
+               n = 2;
+               cx++;
+       } else
+               force_wide = 0;
 
        /* Set the new cell. */
-       grid_view_set_cell(gd, *xx, s->cy, &gc);
+       grid_view_set_cell(gd, cx - n, cy, &last);
+       if (force_wide)
+               grid_view_set_padding(gd, cx, cy);
 
-       *cx = (*xx) + width;
-       log_debug("%s: character at %u; cursor at %u", __func__, *xx, *cx);
-       return (&gc);
+       /*
+        * Redraw the combined cell. If forcing the cell to width 2, reset the
+        * cached cursor position in the tty, since we don't really know
+        * whether the terminal thought the character was width 1 or width 2
+        * and what it is going to do now.
+        */
+       screen_write_set_cursor(ctx, cx - n, cy);
+       screen_write_initctx(ctx, &ttyctx, 0);
+       ttyctx.cell = &last;
+       ttyctx.num = force_wide; /* reset cached cursor position */
+       tty_write(tty_cmd_cell, &ttyctx);
+       screen_write_set_cursor(ctx, cx, cy);
+
+       return (1);
 }
 
 /*
index c9d116f..494a623 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: server.c,v 1.204 2023/09/01 14:29:11 nicm Exp $ */
+/* $OpenBSD: server.c,v 1.205 2023/09/15 15:49:05 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -205,7 +205,6 @@ server_start(struct tmuxproc *client, int flags, struct event_base *base,
                fatal("pledge failed");
 
        input_key_build();
-       utf8_build_combined();
        RB_INIT(&windows);
        RB_INIT(&all_window_panes);
        TAILQ_INIT(&clients);
index 18d0da1..43bf574 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: tmux.h,v 1.1210 2023/09/15 06:31:49 nicm Exp $ */
+/* $OpenBSD: tmux.h,v 1.1211 2023/09/15 15:49:05 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -30,6 +30,7 @@
 #include <stdint.h>
 #include <stdio.h>
 #include <termios.h>
+#include <wchar.h>
 
 #include "tmux-protocol.h"
 #include "xmalloc.h"
@@ -619,15 +620,6 @@ enum utf8_state {
        UTF8_ERROR
 };
 
-/* UTF-8 combine state. */
-enum utf8_combine_state {
-       UTF8_DISCARD_NOW,          /* discard immediately */
-       UTF8_WRITE_NOW,            /* do not combine, write immediately */
-       UTF8_COMBINE_NOW,          /* combine immediately */
-       UTF8_WRITE_MAYBE_COMBINE,  /* write but try to combine the next */
-       UTF8_DISCARD_MAYBE_COMBINE /* discard but try to combine the next */
-};
-
 /* Colour flags. */
 #define COLOUR_FLAG_256 0x01000000
 #define COLOUR_FLAG_RGB 0x02000000
@@ -900,7 +892,6 @@ struct screen_write_ctx {
 
        int                              flags;
 #define SCREEN_WRITE_SYNC 0x1
-#define SCREEN_WRITE_COMBINE 0x2
 
        screen_write_init_ctx_cb         init_ctx_cb;
        void                            *arg;
@@ -908,7 +899,6 @@ struct screen_write_ctx {
        struct screen_write_citem       *item;
        u_int                            scrolled;
        u_int                            bg;
-       struct utf8_data                 previous;
 };
 
 /* Box border lines option. */
@@ -3277,6 +3267,8 @@ u_int              session_group_attached_count(struct session_group *);
 void            session_renumber_windows(struct session *);
 
 /* utf8.c */
+enum utf8_state         utf8_towc (const struct utf8_data *, wchar_t *);
+int             utf8_in_table(wchar_t, const wchar_t *, u_int);
 utf8_char       utf8_build_one(u_char);
 enum utf8_state         utf8_from_data(const struct utf8_data *, utf8_char *);
 void            utf8_to_data(utf8_char, struct utf8_data *);
@@ -3299,10 +3291,10 @@ char            *utf8_rpadcstr(const char *, u_int);
 int             utf8_cstrhas(const char *, const struct utf8_data *);
 
 /* utf8-combined.c */
-void            utf8_build_combined(void);
-int             utf8_try_combined(const struct utf8_data *,
-                    const struct utf8_data *, const struct utf8_data **,
-                    u_int *width);
+int             utf8_has_zwj(const struct utf8_data *);
+int             utf8_is_zwj(const struct utf8_data *);
+int             utf8_is_vs(const struct utf8_data *);
+int             utf8_is_modifier(const struct utf8_data *);
 
 /* procname.c */
 char   *get_proc_name(int, char *);
index efbc4a3..e6205c8 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: tty.c,v 1.434 2023/09/02 20:03:10 nicm Exp $ */
+/* $OpenBSD: tty.c,v 1.435 2023/09/15 15:49:05 nicm Exp $ */
 
 /*
  * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -2091,6 +2091,9 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx)
 
        tty_cell(tty, ctx->cell, &ctx->defaults, ctx->palette,
            ctx->s->hyperlinks);
+
+       if (ctx->num == 1)
+               tty_invalidate(tty);
 }
 
 void
index 18dffbf..1f0e244 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: utf8-combined.c,v 1.2 2023/09/01 18:43:54 nicm Exp $ */
+/* $OpenBSD: utf8-combined.c,v 1.3 2023/09/15 15:49:05 nicm Exp $ */
 
 /*
  * Copyright (c) 2023 Nicholas Marriott <nicholas.marriott@gmail.com>
 
 #include "tmux.h"
 
-static const struct {
-       wchar_t first;
-       wchar_t second;
-} utf8_combined_table[] = {
-       { 0x1F1E6, 0x1F1E8 }, /* flag: Ascension Island */
-       { 0x1F1E6, 0x1F1E9 }, /* flag: Andorra */
-       { 0x1F1E6, 0x1F1EA }, /* flag: United Arab Emirates */
-       { 0x1F1E6, 0x1F1EB }, /* flag: Afghanistan */
-       { 0x1F1E6, 0x1F1EC }, /* flag: Antigua & Barbuda */
-       { 0x1F1E6, 0x1F1EE }, /* flag: Anguilla */
-       { 0x1F1E6, 0x1F1F1 }, /* flag: Albania */
-       { 0x1F1E6, 0x1F1F2 }, /* flag: Armenia */
-       { 0x1F1E6, 0x1F1F4 }, /* flag: Angola */
-       { 0x1F1E6, 0x1F1F6 }, /* flag: Antarctica */
-       { 0x1F1E6, 0x1F1F7 }, /* flag: Argentina */
-       { 0x1F1E6, 0x1F1F8 }, /* flag: American Samoa */
-       { 0x1F1E6, 0x1F1F9 }, /* flag: Austria */
-       { 0x1F1E6, 0x1F1FA }, /* flag: Australia */
-       { 0x1F1E6, 0x1F1FC }, /* flag: Aruba */
-       { 0x1F1E6, 0x1F1FD }, /* flag: Aland Islands */
-       { 0x1F1E6, 0x1F1FF }, /* flag: Azerbaijan */
-       { 0x1F1E7, 0x1F1E6 }, /* flag: Bosnia & Herzegovina */
-       { 0x1F1E7, 0x1F1E7 }, /* flag: Barbados */
-       { 0x1F1E7, 0x1F1E9 }, /* flag: Bangladesh */
-       { 0x1F1E7, 0x1F1EA }, /* flag: Belgium */
-       { 0x1F1E7, 0x1F1EB }, /* flag: Burkina Faso */
-       { 0x1F1E7, 0x1F1EC }, /* flag: Bulgaria */
-       { 0x1F1E7, 0x1F1ED }, /* flag: Bahrain */
-       { 0x1F1E7, 0x1F1EE }, /* flag: Burundi */
-       { 0x1F1E7, 0x1F1EF }, /* flag: Benin */
-       { 0x1F1E7, 0x1F1F1 }, /* flag: St. Barthelemy */
-       { 0x1F1E7, 0x1F1F2 }, /* flag: Bermuda */
-       { 0x1F1E7, 0x1F1F3 }, /* flag: Brunei */
-       { 0x1F1E7, 0x1F1F4 }, /* flag: Bolivia */
-       { 0x1F1E7, 0x1F1F6 }, /* flag: Caribbean Netherlands */
-       { 0x1F1E7, 0x1F1F7 }, /* flag: Brazil */
-       { 0x1F1E7, 0x1F1F8 }, /* flag: Bahamas */
-       { 0x1F1E7, 0x1F1F9 }, /* flag: Bhutan */
-       { 0x1F1E7, 0x1F1FB }, /* flag: Bouvet Island */
-       { 0x1F1E7, 0x1F1FC }, /* flag: Botswana */
-       { 0x1F1E7, 0x1F1FE }, /* flag: Belarus */
-       { 0x1F1E7, 0x1F1FF }, /* flag: Belize */
-       { 0x1F1E8, 0x1F1E6 }, /* flag: Canada */
-       { 0x1F1E8, 0x1F1E8 }, /* flag: Cocos (Keeling) Islands */
-       { 0x1F1E8, 0x1F1E9 }, /* flag: Congo - Kinshasa */
-       { 0x1F1E8, 0x1F1EB }, /* flag: Central African Republic */
-       { 0x1F1E8, 0x1F1EC }, /* flag: Congo - Brazzaville */
-       { 0x1F1E8, 0x1F1ED }, /* flag: Switzerland */
-       { 0x1F1E8, 0x1F1EE }, /* flag: Cote d'Ivoire */
-       { 0x1F1E8, 0x1F1F0 }, /* flag: Cook Islands */
-       { 0x1F1E8, 0x1F1F1 }, /* flag: Chile */
-       { 0x1F1E8, 0x1F1F2 }, /* flag: Cameroon */
-       { 0x1F1E8, 0x1F1F3 }, /* flag: China */
-       { 0x1F1E8, 0x1F1F4 }, /* flag: Colombia */
-       { 0x1F1E8, 0x1F1F5 }, /* flag: Clipperton Island */
-       { 0x1F1E8, 0x1F1F7 }, /* flag: Costa Rica */
-       { 0x1F1E8, 0x1F1FA }, /* flag: Cuba */
-       { 0x1F1E8, 0x1F1FB }, /* flag: Cape Verde */
-       { 0x1F1E8, 0x1F1FC }, /* flag: Curacao */
-       { 0x1F1E8, 0x1F1FD }, /* flag: Christmas Island */
-       { 0x1F1E8, 0x1F1FE }, /* flag: Cyprus */
-       { 0x1F1E8, 0x1F1FF }, /* flag: Czechia */
-       { 0x1F1E9, 0x1F1EA }, /* flag: Germany */
-       { 0x1F1E9, 0x1F1EC }, /* flag: Diego Garcia */
-       { 0x1F1E9, 0x1F1EF }, /* flag: Djibouti */
-       { 0x1F1E9, 0x1F1F0 }, /* flag: Denmark */
-       { 0x1F1E9, 0x1F1F2 }, /* flag: Dominica */
-       { 0x1F1E9, 0x1F1F4 }, /* flag: Dominican Republic */
-       { 0x1F1E9, 0x1F1FF }, /* flag: Algeria */
-       { 0x1F1EA, 0x1F1E6 }, /* flag: Ceuta & Melilla */
-       { 0x1F1EA, 0x1F1E8 }, /* flag: Ecuador */
-       { 0x1F1EA, 0x1F1EA }, /* flag: Estonia */
-       { 0x1F1EA, 0x1F1EC }, /* flag: Egypt */
-       { 0x1F1EA, 0x1F1ED }, /* flag: Western Sahara */
-       { 0x1F1EA, 0x1F1F7 }, /* flag: Eritrea */
-       { 0x1F1EA, 0x1F1F8 }, /* flag: Spain */
-       { 0x1F1EA, 0x1F1F9 }, /* flag: Ethiopia */
-       { 0x1F1EA, 0x1F1FA }, /* flag: European Union */
-       { 0x1F1EB, 0x1F1EE }, /* flag: Finland */
-       { 0x1F1EB, 0x1F1EF }, /* flag: Fiji */
-       { 0x1F1EB, 0x1F1F0 }, /* flag: Falkland Islands */
-       { 0x1F1EB, 0x1F1F2 }, /* flag: Micronesia */
-       { 0x1F1EB, 0x1F1F4 }, /* flag: Faroe Islands */
-       { 0x1F1EB, 0x1F1F7 }, /* flag: France */
-       { 0x1F1EC, 0x1F1E6 }, /* flag: Gabon */
-       { 0x1F1EC, 0x1F1E7 }, /* flag: United Kingdom */
-       { 0x1F1EC, 0x1F1E9 }, /* flag: Grenada */
-       { 0x1F1EC, 0x1F1EA }, /* flag: Georgia */
-       { 0x1F1EC, 0x1F1EB }, /* flag: French Guiana */
-       { 0x1F1EC, 0x1F1EC }, /* flag: Guernsey */
-       { 0x1F1EC, 0x1F1ED }, /* flag: Ghana */
-       { 0x1F1EC, 0x1F1EE }, /* flag: Gibraltar */
-       { 0x1F1EC, 0x1F1F1 }, /* flag: Greenland */
-       { 0x1F1EC, 0x1F1F2 }, /* flag: Gambia */
-       { 0x1F1EC, 0x1F1F3 }, /* flag: Guinea */
-       { 0x1F1EC, 0x1F1F5 }, /* flag: Guadeloupe */
-       { 0x1F1EC, 0x1F1F6 }, /* flag: Equatorial Guinea */
-       { 0x1F1EC, 0x1F1F7 }, /* flag: Greece */
-       { 0x1F1EC, 0x1F1F8 }, /* flag: South Georgia & South Sandwich Islands */
-       { 0x1F1EC, 0x1F1F9 }, /* flag: Guatemala */
-       { 0x1F1EC, 0x1F1FA }, /* flag: Guam */
-       { 0x1F1EC, 0x1F1FC }, /* flag: Guinea-Bissau */
-       { 0x1F1EC, 0x1F1FE }, /* flag: Guyana */
-       { 0x1F1ED, 0x1F1F0 }, /* flag: Hong Kong SAR China */
-       { 0x1F1ED, 0x1F1F2 }, /* flag: Heard & McDonald Islands */
-       { 0x1F1ED, 0x1F1F3 }, /* flag: Honduras */
-       { 0x1F1ED, 0x1F1F7 }, /* flag: Croatia */
-       { 0x1F1ED, 0x1F1F9 }, /* flag: Haiti */
-       { 0x1F1ED, 0x1F1FA }, /* flag: Hungary */
-       { 0x1F1EE, 0x1F1E8 }, /* flag: Canary Islands */
-       { 0x1F1EE, 0x1F1E9 }, /* flag: Indonesia */
-       { 0x1F1EE, 0x1F1EA }, /* flag: Ireland */
-       { 0x1F1EE, 0x1F1F1 }, /* flag: Israel */
-       { 0x1F1EE, 0x1F1F2 }, /* flag: Isle of Man */
-       { 0x1F1EE, 0x1F1F3 }, /* flag: India */
-       { 0x1F1EE, 0x1F1F4 }, /* flag: British Indian Ocean Territory */
-       { 0x1F1EE, 0x1F1F6 }, /* flag: Iraq */
-       { 0x1F1EE, 0x1F1F7 }, /* flag: Iran */
-       { 0x1F1EE, 0x1F1F8 }, /* flag: Iceland */
-       { 0x1F1EE, 0x1F1F9 }, /* flag: Italy */
-       { 0x1F1EF, 0x1F1EA }, /* flag: Jersey */
-       { 0x1F1EF, 0x1F1F2 }, /* flag: Jamaica */
-       { 0x1F1EF, 0x1F1F4 }, /* flag: Jordan */
-       { 0x1F1EF, 0x1F1F5 }, /* flag: Japan */
-       { 0x1F1F0, 0x1F1EA }, /* flag: Kenya */
-       { 0x1F1F0, 0x1F1EC }, /* flag: Kyrgyzstan */
-       { 0x1F1F0, 0x1F1ED }, /* flag: Cambodia */
-       { 0x1F1F0, 0x1F1EE }, /* flag: Kiribati */
-       { 0x1F1F0, 0x1F1F2 }, /* flag: Comoros */
-       { 0x1F1F0, 0x1F1F3 }, /* flag: St. Kitts & Nevis */
-       { 0x1F1F0, 0x1F1F5 }, /* flag: North Korea */
-       { 0x1F1F0, 0x1F1F7 }, /* flag: South Korea */
-       { 0x1F1F0, 0x1F1FC }, /* flag: Kuwait */
-       { 0x1F1F0, 0x1F1FE }, /* flag: Cayman Islands */
-       { 0x1F1F0, 0x1F1FF }, /* flag: Kazakhstan */
-       { 0x1F1F1, 0x1F1E6 }, /* flag: Laos */
-       { 0x1F1F1, 0x1F1E7 }, /* flag: Lebanon */
-       { 0x1F1F1, 0x1F1E8 }, /* flag: St. Lucia */
-       { 0x1F1F1, 0x1F1EE }, /* flag: Liechtenstein */
-       { 0x1F1F1, 0x1F1F0 }, /* flag: Sri Lanka */
-       { 0x1F1F1, 0x1F1F7 }, /* flag: Liberia */
-       { 0x1F1F1, 0x1F1F8 }, /* flag: Lesotho */
-       { 0x1F1F1, 0x1F1F9 }, /* flag: Lithuania */
-       { 0x1F1F1, 0x1F1FA }, /* flag: Luxembourg */
-       { 0x1F1F1, 0x1F1FB }, /* flag: Latvia */
-       { 0x1F1F1, 0x1F1FE }, /* flag: Libya */
-       { 0x1F1F2, 0x1F1E6 }, /* flag: Morocco */
-       { 0x1F1F2, 0x1F1E8 }, /* flag: Monaco */
-       { 0x1F1F2, 0x1F1E9 }, /* flag: Moldova */
-       { 0x1F1F2, 0x1F1EA }, /* flag: Montenegro */
-       { 0x1F1F2, 0x1F1EB }, /* flag: St. Martin */
-       { 0x1F1F2, 0x1F1EC }, /* flag: Madagascar */
-       { 0x1F1F2, 0x1F1ED }, /* flag: Marshall Islands */
-       { 0x1F1F2, 0x1F1F0 }, /* flag: North Macedonia */
-       { 0x1F1F2, 0x1F1F1 }, /* flag: Mali */
-       { 0x1F1F2, 0x1F1F2 }, /* flag: Myanmar (Burma */
-       { 0x1F1F2, 0x1F1F3 }, /* flag: Mongolia */
-       { 0x1F1F2, 0x1F1F4 }, /* flag: Macao SAR China */
-       { 0x1F1F2, 0x1F1F5 }, /* flag: Northern Mariana Islands */
-       { 0x1F1F2, 0x1F1F6 }, /* flag: Martinique */
-       { 0x1F1F2, 0x1F1F7 }, /* flag: Mauritania */
-       { 0x1F1F2, 0x1F1F8 }, /* flag: Montserrat */
-       { 0x1F1F2, 0x1F1F9 }, /* flag: Malta */
-       { 0x1F1F2, 0x1F1FA }, /* flag: Mauritius */
-       { 0x1F1F2, 0x1F1FB }, /* flag: Maldives */
-       { 0x1F1F2, 0x1F1FC }, /* flag: Malawi */
-       { 0x1F1F2, 0x1F1FD }, /* flag: Mexico */
-       { 0x1F1F2, 0x1F1FE }, /* flag: Malaysia */
-       { 0x1F1F2, 0x1F1FF }, /* flag: Mozambique */
-       { 0x1F1F3, 0x1F1E6 }, /* flag: Namibia */
-       { 0x1F1F3, 0x1F1E8 }, /* flag: New Caledonia */
-       { 0x1F1F3, 0x1F1EA }, /* flag: Niger */
-       { 0x1F1F3, 0x1F1EB }, /* flag: Norfolk Island */
-       { 0x1F1F3, 0x1F1EC }, /* flag: Nigeria */
-       { 0x1F1F3, 0x1F1EE }, /* flag: Nicaragua */
-       { 0x1F1F3, 0x1F1F1 }, /* flag: Netherlands */
-       { 0x1F1F3, 0x1F1F4 }, /* flag: Norway */
-       { 0x1F1F3, 0x1F1F5 }, /* flag: Nepal */
-       { 0x1F1F3, 0x1F1F7 }, /* flag: Nauru */
-       { 0x1F1F3, 0x1F1FA }, /* flag: Niue */
-       { 0x1F1F3, 0x1F1FF }, /* flag: New Zealand */
-       { 0x1F1F4, 0x1F1F2 }, /* flag: Oman */
-       { 0x1F1F5, 0x1F1E6 }, /* flag: Panama */
-       { 0x1F1F5, 0x1F1EA }, /* flag: Peru */
-       { 0x1F1F5, 0x1F1EB }, /* flag: French Polynesia */
-       { 0x1F1F5, 0x1F1EC }, /* flag: Papua New Guinea */
-       { 0x1F1F5, 0x1F1ED }, /* flag: Philippines */
-       { 0x1F1F5, 0x1F1F0 }, /* flag: Pakistan */
-       { 0x1F1F5, 0x1F1F1 }, /* flag: Poland */
-       { 0x1F1F5, 0x1F1F2 }, /* flag: St. Pierre & Miquelon */
-       { 0x1F1F5, 0x1F1F3 }, /* flag: Pitcairn Islands */
-       { 0x1F1F5, 0x1F1F7 }, /* flag: Puerto Rico */
-       { 0x1F1F5, 0x1F1F8 }, /* flag: Palestinian Territories */
-       { 0x1F1F5, 0x1F1F9 }, /* flag: Portugal */
-       { 0x1F1F5, 0x1F1FC }, /* flag: Palau */
-       { 0x1F1F5, 0x1F1FE }, /* flag: Paraguay */
-       { 0x1F1F6, 0x1F1E6 }, /* flag: Qatar */
-       { 0x1F1F7, 0x1F1EA }, /* flag: Reunion */
-       { 0x1F1F7, 0x1F1F4 }, /* flag: Romania */
-       { 0x1F1F7, 0x1F1F8 }, /* flag: Serbia */
-       { 0x1F1F7, 0x1F1FA }, /* flag: Russia */
-       { 0x1F1F7, 0x1F1FC }, /* flag: Rwanda */
-       { 0x1F1F8, 0x1F1E6 }, /* flag: Saudi Arabia */
-       { 0x1F1F8, 0x1F1E7 }, /* flag: Solomon Islands */
-       { 0x1F1F8, 0x1F1E8 }, /* flag: Seychelles */
-       { 0x1F1F8, 0x1F1E9 }, /* flag: Sudan */
-       { 0x1F1F8, 0x1F1EA }, /* flag: Sweden */
-       { 0x1F1F8, 0x1F1EC }, /* flag: Singapore */
-       { 0x1F1F8, 0x1F1ED }, /* flag: St. Helena */
-       { 0x1F1F8, 0x1F1EE }, /* flag: Slovenia */
-       { 0x1F1F8, 0x1F1EF }, /* flag: Svalbard & Jan Mayen */
-       { 0x1F1F8, 0x1F1F0 }, /* flag: Slovakia */
-       { 0x1F1F8, 0x1F1F1 }, /* flag: Sierra Leone */
-       { 0x1F1F8, 0x1F1F2 }, /* flag: San Marino */
-       { 0x1F1F8, 0x1F1F3 }, /* flag: Senegal */
-       { 0x1F1F8, 0x1F1F4 }, /* flag: Somalia */
-       { 0x1F1F8, 0x1F1F7 }, /* flag: Suriname */
-       { 0x1F1F8, 0x1F1F8 }, /* flag: South Sudan */
-       { 0x1F1F8, 0x1F1F9 }, /* flag: Sao Tome & Principe */
-       { 0x1F1F8, 0x1F1FB }, /* flag: El Salvador */
-       { 0x1F1F8, 0x1F1FD }, /* flag: Sint Maarten */
-       { 0x1F1F8, 0x1F1FE }, /* flag: Syria */
-       { 0x1F1F8, 0x1F1FF }, /* flag: Eswatini */
-       { 0x1F1F9, 0x1F1E6 }, /* flag: Tristan da Cunha */
-       { 0x1F1F9, 0x1F1E8 }, /* flag: Turks & Caicos Islands */
-       { 0x1F1F9, 0x1F1E9 }, /* flag: Chad */
-       { 0x1F1F9, 0x1F1EB }, /* flag: French Southern Territories */
-       { 0x1F1F9, 0x1F1EC }, /* flag: Togo */
-       { 0x1F1F9, 0x1F1ED }, /* flag: Thailand */
-       { 0x1F1F9, 0x1F1EF }, /* flag: Tajikistan */
-       { 0x1F1F9, 0x1F1F0 }, /* flag: Tokelau */
-       { 0x1F1F9, 0x1F1F1 }, /* flag: Timor-Leste */
-       { 0x1F1F9, 0x1F1F2 }, /* flag: Turkmenistan */
-       { 0x1F1F9, 0x1F1F3 }, /* flag: Tunisia */
-       { 0x1F1F9, 0x1F1F4 }, /* flag: Tonga */
-       { 0x1F1F9, 0x1F1F7 }, /* flag: Turkey */
-       { 0x1F1F9, 0x1F1F9 }, /* flag: Trinidad & Tobago */
-       { 0x1F1F9, 0x1F1FB }, /* flag: Tuvalu */
-       { 0x1F1F9, 0x1F1FC }, /* flag: Taiwan */
-       { 0x1F1F9, 0x1F1FF }, /* flag: Tanzania */
-       { 0x1F1FA, 0x1F1E6 }, /* flag: Ukraine */
-       { 0x1F1FA, 0x1F1EC }, /* flag: Uganda */
-       { 0x1F1FA, 0x1F1F2 }, /* flag: U.S. Outlying Islands */
-       { 0x1F1FA, 0x1F1F3 }, /* flag: United Nations */
-       { 0x1F1FA, 0x1F1F8 }, /* flag: United States */
-       { 0x1F1FA, 0x1F1FE }, /* flag: Uruguay */
-       { 0x1F1FA, 0x1F1FF }, /* flag: Uzbekistan */
-       { 0x1F1FB, 0x1F1E6 }, /* flag: Vatican City */
-       { 0x1F1FB, 0x1F1E8 }, /* flag: St. Vincent & Grenadines */
-       { 0x1F1FB, 0x1F1EA }, /* flag: Venezuela */
-       { 0x1F1FB, 0x1F1EC }, /* flag: British Virgin Islands */
-       { 0x1F1FB, 0x1F1EE }, /* flag: U.S. Virgin Islands */
-       { 0x1F1FB, 0x1F1F3 }, /* flag: Vietnam */
-       { 0x1F1FB, 0x1F1FA }, /* flag: Vanuatu */
-       { 0x1F1FC, 0x1F1EB }, /* flag: Wallis & Futuna */
-       { 0x1F1FC, 0x1F1F8 }, /* flag: Samoa */
-       { 0x1F1FD, 0x1F1F0 }, /* flag: Kosovo */
-       { 0x1F1FE, 0x1F1EA }, /* flag: Yemen */
-       { 0x1F1FE, 0x1F1F9 }, /* flag: Mayotte */
-       { 0x1F1FF, 0x1F1E6 }, /* flag: South Africa */
-       { 0x1F1FF, 0x1F1F2 }, /* flag: Zambia */
-       { 0x1F1FF, 0x1F1FC }, /* flag: Zimbabwe */
-       { 0x0261D, 0x1F3FB }, /* index pointing up: light skin tone */
-       { 0x0261D, 0x1F3FC }, /* index pointing up: medium-light skin tone */
-       { 0x0261D, 0x1F3FD }, /* index pointing up: medium skin tone */
-       { 0x0261D, 0x1F3FE }, /* index pointing up: medium-dark skin tone */
-       { 0x0261D, 0x1F3FF }, /* index pointing up: dark skin tone */
-       { 0x026F9, 0x1F3FB }, /* person bouncing ball: light skin tone */
-       { 0x026F9, 0x1F3FC }, /* person bouncing ball: medium-light skin tone */
-       { 0x026F9, 0x1F3FD }, /* person bouncing ball: medium skin tone */
-       { 0x026F9, 0x1F3FE }, /* person bouncing ball: medium-dark skin tone */
-       { 0x026F9, 0x1F3FF }, /* person bouncing ball: dark skin tone */
-       { 0x0270A, 0x1F3FB }, /* raised fist: light skin tone */
-       { 0x0270A, 0x1F3FC }, /* raised fist: medium-light skin tone */
-       { 0x0270A, 0x1F3FD }, /* raised fist: medium skin tone */
-       { 0x0270A, 0x1F3FE }, /* raised fist: medium-dark skin tone */
-       { 0x0270A, 0x1F3FF }, /* raised fist: dark skin tone */
-       { 0x0270B, 0x1F3FB }, /* raised hand: light skin tone */
-       { 0x0270B, 0x1F3FC }, /* raised hand: medium-light skin tone */
-       { 0x0270B, 0x1F3FD }, /* raised hand: medium skin tone */
-       { 0x0270B, 0x1F3FE }, /* raised hand: medium-dark skin tone */
-       { 0x0270B, 0x1F3FF }, /* raised hand: dark skin tone */
-       { 0x0270C, 0x1F3FB }, /* victory hand: light skin tone */
-       { 0x0270C, 0x1F3FC }, /* victory hand: medium-light skin tone */
-       { 0x0270C, 0x1F3FD }, /* victory hand: medium skin tone */
-       { 0x0270C, 0x1F3FE }, /* victory hand: medium-dark skin tone */
-       { 0x0270C, 0x1F3FF }, /* victory hand: dark skin tone */
-       { 0x0270D, 0x1F3FB }, /* writing hand: light skin tone */
-       { 0x0270D, 0x1F3FC }, /* writing hand: medium-light skin tone */
-       { 0x0270D, 0x1F3FD }, /* writing hand: medium skin tone */
-       { 0x0270D, 0x1F3FE }, /* writing hand: medium-dark skin tone */
-       { 0x0270D, 0x1F3FF }, /* writing hand: dark skin tone */
-       { 0x1F385, 0x1F3FB }, /* Santa Claus: light skin tone */
-       { 0x1F385, 0x1F3FC }, /* Santa Claus: medium-light skin tone */
-       { 0x1F385, 0x1F3FD }, /* Santa Claus: medium skin tone */
-       { 0x1F385, 0x1F3FE }, /* Santa Claus: medium-dark skin tone */
-       { 0x1F385, 0x1F3FF }, /* Santa Claus: dark skin tone */
-       { 0x1F3C2, 0x1F3FB }, /* snowboarder: light skin tone */
-       { 0x1F3C2, 0x1F3FC }, /* snowboarder: medium-light skin tone */
-       { 0x1F3C2, 0x1F3FD }, /* snowboarder: medium skin tone */
-       { 0x1F3C2, 0x1F3FE }, /* snowboarder: medium-dark skin tone */
-       { 0x1F3C2, 0x1F3FF }, /* snowboarder: dark skin tone */
-       { 0x1F3C3, 0x1F3FB }, /* person running: light skin tone */
-       { 0x1F3C3, 0x1F3FC }, /* person running: medium-light skin tone */
-       { 0x1F3C3, 0x1F3FD }, /* person running: medium skin tone */
-       { 0x1F3C3, 0x1F3FE }, /* person running: medium-dark skin tone */
-       { 0x1F3C3, 0x1F3FF }, /* person running: dark skin tone */
-       { 0x1F3C4, 0x1F3FB }, /* person surfing: light skin tone */
-       { 0x1F3C4, 0x1F3FC }, /* person surfing: medium-light skin tone */
-       { 0x1F3C4, 0x1F3FD }, /* person surfing: medium skin tone */
-       { 0x1F3C4, 0x1F3FE }, /* person surfing: medium-dark skin tone */
-       { 0x1F3C4, 0x1F3FF }, /* person surfing: dark skin tone */
-       { 0x1F3C7, 0x1F3FB }, /* horse racing: light skin tone */
-       { 0x1F3C7, 0x1F3FC }, /* horse racing: medium-light skin tone */
-       { 0x1F3C7, 0x1F3FD }, /* horse racing: medium skin tone */
-       { 0x1F3C7, 0x1F3FE }, /* horse racing: medium-dark skin tone */
-       { 0x1F3C7, 0x1F3FF }, /* horse racing: dark skin tone */
-       { 0x1F3CA, 0x1F3FB }, /* person swimming: light skin tone */
-       { 0x1F3CA, 0x1F3FC }, /* person swimming: medium-light skin tone */
-       { 0x1F3CA, 0x1F3FD }, /* person swimming: medium skin tone */
-       { 0x1F3CA, 0x1F3FE }, /* person swimming: medium-dark skin tone */
-       { 0x1F3CA, 0x1F3FF }, /* person swimming: dark skin tone */
-       { 0x1F3CB, 0x1F3FB }, /* person lifting weights: light skin tone */
-       { 0x1F3CB, 0x1F3FC }, /* person lifting weights: medium-light skin tone */
-       { 0x1F3CB, 0x1F3FD }, /* person lifting weights: medium skin tone */
-       { 0x1F3CB, 0x1F3FE }, /* person lifting weights: medium-dark skin tone */
-       { 0x1F3CB, 0x1F3FF }, /* person lifting weights: dark skin tone */
-       { 0x1F3CC, 0x1F3FB }, /* person golfing: light skin tone */
-       { 0x1F3CC, 0x1F3FC }, /* person golfing: medium-light skin tone */
-       { 0x1F3CC, 0x1F3FD }, /* person golfing: medium skin tone */
-       { 0x1F3CC, 0x1F3FE }, /* person golfing: medium-dark skin tone */
-       { 0x1F3CC, 0x1F3FF }, /* person golfing: dark skin tone */
-       { 0x1F442, 0x1F3FB }, /* ear: light skin tone */
-       { 0x1F442, 0x1F3FC }, /* ear: medium-light skin tone */
-       { 0x1F442, 0x1F3FD }, /* ear: medium skin tone */
-       { 0x1F442, 0x1F3FE }, /* ear: medium-dark skin tone */
-       { 0x1F442, 0x1F3FF }, /* ear: dark skin tone */
-       { 0x1F443, 0x1F3FB }, /* nose: light skin tone */
-       { 0x1F443, 0x1F3FC }, /* nose: medium-light skin tone */
-       { 0x1F443, 0x1F3FD }, /* nose: medium skin tone */
-       { 0x1F443, 0x1F3FE }, /* nose: medium-dark skin tone */
-       { 0x1F443, 0x1F3FF }, /* nose: dark skin tone */
-       { 0x1F446, 0x1F3FB }, /* backhand index pointing up: light skin tone */
-       { 0x1F446, 0x1F3FC }, /* backhand index pointing up: medium-light skin tone */
-       { 0x1F446, 0x1F3FD }, /* backhand index pointing up: medium skin tone */
-       { 0x1F446, 0x1F3FE }, /* backhand index pointing up: medium-dark skin tone */
-       { 0x1F446, 0x1F3FF }, /* backhand index pointing up: dark skin tone */
-       { 0x1F447, 0x1F3FB }, /* backhand index pointing down: light skin tone */
-       { 0x1F447, 0x1F3FC }, /* backhand index pointing down: medium-light skin tone */
-       { 0x1F447, 0x1F3FD }, /* backhand index pointing down: medium skin tone */
-       { 0x1F447, 0x1F3FE }, /* backhand index pointing down: medium-dark skin tone */
-       { 0x1F447, 0x1F3FF }, /* backhand index pointing down: dark skin tone */
-       { 0x1F448, 0x1F3FB }, /* backhand index pointing left: light skin tone */
-       { 0x1F448, 0x1F3FC }, /* backhand index pointing left: medium-light skin tone */
-       { 0x1F448, 0x1F3FD }, /* backhand index pointing left: medium skin tone */
-       { 0x1F448, 0x1F3FE }, /* backhand index pointing left: medium-dark skin tone */
-       { 0x1F448, 0x1F3FF }, /* backhand index pointing left: dark skin tone */
-       { 0x1F449, 0x1F3FB }, /* backhand index pointing right: light skin tone */
-       { 0x1F449, 0x1F3FC }, /* backhand index pointing right: medium-light skin tone */
-       { 0x1F449, 0x1F3FD }, /* backhand index pointing right: medium skin tone */
-       { 0x1F449, 0x1F3FE }, /* backhand index pointing right: medium-dark skin tone */
-       { 0x1F449, 0x1F3FF }, /* backhand index pointing right: dark skin tone */
-       { 0x1F44A, 0x1F3FB }, /* oncoming fist: light skin tone */
-       { 0x1F44A, 0x1F3FC }, /* oncoming fist: medium-light skin tone */
-       { 0x1F44A, 0x1F3FD }, /* oncoming fist: medium skin tone */
-       { 0x1F44A, 0x1F3FE }, /* oncoming fist: medium-dark skin tone */
-       { 0x1F44A, 0x1F3FF }, /* oncoming fist: dark skin tone */
-       { 0x1F44B, 0x1F3FB }, /* waving hand: light skin tone */
-       { 0x1F44B, 0x1F3FC }, /* waving hand: medium-light skin tone */
-       { 0x1F44B, 0x1F3FD }, /* waving hand: medium skin tone */
-       { 0x1F44B, 0x1F3FE }, /* waving hand: medium-dark skin tone */
-       { 0x1F44B, 0x1F3FF }, /* waving hand: dark skin tone */
-       { 0x1F44C, 0x1F3FB }, /* OK hand: light skin tone */
-       { 0x1F44C, 0x1F3FC }, /* OK hand: medium-light skin tone */
-       { 0x1F44C, 0x1F3FD }, /* OK hand: medium skin tone */
-       { 0x1F44C, 0x1F3FE }, /* OK hand: medium-dark skin tone */
-       { 0x1F44C, 0x1F3FF }, /* OK hand: dark skin tone */
-       { 0x1F44D, 0x1F3FB }, /* thumbs up: light skin tone */
-       { 0x1F44D, 0x1F3FC }, /* thumbs up: medium-light skin tone */
-       { 0x1F44D, 0x1F3FD }, /* thumbs up: medium skin tone */
-       { 0x1F44D, 0x1F3FE }, /* thumbs up: medium-dark skin tone */
-       { 0x1F44D, 0x1F3FF }, /* thumbs up: dark skin tone */
-       { 0x1F44E, 0x1F3FB }, /* thumbs down: light skin tone */
-       { 0x1F44E, 0x1F3FC }, /* thumbs down: medium-light skin tone */
-       { 0x1F44E, 0x1F3FD }, /* thumbs down: medium skin tone */
-       { 0x1F44E, 0x1F3FE }, /* thumbs down: medium-dark skin tone */
-       { 0x1F44E, 0x1F3FF }, /* thumbs down: dark skin tone */
-       { 0x1F44F, 0x1F3FB }, /* clapping hands: light skin tone */
-       { 0x1F44F, 0x1F3FC }, /* clapping hands: medium-light skin tone */
-       { 0x1F44F, 0x1F3FD }, /* clapping hands: medium skin tone */
-       { 0x1F44F, 0x1F3FE }, /* clapping hands: medium-dark skin tone */
-       { 0x1F44F, 0x1F3FF }, /* clapping hands: dark skin tone */
-       { 0x1F450, 0x1F3FB }, /* open hands: light skin tone */
-       { 0x1F450, 0x1F3FC }, /* open hands: medium-light skin tone */
-       { 0x1F450, 0x1F3FD }, /* open hands: medium skin tone */
-       { 0x1F450, 0x1F3FE }, /* open hands: medium-dark skin tone */
-       { 0x1F450, 0x1F3FF }, /* open hands: dark skin tone */
-       { 0x1F466, 0x1F3FB }, /* boy: light skin tone */
-       { 0x1F466, 0x1F3FC }, /* boy: medium-light skin tone */
-       { 0x1F466, 0x1F3FD }, /* boy: medium skin tone */
-       { 0x1F466, 0x1F3FE }, /* boy: medium-dark skin tone */
-       { 0x1F466, 0x1F3FF }, /* boy: dark skin tone */
-       { 0x1F467, 0x1F3FB }, /* girl: light skin tone */
-       { 0x1F467, 0x1F3FC }, /* girl: medium-light skin tone */
-       { 0x1F467, 0x1F3FD }, /* girl: medium skin tone */
-       { 0x1F467, 0x1F3FE }, /* girl: medium-dark skin tone */
-       { 0x1F467, 0x1F3FF }, /* girl: dark skin tone */
-       { 0x1F468, 0x1F3FB }, /* man: light skin tone */
-       { 0x1F468, 0x1F3FC }, /* man: medium-light skin tone */
-       { 0x1F468, 0x1F3FD }, /* man: medium skin tone */
-       { 0x1F468, 0x1F3FE }, /* man: medium-dark skin tone */
-       { 0x1F468, 0x1F3FF }, /* man: dark skin tone */
-       { 0x1F469, 0x1F3FB }, /* woman: light skin tone */
-       { 0x1F469, 0x1F3FC }, /* woman: medium-light skin tone */
-       { 0x1F469, 0x1F3FD }, /* woman: medium skin tone */
-       { 0x1F469, 0x1F3FE }, /* woman: medium-dark skin tone */
-       { 0x1F469, 0x1F3FF }, /* woman: dark skin tone */
-       { 0x1F46B, 0x1F3FB }, /* woman and man holding hands: light skin tone */
-       { 0x1F46B, 0x1F3FC }, /* woman and man holding hands: medium-light skin tone */
-       { 0x1F46B, 0x1F3FD }, /* woman and man holding hands: medium skin tone */
-       { 0x1F46B, 0x1F3FE }, /* woman and man holding hands: medium-dark skin tone */
-       { 0x1F46B, 0x1F3FF }, /* woman and man holding hands: dark skin tone */
-       { 0x1F46C, 0x1F3FB }, /* men holding hands: light skin tone */
-       { 0x1F46C, 0x1F3FC }, /* men holding hands: medium-light skin tone */
-       { 0x1F46C, 0x1F3FD }, /* men holding hands: medium skin tone */
-       { 0x1F46C, 0x1F3FE }, /* men holding hands: medium-dark skin tone */
-       { 0x1F46C, 0x1F3FF }, /* men holding hands: dark skin tone */
-       { 0x1F46D, 0x1F3FB }, /* women holding hands: light skin tone */
-       { 0x1F46D, 0x1F3FC }, /* women holding hands: medium-light skin tone */
-       { 0x1F46D, 0x1F3FD }, /* women holding hands: medium skin tone */
-       { 0x1F46D, 0x1F3FE }, /* women holding hands: medium-dark skin tone */
-       { 0x1F46D, 0x1F3FF }, /* women holding hands: dark skin tone */
-       { 0x1F46E, 0x1F3FB }, /* police officer: light skin tone */
-       { 0x1F46E, 0x1F3FC }, /* police officer: medium-light skin tone */
-       { 0x1F46E, 0x1F3FD }, /* police officer: medium skin tone */
-       { 0x1F46E, 0x1F3FE }, /* police officer: medium-dark skin tone */
-       { 0x1F46E, 0x1F3FF }, /* police officer: dark skin tone */
-       { 0x1F470, 0x1F3FB }, /* person with veil: light skin tone */
-       { 0x1F470, 0x1F3FC }, /* person with veil: medium-light skin tone */
-       { 0x1F470, 0x1F3FD }, /* person with veil: medium skin tone */
-       { 0x1F470, 0x1F3FE }, /* person with veil: medium-dark skin tone */
-       { 0x1F470, 0x1F3FF }, /* person with veil: dark skin tone */
-       { 0x1F471, 0x1F3FB }, /* person: light skin tone, blond hair */
-       { 0x1F471, 0x1F3FC }, /* person: medium-light skin tone, blond hair */
-       { 0x1F471, 0x1F3FD }, /* person: medium skin tone, blond hair */
-       { 0x1F471, 0x1F3FE }, /* person: medium-dark skin tone, blond hair */
-       { 0x1F471, 0x1F3FF }, /* person: dark skin tone, blond hair */
-       { 0x1F472, 0x1F3FB }, /* person with skullcap: light skin tone */
-       { 0x1F472, 0x1F3FC }, /* person with skullcap: medium-light skin tone */
-       { 0x1F472, 0x1F3FD }, /* person with skullcap: medium skin tone */
-       { 0x1F472, 0x1F3FE }, /* person with skullcap: medium-dark skin tone */
-       { 0x1F472, 0x1F3FF }, /* person with skullcap: dark skin tone */
-       { 0x1F473, 0x1F3FB }, /* person wearing turban: light skin tone */
-       { 0x1F473, 0x1F3FC }, /* person wearing turban: medium-light skin tone */
-       { 0x1F473, 0x1F3FD }, /* person wearing turban: medium skin tone */
-       { 0x1F473, 0x1F3FE }, /* person wearing turban: medium-dark skin tone */
-       { 0x1F473, 0x1F3FF }, /* person wearing turban: dark skin tone */
-       { 0x1F474, 0x1F3FB }, /* old man: light skin tone */
-       { 0x1F474, 0x1F3FC }, /* old man: medium-light skin tone */
-       { 0x1F474, 0x1F3FD }, /* old man: medium skin tone */
-       { 0x1F474, 0x1F3FE }, /* old man: medium-dark skin tone */
-       { 0x1F474, 0x1F3FF }, /* old man: dark skin tone */
-       { 0x1F475, 0x1F3FB }, /* old woman: light skin tone */
-       { 0x1F475, 0x1F3FC }, /* old woman: medium-light skin tone */
-       { 0x1F475, 0x1F3FD }, /* old woman: medium skin tone */
-       { 0x1F475, 0x1F3FE }, /* old woman: medium-dark skin tone */
-       { 0x1F475, 0x1F3FF }, /* old woman: dark skin tone */
-       { 0x1F476, 0x1F3FB }, /* baby: light skin tone */
-       { 0x1F476, 0x1F3FC }, /* baby: medium-light skin tone */
-       { 0x1F476, 0x1F3FD }, /* baby: medium skin tone */
-       { 0x1F476, 0x1F3FE }, /* baby: medium-dark skin tone */
-       { 0x1F476, 0x1F3FF }, /* baby: dark skin tone */
-       { 0x1F477, 0x1F3FB }, /* construction worker: light skin tone */
-       { 0x1F477, 0x1F3FC }, /* construction worker: medium-light skin tone */
-       { 0x1F477, 0x1F3FD }, /* construction worker: medium skin tone */
-       { 0x1F477, 0x1F3FE }, /* construction worker: medium-dark skin tone */
-       { 0x1F477, 0x1F3FF }, /* construction worker: dark skin tone */
-       { 0x1F478, 0x1F3FB }, /* princess: light skin tone */
-       { 0x1F478, 0x1F3FC }, /* princess: medium-light skin tone */
-       { 0x1F478, 0x1F3FD }, /* princess: medium skin tone */
-       { 0x1F478, 0x1F3FE }, /* princess: medium-dark skin tone */
-       { 0x1F478, 0x1F3FF }, /* princess: dark skin tone */
-       { 0x1F47C, 0x1F3FB }, /* baby angel: light skin tone */
-       { 0x1F47C, 0x1F3FC }, /* baby angel: medium-light skin tone */
-       { 0x1F47C, 0x1F3FD }, /* baby angel: medium skin tone */
-       { 0x1F47C, 0x1F3FE }, /* baby angel: medium-dark skin tone */
-       { 0x1F47C, 0x1F3FF }, /* baby angel: dark skin tone */
-       { 0x1F481, 0x1F3FB }, /* person tipping hand: light skin tone */
-       { 0x1F481, 0x1F3FC }, /* person tipping hand: medium-light skin tone */
-       { 0x1F481, 0x1F3FD }, /* person tipping hand: medium skin tone */
-       { 0x1F481, 0x1F3FE }, /* person tipping hand: medium-dark skin tone */
-       { 0x1F481, 0x1F3FF }, /* person tipping hand: dark skin tone */
-       { 0x1F482, 0x1F3FB }, /* guard: light skin tone */
-       { 0x1F482, 0x1F3FC }, /* guard: medium-light skin tone */
-       { 0x1F482, 0x1F3FD }, /* guard: medium skin tone */
-       { 0x1F482, 0x1F3FE }, /* guard: medium-dark skin tone */
-       { 0x1F482, 0x1F3FF }, /* guard: dark skin tone */
-       { 0x1F483, 0x1F3FB }, /* woman dancing: light skin tone */
-       { 0x1F483, 0x1F3FC }, /* woman dancing: medium-light skin tone */
-       { 0x1F483, 0x1F3FD }, /* woman dancing: medium skin tone */
-       { 0x1F483, 0x1F3FE }, /* woman dancing: medium-dark skin tone */
-       { 0x1F483, 0x1F3FF }, /* woman dancing: dark skin tone */
-       { 0x1F485, 0x1F3FB }, /* nail polish: light skin tone */
-       { 0x1F485, 0x1F3FC }, /* nail polish: medium-light skin tone */
-       { 0x1F485, 0x1F3FD }, /* nail polish: medium skin tone */
-       { 0x1F485, 0x1F3FE }, /* nail polish: medium-dark skin tone */
-       { 0x1F485, 0x1F3FF }, /* nail polish: dark skin tone */
-       { 0x1F486, 0x1F3FB }, /* person getting massage: light skin tone */
-       { 0x1F486, 0x1F3FC }, /* person getting massage: medium-light skin tone */
-       { 0x1F486, 0x1F3FD }, /* person getting massage: medium skin tone */
-       { 0x1F486, 0x1F3FE }, /* person getting massage: medium-dark skin tone */
-       { 0x1F486, 0x1F3FF }, /* person getting massage: dark skin tone */
-       { 0x1F487, 0x1F3FB }, /* person getting haircut: light skin tone */
-       { 0x1F487, 0x1F3FC }, /* person getting haircut: medium-light skin tone */
-       { 0x1F487, 0x1F3FD }, /* person getting haircut: medium skin tone */
-       { 0x1F487, 0x1F3FE }, /* person getting haircut: medium-dark skin tone */
-       { 0x1F487, 0x1F3FF }, /* person getting haircut: dark skin tone */
-       { 0x1F48F, 0x1F3FB }, /* kiss: light skin tone */
-       { 0x1F48F, 0x1F3FC }, /* kiss: medium-light skin tone */
-       { 0x1F48F, 0x1F3FD }, /* kiss: medium skin tone */
-       { 0x1F48F, 0x1F3FE }, /* kiss: medium-dark skin tone */
-       { 0x1F48F, 0x1F3FF }, /* kiss: dark skin tone */
-       { 0x1F491, 0x1F3FB }, /* couple with heart: light skin tone */
-       { 0x1F491, 0x1F3FC }, /* couple with heart: medium-light skin tone */
-       { 0x1F491, 0x1F3FD }, /* couple with heart: medium skin tone */
-       { 0x1F491, 0x1F3FE }, /* couple with heart: medium-dark skin tone */
-       { 0x1F491, 0x1F3FF }, /* couple with heart: dark skin tone */
-       { 0x1F4AA, 0x1F3FB }, /* flexed biceps: light skin tone */
-       { 0x1F4AA, 0x1F3FC }, /* flexed biceps: medium-light skin tone */
-       { 0x1F4AA, 0x1F3FD }, /* flexed biceps: medium skin tone */
-       { 0x1F4AA, 0x1F3FE }, /* flexed biceps: medium-dark skin tone */
-       { 0x1F4AA, 0x1F3FF }, /* flexed biceps: dark skin tone */
-       { 0x1F574, 0x1F3FB }, /* person in suit levitating: light skin tone */
-       { 0x1F574, 0x1F3FC }, /* person in suit levitating: medium-light skin tone */
-       { 0x1F574, 0x1F3FD }, /* person in suit levitating: medium skin tone */
-       { 0x1F574, 0x1F3FE }, /* person in suit levitating: medium-dark skin tone */
-       { 0x1F574, 0x1F3FF }, /* person in suit levitating: dark skin tone */
-       { 0x1F575, 0x1F3FB }, /* detective: light skin tone */
-       { 0x1F575, 0x1F3FC }, /* detective: medium-light skin tone */
-       { 0x1F575, 0x1F3FD }, /* detective: medium skin tone */
-       { 0x1F575, 0x1F3FE }, /* detective: medium-dark skin tone */
-       { 0x1F575, 0x1F3FF }, /* detective: dark skin tone */
-       { 0x1F57A, 0x1F3FB }, /* man dancing: light skin tone */
-       { 0x1F57A, 0x1F3FC }, /* man dancing: medium-light skin tone */
-       { 0x1F57A, 0x1F3FD }, /* man dancing: medium skin tone */
-       { 0x1F57A, 0x1F3FE }, /* man dancing: medium-dark skin tone */
-       { 0x1F57A, 0x1F3FF }, /* man dancing: dark skin tone */
-       { 0x1F590, 0x1F3FB }, /* hand with fingers splayed: light skin tone */
-       { 0x1F590, 0x1F3FC }, /* hand with fingers splayed: medium-light skin tone */
-       { 0x1F590, 0x1F3FD }, /* hand with fingers splayed: medium skin tone */
-       { 0x1F590, 0x1F3FE }, /* hand with fingers splayed: medium-dark skin tone */
-       { 0x1F590, 0x1F3FF }, /* hand with fingers splayed: dark skin tone */
-       { 0x1F595, 0x1F3FB }, /* middle finger: light skin tone */
-       { 0x1F595, 0x1F3FC }, /* middle finger: medium-light skin tone */
-       { 0x1F595, 0x1F3FD }, /* middle finger: medium skin tone */
-       { 0x1F595, 0x1F3FE }, /* middle finger: medium-dark skin tone */
-       { 0x1F595, 0x1F3FF }, /* middle finger: dark skin tone */
-       { 0x1F596, 0x1F3FB }, /* vulcan salute: light skin tone */
-       { 0x1F596, 0x1F3FC }, /* vulcan salute: medium-light skin tone */
-       { 0x1F596, 0x1F3FD }, /* vulcan salute: medium skin tone */
-       { 0x1F596, 0x1F3FE }, /* vulcan salute: medium-dark skin tone */
-       { 0x1F596, 0x1F3FF }, /* vulcan salute: dark skin tone */
-       { 0x1F645, 0x1F3FB }, /* person gesturing NO: light skin tone */
-       { 0x1F645, 0x1F3FC }, /* person gesturing NO: medium-light skin tone */
-       { 0x1F645, 0x1F3FD }, /* person gesturing NO: medium skin tone */
-       { 0x1F645, 0x1F3FE }, /* person gesturing NO: medium-dark skin tone */
-       { 0x1F645, 0x1F3FF }, /* person gesturing NO: dark skin tone */
-       { 0x1F646, 0x1F3FB }, /* person gesturing OK: light skin tone */
-       { 0x1F646, 0x1F3FC }, /* person gesturing OK: medium-light skin tone */
-       { 0x1F646, 0x1F3FD }, /* person gesturing OK: medium skin tone */
-       { 0x1F646, 0x1F3FE }, /* person gesturing OK: medium-dark skin tone */
-       { 0x1F646, 0x1F3FF }, /* person gesturing OK: dark skin tone */
-       { 0x1F647, 0x1F3FB }, /* person bowing: light skin tone */
-       { 0x1F647, 0x1F3FC }, /* person bowing: medium-light skin tone */
-       { 0x1F647, 0x1F3FD }, /* person bowing: medium skin tone */
-       { 0x1F647, 0x1F3FE }, /* person bowing: medium-dark skin tone */
-       { 0x1F647, 0x1F3FF }, /* person bowing: dark skin tone */
-       { 0x1F64B, 0x1F3FB }, /* person raising hand: light skin tone */
-       { 0x1F64B, 0x1F3FC }, /* person raising hand: medium-light skin tone */
-       { 0x1F64B, 0x1F3FD }, /* person raising hand: medium skin tone */
-       { 0x1F64B, 0x1F3FE }, /* person raising hand: medium-dark skin tone */
-       { 0x1F64B, 0x1F3FF }, /* person raising hand: dark skin tone */
-       { 0x1F64C, 0x1F3FB }, /* raising hands: light skin tone */
-       { 0x1F64C, 0x1F3FC }, /* raising hands: medium-light skin tone */
-       { 0x1F64C, 0x1F3FD }, /* raising hands: medium skin tone */
-       { 0x1F64C, 0x1F3FE }, /* raising hands: medium-dark skin tone */
-       { 0x1F64C, 0x1F3FF }, /* raising hands: dark skin tone */
-       { 0x1F64D, 0x1F3FB }, /* person frowning: light skin tone */
-       { 0x1F64D, 0x1F3FC }, /* person frowning: medium-light skin tone */
-       { 0x1F64D, 0x1F3FD }, /* person frowning: medium skin tone */
-       { 0x1F64D, 0x1F3FE }, /* person frowning: medium-dark skin tone */
-       { 0x1F64D, 0x1F3FF }, /* person frowning: dark skin tone */
-       { 0x1F64E, 0x1F3FB }, /* person pouting: light skin tone */
-       { 0x1F64E, 0x1F3FC }, /* person pouting: medium-light skin tone */
-       { 0x1F64E, 0x1F3FD }, /* person pouting: medium skin tone */
-       { 0x1F64E, 0x1F3FE }, /* person pouting: medium-dark skin tone */
-       { 0x1F64E, 0x1F3FF }, /* person pouting: dark skin tone */
-       { 0x1F64F, 0x1F3FB }, /* folded hands: light skin tone */
-       { 0x1F64F, 0x1F3FC }, /* folded hands: medium-light skin tone */
-       { 0x1F64F, 0x1F3FD }, /* folded hands: medium skin tone */
-       { 0x1F64F, 0x1F3FE }, /* folded hands: medium-dark skin tone */
-       { 0x1F64F, 0x1F3FF }, /* folded hands: dark skin tone */
-       { 0x1F6A3, 0x1F3FB }, /* person rowing boat: light skin tone */
-       { 0x1F6A3, 0x1F3FC }, /* person rowing boat: medium-light skin tone */
-       { 0x1F6A3, 0x1F3FD }, /* person rowing boat: medium skin tone */
-       { 0x1F6A3, 0x1F3FE }, /* person rowing boat: medium-dark skin tone */
-       { 0x1F6A3, 0x1F3FF }, /* person rowing boat: dark skin tone */
-       { 0x1F6B4, 0x1F3FB }, /* person biking: light skin tone */
-       { 0x1F6B4, 0x1F3FC }, /* person biking: medium-light skin tone */
-       { 0x1F6B4, 0x1F3FD }, /* person biking: medium skin tone */
-       { 0x1F6B4, 0x1F3FE }, /* person biking: medium-dark skin tone */
-       { 0x1F6B4, 0x1F3FF }, /* person biking: dark skin tone */
-       { 0x1F6B5, 0x1F3FB }, /* person mountain biking: light skin tone */
-       { 0x1F6B5, 0x1F3FC }, /* person mountain biking: medium-light skin tone */
-       { 0x1F6B5, 0x1F3FD }, /* person mountain biking: medium skin tone */
-       { 0x1F6B5, 0x1F3FE }, /* person mountain biking: medium-dark skin tone */
-       { 0x1F6B5, 0x1F3FF }, /* person mountain biking: dark skin tone */
-       { 0x1F6B6, 0x1F3FB }, /* person walking: light skin tone */
-       { 0x1F6B6, 0x1F3FC }, /* person walking: medium-light skin tone */
-       { 0x1F6B6, 0x1F3FD }, /* person walking: medium skin tone */
-       { 0x1F6B6, 0x1F3FE }, /* person walking: medium-dark skin tone */
-       { 0x1F6B6, 0x1F3FF }, /* person walking: dark skin tone */
-       { 0x1F6C0, 0x1F3FB }, /* person taking bath: light skin tone */
-       { 0x1F6C0, 0x1F3FC }, /* person taking bath: medium-light skin tone */
-       { 0x1F6C0, 0x1F3FD }, /* person taking bath: medium skin tone */
-       { 0x1F6C0, 0x1F3FE }, /* person taking bath: medium-dark skin tone */
-       { 0x1F6C0, 0x1F3FF }, /* person taking bath: dark skin tone */
-       { 0x1F6CC, 0x1F3FB }, /* person in bed: light skin tone */
-       { 0x1F6CC, 0x1F3FC }, /* person in bed: medium-light skin tone */
-       { 0x1F6CC, 0x1F3FD }, /* person in bed: medium skin tone */
-       { 0x1F6CC, 0x1F3FE }, /* person in bed: medium-dark skin tone */
-       { 0x1F6CC, 0x1F3FF }, /* person in bed: dark skin tone */
-       { 0x1F90C, 0x1F3FB }, /* pinched fingers: light skin tone */
-       { 0x1F90C, 0x1F3FC }, /* pinched fingers: medium-light skin tone */
-       { 0x1F90C, 0x1F3FD }, /* pinched fingers: medium skin tone */
-       { 0x1F90C, 0x1F3FE }, /* pinched fingers: medium-dark skin tone */
-       { 0x1F90C, 0x1F3FF }, /* pinched fingers: dark skin tone */
-       { 0x1F90F, 0x1F3FB }, /* pinching hand: light skin tone */
-       { 0x1F90F, 0x1F3FC }, /* pinching hand: medium-light skin tone */
-       { 0x1F90F, 0x1F3FD }, /* pinching hand: medium skin tone */
-       { 0x1F90F, 0x1F3FE }, /* pinching hand: medium-dark skin tone */
-       { 0x1F90F, 0x1F3FF }, /* pinching hand: dark skin tone */
-       { 0x1F918, 0x1F3FB }, /* sign of the horns: light skin tone */
-       { 0x1F918, 0x1F3FC }, /* sign of the horns: medium-light skin tone */
-       { 0x1F918, 0x1F3FD }, /* sign of the horns: medium skin tone */
-       { 0x1F918, 0x1F3FE }, /* sign of the horns: medium-dark skin tone */
-       { 0x1F918, 0x1F3FF }, /* sign of the horns: dark skin tone */
-       { 0x1F919, 0x1F3FB }, /* call me hand: light skin tone */
-       { 0x1F919, 0x1F3FC }, /* call me hand: medium-light skin tone */
-       { 0x1F919, 0x1F3FD }, /* call me hand: medium skin tone */
-       { 0x1F919, 0x1F3FE }, /* call me hand: medium-dark skin tone */
-       { 0x1F919, 0x1F3FF }, /* call me hand: dark skin tone */
-       { 0x1F91A, 0x1F3FB }, /* raised back of hand: light skin tone */
-       { 0x1F91A, 0x1F3FC }, /* raised back of hand: medium-light skin tone */
-       { 0x1F91A, 0x1F3FD }, /* raised back of hand: medium skin tone */
-       { 0x1F91A, 0x1F3FE }, /* raised back of hand: medium-dark skin tone */
-       { 0x1F91A, 0x1F3FF }, /* raised back of hand: dark skin tone */
-       { 0x1F91B, 0x1F3FB }, /* left-facing fist: light skin tone */
-       { 0x1F91B, 0x1F3FC }, /* left-facing fist: medium-light skin tone */
-       { 0x1F91B, 0x1F3FD }, /* left-facing fist: medium skin tone */
-       { 0x1F91B, 0x1F3FE }, /* left-facing fist: medium-dark skin tone */
-       { 0x1F91B, 0x1F3FF }, /* left-facing fist: dark skin tone */
-       { 0x1F91C, 0x1F3FB }, /* right-facing fist: light skin tone */
-       { 0x1F91C, 0x1F3FC }, /* right-facing fist: medium-light skin tone */
-       { 0x1F91C, 0x1F3FD }, /* right-facing fist: medium skin tone */
-       { 0x1F91C, 0x1F3FE }, /* right-facing fist: medium-dark skin tone */
-       { 0x1F91C, 0x1F3FF }, /* right-facing fist: dark skin tone */
-       { 0x1F91D, 0x1F3FB }, /* handshake: light skin tone */
-       { 0x1F91D, 0x1F3FC }, /* handshake: medium-light skin tone */
-       { 0x1F91D, 0x1F3FD }, /* handshake: medium skin tone */
-       { 0x1F91D, 0x1F3FE }, /* handshake: medium-dark skin tone */
-       { 0x1F91D, 0x1F3FF }, /* handshake: dark skin tone */
-       { 0x1F91E, 0x1F3FB }, /* crossed fingers: light skin tone */
-       { 0x1F91E, 0x1F3FC }, /* crossed fingers: medium-light skin tone */
-       { 0x1F91E, 0x1F3FD }, /* crossed fingers: medium skin tone */
-       { 0x1F91E, 0x1F3FE }, /* crossed fingers: medium-dark skin tone */
-       { 0x1F91E, 0x1F3FF }, /* crossed fingers: dark skin tone */
-       { 0x1F91F, 0x1F3FB }, /* love-you gesture: light skin tone */
-       { 0x1F91F, 0x1F3FC }, /* love-you gesture: medium-light skin tone */
-       { 0x1F91F, 0x1F3FD }, /* love-you gesture: medium skin tone */
-       { 0x1F91F, 0x1F3FE }, /* love-you gesture: medium-dark skin tone */
-       { 0x1F91F, 0x1F3FF }, /* love-you gesture: dark skin tone */
-       { 0x1F926, 0x1F3FB }, /* person facepalming: light skin tone */
-       { 0x1F926, 0x1F3FC }, /* person facepalming: medium-light skin tone */
-       { 0x1F926, 0x1F3FD }, /* person facepalming: medium skin tone */
-       { 0x1F926, 0x1F3FE }, /* person facepalming: medium-dark skin tone */
-       { 0x1F926, 0x1F3FF }, /* person facepalming: dark skin tone */
-       { 0x1F930, 0x1F3FB }, /* pregnant woman: light skin tone */
-       { 0x1F930, 0x1F3FC }, /* pregnant woman: medium-light skin tone */
-       { 0x1F930, 0x1F3FD }, /* pregnant woman: medium skin tone */
-       { 0x1F930, 0x1F3FE }, /* pregnant woman: medium-dark skin tone */
-       { 0x1F930, 0x1F3FF }, /* pregnant woman: dark skin tone */
-       { 0x1F931, 0x1F3FB }, /* breast-feeding: light skin tone */
-       { 0x1F931, 0x1F3FC }, /* breast-feeding: medium-light skin tone */
-       { 0x1F931, 0x1F3FD }, /* breast-feeding: medium skin tone */
-       { 0x1F931, 0x1F3FE }, /* breast-feeding: medium-dark skin tone */
-       { 0x1F931, 0x1F3FF }, /* breast-feeding: dark skin tone */
-       { 0x1F932, 0x1F3FB }, /* palms up together: light skin tone */
-       { 0x1F932, 0x1F3FC }, /* palms up together: medium-light skin tone */
-       { 0x1F932, 0x1F3FD }, /* palms up together: medium skin tone */
-       { 0x1F932, 0x1F3FE }, /* palms up together: medium-dark skin tone */
-       { 0x1F932, 0x1F3FF }, /* palms up together: dark skin tone */
-       { 0x1F933, 0x1F3FB }, /* selfie: light skin tone */
-       { 0x1F933, 0x1F3FC }, /* selfie: medium-light skin tone */
-       { 0x1F933, 0x1F3FD }, /* selfie: medium skin tone */
-       { 0x1F933, 0x1F3FE }, /* selfie: medium-dark skin tone */
-       { 0x1F933, 0x1F3FF }, /* selfie: dark skin tone */
-       { 0x1F934, 0x1F3FB }, /* prince: light skin tone */
-       { 0x1F934, 0x1F3FC }, /* prince: medium-light skin tone */
-       { 0x1F934, 0x1F3FD }, /* prince: medium skin tone */
-       { 0x1F934, 0x1F3FE }, /* prince: medium-dark skin tone */
-       { 0x1F934, 0x1F3FF }, /* prince: dark skin tone */
-       { 0x1F935, 0x1F3FB }, /* person in tuxedo: light skin tone */
-       { 0x1F935, 0x1F3FC }, /* person in tuxedo: medium-light skin tone */
-       { 0x1F935, 0x1F3FD }, /* person in tuxedo: medium skin tone */
-       { 0x1F935, 0x1F3FE }, /* person in tuxedo: medium-dark skin tone */
-       { 0x1F935, 0x1F3FF }, /* person in tuxedo: dark skin tone */
-       { 0x1F936, 0x1F3FB }, /* Mrs. Claus: light skin tone */
-       { 0x1F936, 0x1F3FC }, /* Mrs. Claus: medium-light skin tone */
-       { 0x1F936, 0x1F3FD }, /* Mrs. Claus: medium skin tone */
-       { 0x1F936, 0x1F3FE }, /* Mrs. Claus: medium-dark skin tone */
-       { 0x1F936, 0x1F3FF }, /* Mrs. Claus: dark skin tone */
-       { 0x1F937, 0x1F3FB }, /* person shrugging: light skin tone */
-       { 0x1F937, 0x1F3FC }, /* person shrugging: medium-light skin tone */
-       { 0x1F937, 0x1F3FD }, /* person shrugging: medium skin tone */
-       { 0x1F937, 0x1F3FE }, /* person shrugging: medium-dark skin tone */
-       { 0x1F937, 0x1F3FF }, /* person shrugging: dark skin tone */
-       { 0x1F938, 0x1F3FB }, /* person cartwheeling: light skin tone */
-       { 0x1F938, 0x1F3FC }, /* person cartwheeling: medium-light skin tone */
-       { 0x1F938, 0x1F3FD }, /* person cartwheeling: medium skin tone */
-       { 0x1F938, 0x1F3FE }, /* person cartwheeling: medium-dark skin tone */
-       { 0x1F938, 0x1F3FF }, /* person cartwheeling: dark skin tone */
-       { 0x1F939, 0x1F3FB }, /* person juggling: light skin tone */
-       { 0x1F939, 0x1F3FC }, /* person juggling: medium-light skin tone */
-       { 0x1F939, 0x1F3FD }, /* person juggling: medium skin tone */
-       { 0x1F939, 0x1F3FE }, /* person juggling: medium-dark skin tone */
-       { 0x1F939, 0x1F3FF }, /* person juggling: dark skin tone */
-       { 0x1F93D, 0x1F3FB }, /* person playing water polo: light skin tone */
-       { 0x1F93D, 0x1F3FC }, /* person playing water polo: medium-light skin tone */
-       { 0x1F93D, 0x1F3FD }, /* person playing water polo: medium skin tone */
-       { 0x1F93D, 0x1F3FE }, /* person playing water polo: medium-dark skin tone */
-       { 0x1F93D, 0x1F3FF }, /* person playing water polo: dark skin tone */
-       { 0x1F93E, 0x1F3FB }, /* person playing handball: light skin tone */
-       { 0x1F93E, 0x1F3FC }, /* person playing handball: medium-light skin tone */
-       { 0x1F93E, 0x1F3FD }, /* person playing handball: medium skin tone */
-       { 0x1F93E, 0x1F3FE }, /* person playing handball: medium-dark skin tone */
-       { 0x1F93E, 0x1F3FF }, /* person playing handball: dark skin tone */
-       { 0x1F977, 0x1F3FB }, /* ninja: light skin tone */
-       { 0x1F977, 0x1F3FC }, /* ninja: medium-light skin tone */
-       { 0x1F977, 0x1F3FD }, /* ninja: medium skin tone */
-       { 0x1F977, 0x1F3FE }, /* ninja: medium-dark skin tone */
-       { 0x1F977, 0x1F3FF }, /* ninja: dark skin tone */
-       { 0x1F9B5, 0x1F3FB }, /* leg: light skin tone */
-       { 0x1F9B5, 0x1F3FC }, /* leg: medium-light skin tone */
-       { 0x1F9B5, 0x1F3FD }, /* leg: medium skin tone */
-       { 0x1F9B5, 0x1F3FE }, /* leg: medium-dark skin tone */
-       { 0x1F9B5, 0x1F3FF }, /* leg: dark skin tone */
-       { 0x1F9B6, 0x1F3FB }, /* foot: light skin tone */
-       { 0x1F9B6, 0x1F3FC }, /* foot: medium-light skin tone */
-       { 0x1F9B6, 0x1F3FD }, /* foot: medium skin tone */
-       { 0x1F9B6, 0x1F3FE }, /* foot: medium-dark skin tone */
-       { 0x1F9B6, 0x1F3FF }, /* foot: dark skin tone */
-       { 0x1F9B8, 0x1F3FB }, /* superhero: light skin tone */
-       { 0x1F9B8, 0x1F3FC }, /* superhero: medium-light skin tone */
-       { 0x1F9B8, 0x1F3FD }, /* superhero: medium skin tone */
-       { 0x1F9B8, 0x1F3FE }, /* superhero: medium-dark skin tone */
-       { 0x1F9B8, 0x1F3FF }, /* superhero: dark skin tone */
-       { 0x1F9B9, 0x1F3FB }, /* supervillain: light skin tone */
-       { 0x1F9B9, 0x1F3FC }, /* supervillain: medium-light skin tone */
-       { 0x1F9B9, 0x1F3FD }, /* supervillain: medium skin tone */
-       { 0x1F9B9, 0x1F3FE }, /* supervillain: medium-dark skin tone */
-       { 0x1F9B9, 0x1F3FF }, /* supervillain: dark skin tone */
-       { 0x1F9BB, 0x1F3FB }, /* ear with hearing aid: light skin tone */
-       { 0x1F9BB, 0x1F3FC }, /* ear with hearing aid: medium-light skin tone */
-       { 0x1F9BB, 0x1F3FD }, /* ear with hearing aid: medium skin tone */
-       { 0x1F9BB, 0x1F3FE }, /* ear with hearing aid: medium-dark skin tone */
-       { 0x1F9BB, 0x1F3FF }, /* ear with hearing aid: dark skin tone */
-       { 0x1F9CD, 0x1F3FB }, /* person standing: light skin tone */
-       { 0x1F9CD, 0x1F3FC }, /* person standing: medium-light skin tone */
-       { 0x1F9CD, 0x1F3FD }, /* person standing: medium skin tone */
-       { 0x1F9CD, 0x1F3FE }, /* person standing: medium-dark skin tone */
-       { 0x1F9CD, 0x1F3FF }, /* person standing: dark skin tone */
-       { 0x1F9CE, 0x1F3FB }, /* person kneeling: light skin tone */
-       { 0x1F9CE, 0x1F3FC }, /* person kneeling: medium-light skin tone */
-       { 0x1F9CE, 0x1F3FD }, /* person kneeling: medium skin tone */
-       { 0x1F9CE, 0x1F3FE }, /* person kneeling: medium-dark skin tone */
-       { 0x1F9CE, 0x1F3FF }, /* person kneeling: dark skin tone */
-       { 0x1F9CF, 0x1F3FB }, /* deaf person: light skin tone */
-       { 0x1F9CF, 0x1F3FC }, /* deaf person: medium-light skin tone */
-       { 0x1F9CF, 0x1F3FD }, /* deaf person: medium skin tone */
-       { 0x1F9CF, 0x1F3FE }, /* deaf person: medium-dark skin tone */
-       { 0x1F9CF, 0x1F3FF }, /* deaf person: dark skin tone */
-       { 0x1F9D1, 0x1F3FB }, /* person: light skin tone */
-       { 0x1F9D1, 0x1F3FC }, /* person: medium-light skin tone */
-       { 0x1F9D1, 0x1F3FD }, /* person: medium skin tone */
-       { 0x1F9D1, 0x1F3FE }, /* person: medium-dark skin tone */
-       { 0x1F9D1, 0x1F3FF }, /* person: dark skin tone */
-       { 0x1F9D2, 0x1F3FB }, /* child: light skin tone */
-       { 0x1F9D2, 0x1F3FC }, /* child: medium-light skin tone */
-       { 0x1F9D2, 0x1F3FD }, /* child: medium skin tone */
-       { 0x1F9D2, 0x1F3FE }, /* child: medium-dark skin tone */
-       { 0x1F9D2, 0x1F3FF }, /* child: dark skin tone */
-       { 0x1F9D3, 0x1F3FB }, /* older person: light skin tone */
-       { 0x1F9D3, 0x1F3FC }, /* older person: medium-light skin tone */
-       { 0x1F9D3, 0x1F3FD }, /* older person: medium skin tone */
-       { 0x1F9D3, 0x1F3FE }, /* older person: medium-dark skin tone */
-       { 0x1F9D3, 0x1F3FF }, /* older person: dark skin tone */
-       { 0x1F9D4, 0x1F3FB }, /* person: light skin tone, beard */
-       { 0x1F9D4, 0x1F3FC }, /* person: medium-light skin tone, beard */
-       { 0x1F9D4, 0x1F3FD }, /* person: medium skin tone, beard */
-       { 0x1F9D4, 0x1F3FE }, /* person: medium-dark skin tone, beard */
-       { 0x1F9D4, 0x1F3FF }, /* person: dark skin tone, beard */
-       { 0x1F9D5, 0x1F3FB }, /* woman with headscarf: light skin tone */
-       { 0x1F9D5, 0x1F3FC }, /* woman with headscarf: medium-light skin tone */
-       { 0x1F9D5, 0x1F3FD }, /* woman with headscarf: medium skin tone */
-       { 0x1F9D5, 0x1F3FE }, /* woman with headscarf: medium-dark skin tone */
-       { 0x1F9D5, 0x1F3FF }, /* woman with headscarf: dark skin tone */
-       { 0x1F9D6, 0x1F3FB }, /* person in steamy room: light skin tone */
-       { 0x1F9D6, 0x1F3FC }, /* person in steamy room: medium-light skin tone */
-       { 0x1F9D6, 0x1F3FD }, /* person in steamy room: medium skin tone */
-       { 0x1F9D6, 0x1F3FE }, /* person in steamy room: medium-dark skin tone */
-       { 0x1F9D6, 0x1F3FF }, /* person in steamy room: dark skin tone */
-       { 0x1F9D7, 0x1F3FB }, /* person climbing: light skin tone */
-       { 0x1F9D7, 0x1F3FC }, /* person climbing: medium-light skin tone */
-       { 0x1F9D7, 0x1F3FD }, /* person climbing: medium skin tone */
-       { 0x1F9D7, 0x1F3FE }, /* person climbing: medium-dark skin tone */
-       { 0x1F9D7, 0x1F3FF }, /* person climbing: dark skin tone */
-       { 0x1F9D8, 0x1F3FB }, /* person in lotus position: light skin tone */
-       { 0x1F9D8, 0x1F3FC }, /* person in lotus position: medium-light skin tone */
-       { 0x1F9D8, 0x1F3FD }, /* person in lotus position: medium skin tone */
-       { 0x1F9D8, 0x1F3FE }, /* person in lotus position: medium-dark skin tone */
-       { 0x1F9D8, 0x1F3FF }, /* person in lotus position: dark skin tone */
-       { 0x1F9D9, 0x1F3FB }, /* mage: light skin tone */
-       { 0x1F9D9, 0x1F3FC }, /* mage: medium-light skin tone */
-       { 0x1F9D9, 0x1F3FD }, /* mage: medium skin tone */
-       { 0x1F9D9, 0x1F3FE }, /* mage: medium-dark skin tone */
-       { 0x1F9D9, 0x1F3FF }, /* mage: dark skin tone */
-       { 0x1F9DA, 0x1F3FB }, /* fairy: light skin tone */
-       { 0x1F9DA, 0x1F3FC }, /* fairy: medium-light skin tone */
-       { 0x1F9DA, 0x1F3FD }, /* fairy: medium skin tone */
-       { 0x1F9DA, 0x1F3FE }, /* fairy: medium-dark skin tone */
-       { 0x1F9DA, 0x1F3FF }, /* fairy: dark skin tone */
-       { 0x1F9DB, 0x1F3FB }, /* vampire: light skin tone */
-       { 0x1F9DB, 0x1F3FC }, /* vampire: medium-light skin tone */
-       { 0x1F9DB, 0x1F3FD }, /* vampire: medium skin tone */
-       { 0x1F9DB, 0x1F3FE }, /* vampire: medium-dark skin tone */
-       { 0x1F9DB, 0x1F3FF }, /* vampire: dark skin tone */
-       { 0x1F9DC, 0x1F3FB }, /* merperson: light skin tone */
-       { 0x1F9DC, 0x1F3FC }, /* merperson: medium-light skin tone */
-       { 0x1F9DC, 0x1F3FD }, /* merperson: medium skin tone */
-       { 0x1F9DC, 0x1F3FE }, /* merperson: medium-dark skin tone */
-       { 0x1F9DC, 0x1F3FF }, /* merperson: dark skin tone */
-       { 0x1F9DD, 0x1F3FB }, /* elf: light skin tone */
-       { 0x1F9DD, 0x1F3FC }, /* elf: medium-light skin tone */
-       { 0x1F9DD, 0x1F3FD }, /* elf: medium skin tone */
-       { 0x1F9DD, 0x1F3FE }, /* elf: medium-dark skin tone */
-       { 0x1F9DD, 0x1F3FF }, /* elf: dark skin tone */
-       { 0x1FAC3, 0x1F3FB }, /* pregnant man: light skin tone */
-       { 0x1FAC3, 0x1F3FC }, /* pregnant man: medium-light skin tone */
-       { 0x1FAC3, 0x1F3FD }, /* pregnant man: medium skin tone */
-       { 0x1FAC3, 0x1F3FE }, /* pregnant man: medium-dark skin tone */
-       { 0x1FAC3, 0x1F3FF }, /* pregnant man: dark skin tone */
-       { 0x1FAC4, 0x1F3FB }, /* pregnant person: light skin tone */
-       { 0x1FAC4, 0x1F3FC }, /* pregnant person: medium-light skin tone */
-       { 0x1FAC4, 0x1F3FD }, /* pregnant person: medium skin tone */
-       { 0x1FAC4, 0x1F3FE }, /* pregnant person: medium-dark skin tone */
-       { 0x1FAC4, 0x1F3FF }, /* pregnant person: dark skin tone */
-       { 0x1FAC5, 0x1F3FB }, /* person with crown: light skin tone */
-       { 0x1FAC5, 0x1F3FC }, /* person with crown: medium-light skin tone */
-       { 0x1FAC5, 0x1F3FD }, /* person with crown: medium skin tone */
-       { 0x1FAC5, 0x1F3FE }, /* person with crown: medium-dark skin tone */
-       { 0x1FAC5, 0x1F3FF }, /* person with crown: dark skin tone */
-       { 0x1FAF0, 0x1F3FB }, /* hand with index finger and thumb crossed: light skin tone */
-       { 0x1FAF0, 0x1F3FC }, /* hand with index finger and thumb crossed: medium-light skin tone */
-       { 0x1FAF0, 0x1F3FD }, /* hand with index finger and thumb crossed: medium skin tone */
-       { 0x1FAF0, 0x1F3FE }, /* hand with index finger and thumb crossed: medium-dark skin tone */
-       { 0x1FAF0, 0x1F3FF }, /* hand with index finger and thumb crossed: dark skin tone */
-       { 0x1FAF1, 0x1F3FB }, /* rightwards hand: light skin tone */
-       { 0x1FAF1, 0x1F3FC }, /* rightwards hand: medium-light skin tone */
-       { 0x1FAF1, 0x1F3FD }, /* rightwards hand: medium skin tone */
-       { 0x1FAF1, 0x1F3FE }, /* rightwards hand: medium-dark skin tone */
-       { 0x1FAF1, 0x1F3FF }, /* rightwards hand: dark skin tone */
-       { 0x1FAF2, 0x1F3FB }, /* leftwards hand: light skin tone */
-       { 0x1FAF2, 0x1F3FC }, /* leftwards hand: medium-light skin tone */
-       { 0x1FAF2, 0x1F3FD }, /* leftwards hand: medium skin tone */
-       { 0x1FAF2, 0x1F3FE }, /* leftwards hand: medium-dark skin tone */
-       { 0x1FAF2, 0x1F3FF }, /* leftwards hand: dark skin tone */
-       { 0x1FAF3, 0x1F3FB }, /* palm down hand: light skin tone */
-       { 0x1FAF3, 0x1F3FC }, /* palm down hand: medium-light skin tone */
-       { 0x1FAF3, 0x1F3FD }, /* palm down hand: medium skin tone */
-       { 0x1FAF3, 0x1F3FE }, /* palm down hand: medium-dark skin tone */
-       { 0x1FAF3, 0x1F3FF }, /* palm down hand: dark skin tone */
-       { 0x1FAF4, 0x1F3FB }, /* palm up hand: light skin tone */
-       { 0x1FAF4, 0x1F3FC }, /* palm up hand: medium-light skin tone */
-       { 0x1FAF4, 0x1F3FD }, /* palm up hand: medium skin tone */
-       { 0x1FAF4, 0x1F3FE }, /* palm up hand: medium-dark skin tone */
-       { 0x1FAF4, 0x1F3FF }, /* palm up hand: dark skin tone */
-       { 0x1FAF5, 0x1F3FB }, /* index pointing at the viewer: light skin tone */
-       { 0x1FAF5, 0x1F3FC }, /* index pointing at the viewer: medium-light skin tone */
-       { 0x1FAF5, 0x1F3FD }, /* index pointing at the viewer: medium skin tone */
-       { 0x1FAF5, 0x1F3FE }, /* index pointing at the viewer: medium-dark skin tone */
-       { 0x1FAF5, 0x1F3FF }, /* index pointing at the viewer: dark skin tone */
-       { 0x1FAF6, 0x1F3FB }, /* heart hands: light skin tone */
-       { 0x1FAF6, 0x1F3FC }, /* heart hands: medium-light skin tone */
-       { 0x1FAF6, 0x1F3FD }, /* heart hands: medium skin tone */
-       { 0x1FAF6, 0x1F3FE }, /* heart hands: medium-dark skin tone */
-       { 0x1FAF6, 0x1F3FF }, /* heart hands: dark skin tone */
-       { 0x1FAF7, 0x1F3FB }, /* leftwards pushing hand: light skin tone */
-       { 0x1FAF7, 0x1F3FC }, /* leftwards pushing hand: medium-light skin tone */
-       { 0x1FAF7, 0x1F3FD }, /* leftwards pushing hand: medium skin tone */
-       { 0x1FAF7, 0x1F3FE }, /* leftwards pushing hand: medium-dark skin tone */
-       { 0x1FAF7, 0x1F3FF }, /* leftwards pushing hand: dark skin tone */
-       { 0x1FAF8, 0x1F3FB }, /* rightwards pushing hand: light skin tone */
-       { 0x1FAF8, 0x1F3FC }, /* rightwards pushing hand: medium-light skin tone */
-       { 0x1FAF8, 0x1F3FD }, /* rightwards pushing hand: medium skin tone */
-       { 0x1FAF8, 0x1F3FE }, /* rightwards pushing hand: medium-dark skin tone */
-       { 0x1FAF8, 0x1F3FF }, /* rightwards pushing hand: dark skin tone */
+static const wchar_t utf8_modifier_table[] = {
+       0x1F1E6,
+       0x1F1E7,
+       0x1F1E8,
+       0x1F1E9,
+       0x1F1EA,
+       0x1F1EB,
+       0x1F1EC,
+       0x1F1ED,
+       0x1F1EE,
+       0x1F1EF,
+       0x1F1F0,
+       0x1F1F1,
+       0x1F1F2,
+       0x1F1F3,
+       0x1F1F4,
+       0x1F1F5,
+       0x1F1F6,
+       0x1F1F7,
+       0x1F1F8,
+       0x1F1F9,
+       0x1F1FA,
+       0x1F1FB,
+       0x1F1FC,
+       0x1F1FD,
+       0x1F1FE,
+       0x1F1FF,
+       0x1F3FB,
+       0x1F3FC,
+       0x1F3FD,
+       0x1F3FE,
+       0x1F3FF
 };
 
-struct utf8_combined_first {
-       struct utf8_data                 first;
-
-       struct utf8_data                *second;
-       u_int                            count;
-
-       RB_ENTRY(utf8_combined_first)    entry;
-};
-
-static int
-utf8_combined_first_cmp(struct utf8_combined_first *uf1,
-    struct utf8_combined_first *uf2)
-{
-       struct utf8_data        *ud1 = &uf1->first, *ud2 = &uf2->first;
-
-       if (ud1->size < ud2->size)
-               return (-1);
-       if (ud1->size > ud2->size)
-               return (1);
-       return (memcmp(ud1->data, ud2->data, ud1->size));
-}
-RB_HEAD(utf8_combined_tree, utf8_combined_first);
-RB_GENERATE_STATIC(utf8_combined_tree, utf8_combined_first, entry,
-    utf8_combined_first_cmp);
-static struct utf8_combined_tree utf8_combined_tree =
-    RB_INITIALIZER(utf8_combined_tree);
-
-static int
-utf8_combined_second_cmp(const void *vp1, const void *vp2)
+/* Has this got a zero width joiner at the end? */
+int
+utf8_has_zwj(const struct utf8_data *ud)
 {
-       const struct utf8_data  *ud1 = vp1, *ud2 = vp2;
-
-       if (ud1->size < ud2->size)
-               return (-1);
-       if (ud1->size > ud2->size)
-               return (1);
-       return (memcmp(ud1->data, ud2->data, ud1->size));
+       if (ud->size < 3)
+               return (0);
+       return (memcmp(ud->data + ud->size - 3, "\342\200\215", 3) == 0);
 }
 
-static int
+/* Is this a zero width joiner? */
+int
 utf8_is_zwj(const struct utf8_data *ud)
 {
-       return (ud->size == 3 && memcmp(ud->data, "\342\200\215", 3) == 0);
-}
-
-static struct utf8_data *
-utf8_add_zwj(const struct utf8_data *ud)
-{
-       static struct utf8_data new;
-
-       if (ud->size + 3 > UTF8_SIZE)
-               return (NULL);
-       memset(&new, 0, sizeof new);
-       memcpy(new.data, "\342\200\215", 3);
-       memcpy(new.data + 3, ud->data, ud->size);
-       new.size = 3 + ud->size;
-       new.width = ud->width;
-       return (&new);
-}
-
-static struct utf8_combined_first *
-utf8_find_combined_first(const struct utf8_data *first)
-{
-       struct utf8_combined_first      uf;
-
-       memset(&uf, 0, sizeof uf);
-       utf8_copy(&uf.first, first);
-       return (RB_FIND(utf8_combined_tree, &utf8_combined_tree, &uf));
+       if (ud->size != 3)
+               return (0);
+       return (memcmp(ud->data, "\342\200\215", 3) == 0);
 }
 
-static int
-utf8_find_combined_second(struct utf8_combined_first *uf,
-    const struct utf8_data *second)
-{
-       return (bsearch(second, uf->second, uf->count, sizeof *uf->second,
-           utf8_combined_second_cmp) != NULL);
-}
-
-void
-utf8_build_combined(void)
+/* Is this a variation selector? */
+int
+utf8_is_vs(const struct utf8_data *ud)
 {
-       struct utf8_data                 first, second;
-       int                              mlen;
-       u_int                            i;
-       wchar_t                          wc;
-       struct utf8_combined_first      *uf;
-
-       for (i = 0; i < nitems(utf8_combined_table); i++) {
-               memset(&first, 0, sizeof first);
-               wc = utf8_combined_table[i].first;
-               mlen = wctomb(first.data, wc);
-               if (mlen <= 0 || mlen > UTF8_SIZE) {
-                       log_debug("invalid combined character %05X", wc);
-                       continue;
-               }
-               first.size = mlen;
-
-               uf = utf8_find_combined_first(&first);
-               if (uf == NULL) {
-                       uf = xcalloc(1, sizeof *uf);
-                       utf8_copy(&uf->first, &first);
-                       RB_INSERT(utf8_combined_tree, &utf8_combined_tree, uf);
-               }
-
-               memset(&second, 0, sizeof second);
-               wc = utf8_combined_table[i].second;
-               mlen = wctomb(second.data, wc);
-               if (mlen <= 0 || mlen > UTF8_SIZE) {
-                       log_debug("invalid combined character %05X", wc);
-                       continue;
-               }
-               second.size = mlen;
-
-               log_debug("combined character %05X+%05X = %.*s+%.*s",
-                   utf8_combined_table[i].first, utf8_combined_table[i].second,
-                   (int)first.size, first.data, (int)second.size, second.data);
-
-               uf->second = xreallocarray(uf->second, uf->count + 1,
-                   sizeof *uf->second);
-               utf8_copy(&uf->second[uf->count], &second);
-               uf->count++;
-       }
-
-       RB_FOREACH(uf, utf8_combined_tree, &utf8_combined_tree) {
-               qsort(uf->second, uf->count, sizeof *uf->second,
-                   utf8_combined_second_cmp);
-       }
+       if (ud->size != 3)
+               return (0);
+       return (memcmp(ud->data, "\357\270\217", 3) == 0);
 }
 
+/* Is this in the modifier table? */
 int
-utf8_try_combined(const struct utf8_data *ud, const struct utf8_data *last,
-    const struct utf8_data **combine, u_int *width)
+utf8_is_modifier(const struct utf8_data *ud)
 {
-       struct utf8_combined_first      *uf;
-
-       /* Use the incoming width by default. */
-       *width = ud->width;
-
-       /*
-        * If this is a zero width joiner, discard it but try to combine the
-        * next character.
-        */
-       if (utf8_is_zwj(ud))
-               return (UTF8_DISCARD_MAYBE_COMBINE);
-
-       /*
-        * If there is a previous character to combine and it is a ZWJ,
-        * combine with the new character and a ZWJ.
-        */
-       if (last != NULL && utf8_is_zwj(last)) {
-               *combine = utf8_add_zwj(ud);
-               if (*combine == NULL)
-                       return (UTF8_DISCARD_NOW);
-               return (UTF8_COMBINE_NOW);
-       }
-
-       /*
-        * If the width of this character is zero, combine onto the previous
-        * character.
-        */
-       if (ud->width == 0) {
-               *combine = ud;
-               return (UTF8_COMBINE_NOW);
-       }
-
-       /*
-        * Look up the character in the first character list, if it is missing,
-        * write it immediately. If it is present, write but try to combine
-        * later; also force the width to two.
-        */
-       if (last == NULL) {
-               if (utf8_find_combined_first(ud) != NULL) {
-                       *width = 2;
-                       return (UTF8_WRITE_MAYBE_COMBINE);
-               }
-               return (UTF8_WRITE_NOW);
-       }
-
-       /*
-        * This must be a potential combined character. If both first and
-        * second characters are on the list, combine.
-        */
-       uf = utf8_find_combined_first(last);
-       if (uf != NULL && utf8_find_combined_second(uf, ud)) {
-               *combine = ud;
-               return (UTF8_COMBINE_NOW);
-       }
-
-       return (UTF8_WRITE_NOW);
+       wchar_t wc;
+
+       if (utf8_towc(ud, &wc) != UTF8_DONE)
+               return (0);
+       if (!utf8_in_table(wc, utf8_modifier_table,
+           nitems(utf8_modifier_table)))
+               return (0);
+       return (1);
 }
index c1c56dd..888d015 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: utf8.c,v 1.63 2023/09/01 14:29:11 nicm Exp $ */
+/* $OpenBSD: utf8.c,v 1.64 2023/09/15 15:49:05 nicm Exp $ */
 
 /*
  * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
 #include <stdlib.h>
 #include <string.h>
 #include <vis.h>
-#include <wchar.h>
 
 #include "tmux.h"
 
+static const wchar_t utf8_force_wide[] = {
+       0x0261D,
+       0x026F9,
+       0x0270A,
+       0x0270B,
+       0x0270C,
+       0x0270D,
+       0x1F1E6,
+       0x1F1E7,
+       0x1F1E8,
+       0x1F1E9,
+       0x1F1EA,
+       0x1F1EB,
+       0x1F1EC,
+       0x1F1ED,
+       0x1F1EE,
+       0x1F1EF,
+       0x1F1F0,
+       0x1F1F1,
+       0x1F1F2,
+       0x1F1F3,
+       0x1F1F4,
+       0x1F1F5,
+       0x1F1F6,
+       0x1F1F7,
+       0x1F1F8,
+       0x1F1F9,
+       0x1F1FA,
+       0x1F1FB,
+       0x1F1FC,
+       0x1F1FD,
+       0x1F1FE,
+       0x1F1FF,
+       0x1F385,
+       0x1F3C2,
+       0x1F3C3,
+       0x1F3C4,
+       0x1F3C7,
+       0x1F3CA,
+       0x1F3CB,
+       0x1F3CC,
+       0x1F3FB,
+       0x1F3FC,
+       0x1F3FD,
+       0x1F3FE,
+       0x1F3FF,
+       0x1F442,
+       0x1F443,
+       0x1F446,
+       0x1F447,
+       0x1F448,
+       0x1F449,
+       0x1F44A,
+       0x1F44B,
+       0x1F44C,
+       0x1F44D,
+       0x1F44E,
+       0x1F44F,
+       0x1F450,
+       0x1F466,
+       0x1F467,
+       0x1F468,
+       0x1F469,
+       0x1F46B,
+       0x1F46C,
+       0x1F46D,
+       0x1F46E,
+       0x1F470,
+       0x1F471,
+       0x1F472,
+       0x1F473,
+       0x1F474,
+       0x1F475,
+       0x1F476,
+       0x1F477,
+       0x1F478,
+       0x1F47C,
+       0x1F481,
+       0x1F482,
+       0x1F483,
+       0x1F485,
+       0x1F486,
+       0x1F487,
+       0x1F48F,
+       0x1F491,
+       0x1F4AA,
+       0x1F574,
+       0x1F575,
+       0x1F57A,
+       0x1F590,
+       0x1F595,
+       0x1F596,
+       0x1F645,
+       0x1F646,
+       0x1F647,
+       0x1F64B,
+       0x1F64C,
+       0x1F64D,
+       0x1F64E,
+       0x1F64F,
+       0x1F6A3,
+       0x1F6B4,
+       0x1F6B5,
+       0x1F6B6,
+       0x1F6C0,
+       0x1F6CC,
+       0x1F90C,
+       0x1F90F,
+       0x1F918,
+       0x1F919,
+       0x1F91A,
+       0x1F91B,
+       0x1F91C,
+       0x1F91D,
+       0x1F91E,
+       0x1F91F,
+       0x1F926,
+       0x1F930,
+       0x1F931,
+       0x1F932,
+       0x1F933,
+       0x1F934,
+       0x1F935,
+       0x1F936,
+       0x1F937,
+       0x1F938,
+       0x1F939,
+       0x1F93D,
+       0x1F93E,
+       0x1F977,
+       0x1F9B5,
+       0x1F9B6,
+       0x1F9B8,
+       0x1F9B9,
+       0x1F9BB,
+       0x1F9CD,
+       0x1F9CE,
+       0x1F9CF,
+       0x1F9D1,
+       0x1F9D2,
+       0x1F9D3,
+       0x1F9D4,
+       0x1F9D5,
+       0x1F9D6,
+       0x1F9D7,
+       0x1F9D8,
+       0x1F9D9,
+       0x1F9DA,
+       0x1F9DB,
+       0x1F9DC,
+       0x1F9DD,
+       0x1FAC3,
+       0x1FAC4,
+       0x1FAC5,
+       0x1FAF0,
+       0x1FAF1,
+       0x1FAF2,
+       0x1FAF3,
+       0x1FAF4,
+       0x1FAF5,
+       0x1FAF6,
+       0x1FAF7,
+       0x1FAF8
+};
+
 struct utf8_item {
        RB_ENTRY(utf8_item)     index_entry;
        u_int                   index;
@@ -123,6 +287,28 @@ utf8_put_item(const u_char *data, size_t size, u_int *index)
        return (0);
 }
 
+static int
+utf8_table_cmp(const void *vp1, const void *vp2)
+{
+       const wchar_t   *wc1 = vp1, *wc2 = vp2;
+
+       if (*wc1 < *wc2)
+               return (-1);
+       if (*wc1 > *wc2)
+               return (1);
+       return (0);
+}
+
+/* Check if character in table. */
+int
+utf8_in_table(wchar_t find, const wchar_t *table, u_int count)
+{
+       wchar_t *found;
+
+       found = bsearch(&find, table, count, sizeof *table, utf8_table_cmp);
+       return (found != NULL);
+}
+
 /* Get UTF-8 character from data. */
 enum utf8_state
 utf8_from_data(const struct utf8_data *ud, utf8_char *uc)
@@ -217,16 +403,13 @@ utf8_width(struct utf8_data *ud, int *width)
 {
        wchar_t wc;
 
-       switch (mbtowc(&wc, ud->data, ud->size)) {
-       case -1:
-               log_debug("UTF-8 %.*s, mbtowc() %d", (int)ud->size, ud->data,
-                   errno);
-               mbtowc(NULL, NULL, MB_CUR_MAX);
-               return (UTF8_ERROR);
-       case 0:
+       if (utf8_towc(ud, &wc) != UTF8_DONE)
                return (UTF8_ERROR);
+       if (utf8_in_table(wc, utf8_force_wide, nitems(utf8_force_wide))) {
+               *width = 2;
+               return (UTF8_DONE);
        }
-       log_debug("UTF-8 %.*s is %05X", (int)ud->size, ud->data, (u_int)wc);
+
        *width = wcwidth(wc);
        log_debug("wcwidth(%05X) returned %d", (u_int)wc, *width);
        if (*width < 0) {
@@ -241,6 +424,23 @@ utf8_width(struct utf8_data *ud, int *width)
        return (UTF8_ERROR);
 }
 
+/* Convert UTF-8 character to wide character. */
+enum utf8_state
+utf8_towc(const struct utf8_data *ud, wchar_t *wc)
+{
+       switch (mbtowc(wc, ud->data, ud->size)) {
+       case -1:
+               log_debug("UTF-8 %.*s, mbtowc() %d", (int)ud->size, ud->data,
+                   errno);
+               mbtowc(NULL, NULL, MB_CUR_MAX);
+               return (UTF8_ERROR);
+       case 0:
+               return (UTF8_ERROR);
+       }
+       log_debug("UTF-8 %.*s is %05X", (int)ud->size, ud->data, (u_int)*wc);
+       return (UTF8_DONE);
+}
+
 /*
  * Open UTF-8 sequence.
  *