main
xs_unicode.h
1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2
3#ifndef _XS_UNICODE_H
4
5#define _XS_UNICODE_H
6
7#include "xs.h"
8
9 int xs_utf8_enc(char buf[4], unsigned int cpoint);
10 int xs_is_utf8_cont_byte(char c);
11 unsigned int xs_utf8_dec(const char **str);
12 int xs_unicode_width(unsigned int cpoint);
13 int xs_is_surrogate(unsigned int cpoint);
14 int xs_is_diacritic(unsigned int cpoint);
15 unsigned int xs_surrogate_dec(unsigned int p1, unsigned int p2);
16 unsigned int xs_surrogate_enc(unsigned int cpoint);
17 unsigned int *_xs_unicode_upper_search(unsigned int cpoint);
18 unsigned int *_xs_unicode_lower_search(unsigned int cpoint);
19 #define xs_unicode_is_upper(cpoint) (!!_xs_unicode_upper_search(cpoint))
20 #define xs_unicode_is_lower(cpoint) (!!_xs_unicode_lower_search(cpoint))
21 unsigned int xs_unicode_toupper(unsigned int cpoint);
22 unsigned int xs_unicode_tolower(unsigned int cpoint);
23 int xs_unicode_nfd(unsigned int cpoint, unsigned int *base, unsigned int *diac);
24 int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint);
25 int xs_unicode_is_alpha(unsigned int cpoint);
26 int xs_unicode_is_right_to_left(unsigned int cpoint);
27 int xs_is_emoji(unsigned int cpoint);
28 int xs_utf8_len(const char *str);
29
30#ifdef _XS_H
31 xs_str *xs_utf8_insert(xs_str *str, unsigned int cpoint, int *offset);
32 xs_str *xs_utf8_cat(xs_str *str, unsigned int cpoint);
33 xs_str *xs_utf8_crop_i(xs_str *str, int begin, int end);
34 xs_str *xs_utf8_toupper(const char *str);
35 xs_str *xs_utf8_tolower(const char *str);
36 xs_str *xs_utf8_to_nfd(const char *str);
37 xs_str *xs_utf8_to_nfc(const char *str);
38#endif
39
40#ifdef XS_IMPLEMENTATION
41
42#include <ctype.h>
43
44#ifndef xs_countof
45#define xs_countof(a) (sizeof((a)) / sizeof((*a)))
46#endif
47
48int xs_utf8_enc(char buf[4], unsigned int cpoint)
49/* encodes an Unicode codepoint to utf-8 into buf and returns the size in bytes */
50{
51 char *p = buf;
52
53 if (cpoint < 0x80) /* 1 byte char */
54 *p++ = cpoint & 0xff;
55 else {
56 if (cpoint < 0x800) /* 2 byte char */
57 *p++ = 0xc0 | (cpoint >> 6);
58 else {
59 if (cpoint < 0x10000) /* 3 byte char */
60 *p++ = 0xe0 | (cpoint >> 12);
61 else { /* 4 byte char */
62 *p++ = 0xf0 | (cpoint >> 18);
63 *p++ = 0x80 | ((cpoint >> 12) & 0x3f);
64 }
65
66 *p++ = 0x80 | ((cpoint >> 6) & 0x3f);
67 }
68
69 *p++ = 0x80 | (cpoint & 0x3f);
70 }
71
72 return p - buf;
73}
74
75
76int xs_is_utf8_cont_byte(char c)
77/* returns true if c is an utf8 continuation byte */
78{
79 return ((c & 0xc0) == 0x80);
80}
81
82
83unsigned int xs_utf8_dec(const char **str)
84/* decodes an utf-8 char inside str and updates the pointer */
85{
86 const char *p = *str;
87
88 if (!xs_is_string(p))
89 return 0;
90
91 unsigned int cpoint = 0;
92 unsigned char c = *p++;
93 int cb = 0;
94
95 if ((c & 0x80) == 0) { /* 1 byte char */
96 cpoint = c;
97 }
98 else
99 if ((c & 0xe0) == 0xc0) { /* 2 byte char */
100 cpoint = (c & 0x1f) << 6;
101 cb = 1;
102 }
103 else
104 if ((c & 0xf0) == 0xe0) { /* 3 byte char */
105 cpoint = (c & 0x0f) << 12;
106 cb = 2;
107 }
108 else
109 if ((c & 0xf8) == 0xf0) { /* 4 byte char */
110 cpoint = (c & 0x07) << 18;
111 cb = 3;
112 }
113
114 /* process the continuation bytes */
115 while (cb > 0 && *p && xs_is_utf8_cont_byte(*p))
116 cpoint |= (*p++ & 0x3f) << (--cb * 6);
117
118 /* incomplete or broken? */
119 if (cb)
120 cpoint = 0xfffd;
121
122 *str = p;
123 return cpoint;
124}
125
126
127/** Unicode character width: intentionally dead simple **/
128
129static unsigned int xs_unicode_width_table[] = {
130 0x300, 0x36f, 0, /* diacritics */
131 0x1100, 0x11ff, 2, /* Hangul */
132 0x2e80, 0xa4cf, 2, /* CJK */
133 0xac00, 0xd7a3, 2, /* more Hangul */
134 0xe000, 0xf8ff, 0, /* private use */
135 0xf900, 0xfaff, 2, /* CJK compatibility */
136 0xff00, 0xff60, 2, /* full width things */
137 0xffdf, 0xffe6, 2, /* full width things */
138 0x1f200, 0x1ffff, 2, /* emojis */
139 0x20000, 0x2fffd, 2 /* more CJK */
140};
141
142/* magic number from https://en.wikipedia.org/wiki/Emoji#Unicode_blocks */
143int xs_is_emoji(unsigned int cpoint) {
144/* returns wether the input is an utf8 emoji */
145 return cpoint > 0x00A9 && cpoint < 0x1ffff;
146}
147
148int xs_unicode_width(unsigned int cpoint)
149/* returns the width in columns of a Unicode codepoint (somewhat simplified) */
150{
151 int b = 0;
152 int t = xs_countof(xs_unicode_width_table) / 3 - 1;
153
154 while (t >= b) {
155 int n = (b + t) / 2;
156 unsigned int *p = &xs_unicode_width_table[n * 3];
157
158 if (cpoint < p[0])
159 t = n - 1;
160 else
161 if (cpoint > p[1])
162 b = n + 1;
163 else
164 return p[2];
165 }
166
167 return 1;
168}
169
170
171int xs_is_diacritic(unsigned int cpoint)
172{
173 return cpoint >= 0x300 && cpoint <= 0x36f;
174}
175
176
177/** surrogate pairs **/
178
179int xs_is_surrogate(unsigned int cpoint)
180/* checks if cpoint is the first element of a Unicode surrogate pair */
181{
182 return cpoint >= 0xd800 && cpoint <= 0xdfff;
183}
184
185
186unsigned int xs_surrogate_dec(unsigned int p1, unsigned int p2)
187/* "decodes" a surrogate pair into a codepoint */
188{
189 return 0x10000 | ((p1 & 0x3ff) << 10) | (p2 & 0x3ff);
190}
191
192
193unsigned int xs_surrogate_enc(unsigned int cpoint)
194/* "encodes" a Unicode into a surrogate pair (p1 in the MSB word) */
195{
196 unsigned int p1 = 0xd7c0 + (cpoint >> 10);
197 unsigned int p2 = 0xdc00 + (cpoint & 0x3ff);
198
199 return (p1 << 16) | p2;
200}
201
202
203int xs_utf8_len(const char *str)
204/* counts the number of characters (codepoints) in str */
205{
206 int len = 0;
207
208 while (xs_utf8_dec(&str))
209 len++;
210
211 return len;
212}
213
214
215#ifdef _XS_H
216
217xs_str *xs_utf8_insert(xs_str *str, unsigned int cpoint, int *offset)
218/* encodes an Unicode codepoint to utf-8 into str */
219{
220 char tmp[4];
221
222 int c = xs_utf8_enc(tmp, cpoint);
223
224 str = xs_insert_m(str, *offset, tmp, c);
225
226 *offset += c;
227
228 return str;
229}
230
231
232xs_str *xs_utf8_cat(xs_str *str, unsigned int cpoint)
233/* encodes an Unicode codepoint to utf-8 into str */
234{
235 int offset = strlen(str);
236
237 return xs_utf8_insert(str, cpoint, &offset);
238}
239
240
241xs_str *xs_utf8_crop_i(xs_str *str, int begin, int end)
242/* crops str from begin to end by characters (codepoints) */
243{
244 const char *p = str;
245 int sz = xs_utf8_len(str);
246 unsigned int cpoint;
247
248 if (end <= 0)
249 end = sz + end;
250
251 xs_str *ns = xs_str_new(NULL);
252
253 for (int n = 0; n < end && (cpoint = xs_utf8_dec(&p)); n++) {
254 if (n >= begin)
255 ns = xs_utf8_cat(ns, cpoint);
256 }
257
258 xs_free(str);
259
260 return ns;
261}
262
263
264#endif /* _XS_H */
265
266
267#ifdef _XS_UNICODE_TBL_H
268
269/* include xs_unicode_tbl.h before this one to use these functions */
270
271unsigned int *_xs_unicode_upper_search(unsigned int cpoint)
272/* searches for an uppercase codepoint in the case fold table */
273{
274 int b = 0;
275 int t = xs_countof(xs_unicode_case_fold_table) / 2 - 1;
276
277 while (t >= b) {
278 int n = (b + t) / 2;
279 unsigned int *p = &xs_unicode_case_fold_table[n * 2];
280
281 if (cpoint < p[0])
282 t = n - 1;
283 else
284 if (cpoint > p[0])
285 b = n + 1;
286 else
287 return p;
288 }
289
290 return NULL;
291}
292
293
294unsigned int *_xs_unicode_lower_search(unsigned int cpoint)
295/* searches for a lowercase codepoint in the case fold table */
296{
297 unsigned int *p = xs_unicode_case_fold_table;
298 unsigned int *e = p + xs_countof(xs_unicode_case_fold_table);
299
300 while (p < e) {
301 if (cpoint == p[1])
302 return p;
303
304 p += 2;
305 }
306
307 return NULL;
308}
309
310
311unsigned int xs_unicode_tolower(unsigned int cpoint)
312/* returns the cpoint to lowercase */
313{
314 if (cpoint < 0x80)
315 return tolower(cpoint);
316
317 unsigned int *p = _xs_unicode_upper_search(cpoint);
318
319 return p == NULL ? cpoint : p[1];
320}
321
322
323unsigned int xs_unicode_toupper(unsigned int cpoint)
324/* returns the cpoint to uppercase */
325{
326 if (cpoint < 0x80)
327 return toupper(cpoint);
328
329 unsigned int *p = _xs_unicode_lower_search(cpoint);
330
331 return p == NULL ? cpoint : p[0];
332}
333
334
335int xs_unicode_nfd(unsigned int cpoint, unsigned int *base, unsigned int *diac)
336/* applies unicode Normalization Form D */
337{
338 int b = 0;
339 int t = xs_countof(xs_unicode_nfd_table) / 3 - 1;
340
341 while (t >= b) {
342 int n = (b + t) / 2;
343 unsigned int *p = &xs_unicode_nfd_table[n * 3];
344
345 int c = cpoint - p[0];
346
347 if (c < 0)
348 t = n - 1;
349 else
350 if (c > 0)
351 b = n + 1;
352 else {
353 *base = p[1];
354 *diac = p[2];
355 return 1;
356 }
357 }
358
359 return 0;
360}
361
362
363int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint)
364/* applies unicode Normalization Form C */
365{
366 unsigned int *p = xs_unicode_nfd_table;
367 unsigned int *e = p + xs_countof(xs_unicode_nfd_table);
368
369 while (p < e) {
370 if (p[1] == base && p[2] == diac) {
371 *cpoint = p[0];
372 return 1;
373 }
374
375 p += 3;
376 }
377
378 return 0;
379}
380
381
382int xs_unicode_is_alpha(unsigned int cpoint)
383/* checks if a codepoint is an alpha (i.e. a letter) */
384{
385 int b = 0;
386 int t = xs_countof(xs_unicode_alpha_table) / 2 - 1;
387
388 while (t >= b) {
389 int n = (b + t) / 2;
390 unsigned int *p = &xs_unicode_alpha_table[n * 2];
391
392 if (cpoint < p[0])
393 t = n - 1;
394 else
395 if (cpoint > p[1])
396 b = n + 1;
397 else
398 return 1;
399 }
400
401 return 0;
402}
403
404
405int xs_unicode_is_right_to_left(unsigned int cpoint)
406/* checks if a codepoint is a right-to-left letter */
407{
408 int b = 0;
409 int t = xs_countof(xs_unicode_right_to_left_table) / 2 - 1;
410
411 while (t >= b) {
412 int n = (b + t) / 2;
413 unsigned int *p = &xs_unicode_right_to_left_table[n * 2];
414
415 if (cpoint < p[0])
416 t = n - 1;
417 else
418 if (cpoint > p[1])
419 b = n + 1;
420 else
421 return 1;
422 }
423
424 return 0;
425}
426
427
428#ifdef _XS_H
429
430xs_str *xs_utf8_toupper(const char *str)
431{
432 xs_str *s = xs_str_new(NULL);
433 unsigned int cpoint;
434 int offset = 0;
435
436 while ((cpoint = xs_utf8_dec(&str))) {
437 cpoint = xs_unicode_toupper(cpoint);
438 s = xs_utf8_insert(s, cpoint, &offset);
439 }
440
441 return s;
442}
443
444
445xs_str *xs_utf8_tolower(const char *str)
446{
447 xs_str *s = xs_str_new(NULL);
448 unsigned int cpoint;
449 int offset = 0;
450
451 while ((cpoint = xs_utf8_dec(&str))) {
452 cpoint = xs_unicode_tolower(cpoint);
453 s = xs_utf8_insert(s, cpoint, &offset);
454 }
455
456 return s;
457}
458
459
460xs_str *xs_utf8_to_nfd(const char *str)
461{
462 xs_str *s = xs_str_new(NULL);
463 unsigned int cpoint;
464 int offset = 0;
465
466 while ((cpoint = xs_utf8_dec(&str))) {
467 unsigned int base;
468 unsigned int diac;
469
470 if (xs_unicode_nfd(cpoint, &base, &diac)) {
471 s = xs_utf8_insert(s, base, &offset);
472 s = xs_utf8_insert(s, diac, &offset);
473 }
474 else
475 s = xs_utf8_insert(s, cpoint, &offset);
476 }
477
478 return s;
479}
480
481
482xs_str *xs_utf8_to_nfc(const char *str)
483{
484 xs_str *s = xs_str_new(NULL);
485 unsigned int cpoint;
486 unsigned int base = 0;
487 int offset = 0;
488
489 while ((cpoint = xs_utf8_dec(&str))) {
490 if (xs_is_diacritic(cpoint)) {
491 if (xs_unicode_nfc(base, cpoint, &base))
492 continue;
493 }
494
495 if (base)
496 s = xs_utf8_insert(s, base, &offset);
497
498 base = cpoint;
499 }
500
501 if (base)
502 s = xs_utf8_insert(s, base, &offset);
503
504 return s;
505}
506
507#endif /* _XS_H */
508
509#endif /* _XS_UNICODE_TBL_H */
510
511#endif /* XS_IMPLEMENTATION */
512
513#endif /* _XS_UNICODE_H */