aboutsummaryrefslogtreecommitdiffstats
path: root/text/normalize.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 11:43:16 +0100
committereunhhu <[email protected]>2026-09-28 11:43:16 +0100
commit482d3c93255277f5251008638bc59bfab0eb4b9e (patch)
tree5e230bdcf6c04f50c7a98dfe50f403b502add1e3 /text/normalize.c
parent8ba0615b0fe832a15eb695ccd661674b6946995c (diff)
downloadvega-482d3c93255277f5251008638bc59bfab0eb4b9e.tar.gz
vega-482d3c93255277f5251008638bc59bfab0eb4b9e.zip
Normalize text to NFD and NFC, passing all of Unicode's normalization tests
Diffstat (limited to 'text/normalize.c')
-rw-r--r--text/normalize.c72
1 files changed, 72 insertions, 0 deletions
diff --git a/text/normalize.c b/text/normalize.c
new file mode 100644
index 0000000..302a4de
--- /dev/null
+++ b/text/normalize.c
@@ -0,0 +1,72 @@
+/* Normalization forms D and C (UAX #15, Unicode 15.1): text/text.h. */
+#include "text.h"
+#include <string.h>
+
+/* Hangul syllables are made of jamo arithmetically (Unicode section 3.12). */
+enum { SBASE = 0xAC00, LBASE = 0x1100, VBASE = 0x1161, TBASE = 0x11A7, LCOUNT = 19, VCOUNT = 21, TCOUNT = 28,
+ NCOUNT = VCOUNT * TCOUNT, SCOUNT = LCOUNT * NCOUNT };
+
+size_t vega_nfd(const uint32_t *in, size_t count, uint32_t *out) {
+ size_t n = 0;
+ for (size_t i = 0; i < count; ++i) {
+ uint32_t code = in[i];
+ if (code >= SBASE && code < SBASE + SCOUNT) {
+ uint32_t s = code - SBASE;
+ out[n++] = LBASE + s / NCOUNT;
+ out[n++] = VBASE + s % NCOUNT / TCOUNT;
+ if (s % TCOUNT) out[n++] = TBASE + s % TCOUNT;
+ continue;
+ }
+ const uint32_t *parts;
+ size_t length = vega_decomposition(code, &parts);
+ if (length) {
+ memcpy(out + n, parts, length * sizeof(*out));
+ n += length;
+ } else
+ out[n++] = code;
+ }
+ /* Canonical order: each mark moves before the marks of a higher class
+ * ahead of it, up to a starter (a stable sort of every run of marks). */
+ for (size_t i = 1; i < n; ++i) {
+ uint8_t class = vega_combining_class(out[i]);
+ if (!class) continue;
+ for (size_t j = i; j && vega_combining_class(out[j - 1]) > class; --j) {
+ uint32_t swap = out[j - 1];
+ out[j - 1] = out[j];
+ out[j] = swap;
+ }
+ }
+ return n;
+}
+
+static uint32_t compose(uint32_t first, uint32_t second) {
+ if (first >= LBASE && first < LBASE + LCOUNT && second >= VBASE && second < VBASE + VCOUNT)
+ return SBASE + ((first - LBASE) * VCOUNT + (second - VBASE)) * TCOUNT;
+ if (first >= SBASE && first < SBASE + SCOUNT && (first - SBASE) % TCOUNT == 0 && second > TBASE &&
+ second < TBASE + TCOUNT)
+ return first + (second - TBASE);
+ return vega_composition(first, second);
+}
+
+size_t vega_nfc(const uint32_t *in, size_t count, uint32_t *out) {
+ size_t n = vega_nfd(in, count, out);
+ if (!n) return 0;
+ /* Each character after the last starter composes with it unless a
+ * character between them blocks it (one of the same or a higher class,
+ * or a starter). */
+ size_t starter = 0, written = 1;
+ int last = vega_combining_class(out[0]) ? 256 : 0;
+ for (size_t i = 1; i < n; ++i) {
+ uint32_t code = out[i];
+ int class = vega_combining_class(code);
+ uint32_t composite = compose(out[starter], code);
+ if (composite && (last < class || last == 0)) {
+ out[starter] = composite;
+ continue;
+ }
+ if (!class) starter = written;
+ last = class;
+ out[written++] = code;
+ }
+ return written;
+}