1/* Pango
2 * test-bidi.c: Test bidi apis
3 *
4 * Copyright (C) 2021 Red Hat, Inc.
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
15 *
16 * You should have received a copy of the GNU Library General Public
17 * License along with this library; if not, write to the
18 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19 * Boston, MA 02111-1307, USA.
20 */
21
22#include <locale.h>
23#include <pango/pango.h>
24#include <pango/pangocairo.h>
25
26static PangoContext *context;
27
28G_GNUC_BEGIN_IGNORE_DEPRECATIONS
29
30static void
31test_mirror_char (void)
32{
33 /* just some samples */
34 struct {
35 gunichar a;
36 gunichar b;
37 } tests[] = {
38 { '(', ')' },
39 { '<', '>' },
40 { '[', ']' },
41 { '{', '}' },
42 { 0x00ab, 0x00bb },
43 { 0x2045, 0x2046 },
44 { 0x226e, 0x226f },
45 };
46
47 for (int i = 0; i < G_N_ELEMENTS (tests); i++)
48 {
49 gboolean ret;
50 gunichar ch;
51
52 ret = pango_get_mirror_char (ch: tests[i].a, mirrored_ch: &ch);
53 g_assert_true (ret);
54 g_assert_true (ch == tests[i].b);
55 ret = pango_get_mirror_char (ch: tests[i].b, mirrored_ch: &ch);
56 g_assert_true (ret);
57 g_assert_true (ch == tests[i].a);
58 }
59}
60
61static void
62test_bidi_type_for_unichar (void)
63{
64 /* one representative from each class we support */
65 g_assert_true (pango_bidi_type_for_unichar ('a') == PANGO_BIDI_TYPE_L);
66 g_assert_true (pango_bidi_type_for_unichar (0x202a) == PANGO_BIDI_TYPE_LRE);
67 g_assert_true (pango_bidi_type_for_unichar (0x202d) == PANGO_BIDI_TYPE_LRO);
68 g_assert_true (pango_bidi_type_for_unichar (0x05d0) == PANGO_BIDI_TYPE_R);
69 g_assert_true (pango_bidi_type_for_unichar (0x0627) == PANGO_BIDI_TYPE_AL);
70 g_assert_true (pango_bidi_type_for_unichar (0x202b) == PANGO_BIDI_TYPE_RLE);
71 g_assert_true (pango_bidi_type_for_unichar (0x202e) == PANGO_BIDI_TYPE_RLO);
72 g_assert_true (pango_bidi_type_for_unichar (0x202c) == PANGO_BIDI_TYPE_PDF);
73 g_assert_true (pango_bidi_type_for_unichar ('0') == PANGO_BIDI_TYPE_EN);
74 g_assert_true (pango_bidi_type_for_unichar ('+') == PANGO_BIDI_TYPE_ES);
75 g_assert_true (pango_bidi_type_for_unichar ('#') == PANGO_BIDI_TYPE_ET);
76 g_assert_true (pango_bidi_type_for_unichar (0x601) == PANGO_BIDI_TYPE_AN);
77 g_assert_true (pango_bidi_type_for_unichar (',') == PANGO_BIDI_TYPE_CS);
78 g_assert_true (pango_bidi_type_for_unichar (0x0301) == PANGO_BIDI_TYPE_NSM);
79 g_assert_true (pango_bidi_type_for_unichar (0x200d) == PANGO_BIDI_TYPE_BN);
80 g_assert_true (pango_bidi_type_for_unichar (0x2029) == PANGO_BIDI_TYPE_B);
81 g_assert_true (pango_bidi_type_for_unichar (0x000b) == PANGO_BIDI_TYPE_S);
82 g_assert_true (pango_bidi_type_for_unichar (' ') == PANGO_BIDI_TYPE_WS);
83 g_assert_true (pango_bidi_type_for_unichar ('!') == PANGO_BIDI_TYPE_ON);
84 /* these are new */
85 g_assert_true (pango_bidi_type_for_unichar (0x2066) == PANGO_BIDI_TYPE_LRI);
86 g_assert_true (pango_bidi_type_for_unichar (0x2067) == PANGO_BIDI_TYPE_RLI);
87 g_assert_true (pango_bidi_type_for_unichar (0x2068) == PANGO_BIDI_TYPE_FSI);
88 g_assert_true (pango_bidi_type_for_unichar (0x2069) == PANGO_BIDI_TYPE_PDI);
89}
90
91static void
92test_unichar_direction (void)
93{
94 struct {
95 gunichar ch;
96 PangoDirection dir;
97 } tests[] = {
98 { 'a', PANGO_DIRECTION_LTR },
99 { '0', PANGO_DIRECTION_NEUTRAL },
100 { '.', PANGO_DIRECTION_NEUTRAL },
101 { '(', PANGO_DIRECTION_NEUTRAL },
102 { 0x05d0, PANGO_DIRECTION_RTL },
103 };
104
105 for (int i = 0; i < G_N_ELEMENTS (tests); i++)
106 {
107 g_assert_true (pango_unichar_direction (tests[i].ch) == tests[i].dir);
108 }
109}
110
111G_GNUC_END_IGNORE_DEPRECATIONS
112
113static void
114test_bidi_embedding_levels (void)
115{
116 /* Examples taken from https://www.w3.org/International/articles/inline-bidi-markup/uba-basics */
117 struct {
118 const char *text;
119 PangoDirection dir;
120 const char *levels;
121 PangoDirection out_dir;
122 } tests[] = {
123 { "bahrain مصر kuwait", PANGO_DIRECTION_LTR, "\0\0\0\0\0\0\0\0\1\1\1\0\0\0\0\0\0\0", PANGO_DIRECTION_LTR },
124 { "bahrain مصر kuwait", PANGO_DIRECTION_WEAK_LTR, "\0\0\0\0\0\0\0\0\1\1\1\0\0\0\0\0\0\0", PANGO_DIRECTION_LTR },
125 { "bahrain مصر kuwait", PANGO_DIRECTION_RTL, "\2\2\2\2\2\2\2\1\1\1\1\1\2\2\2\2\2\2", PANGO_DIRECTION_RTL },
126 { "bahrain مصر kuwait", PANGO_DIRECTION_WEAK_RTL, "\0\0\0\0\0\0\0\0\1\1\1\0\0\0\0\0\0\0", PANGO_DIRECTION_LTR },
127 { "The title is مفتاح معايير الويب in Arabic.", PANGO_DIRECTION_LTR, "\0\0\0\0\0\0\0\0\0\0\0\0\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\0\0\0", PANGO_DIRECTION_LTR },
128 { "The title is مفتاح معايير الويب, in Arabic.", PANGO_DIRECTION_LTR, "\0\0\0\0\0\0\0\0\0\0\0\0\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\0\0\0\0", PANGO_DIRECTION_LTR },
129 { "The title is مفتاح معايير الويب⁧!⁩ in Arabic.", PANGO_DIRECTION_LTR, "\0\0\0\0\0\0\0\0\0\0\0\0\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\1\0\0\0\0\0\0\0\0\0\0\0", PANGO_DIRECTION_LTR }, // FIXME
130 { "one two ثلاثة 1234 خمسة", PANGO_DIRECTION_LTR, "\0\0\0\0\0\0\0\0\1\1\1\1\1\1\2\2\2\2\1\1\1\1\1", PANGO_DIRECTION_LTR },
131 { "one two ثلاثة ١٢٣٤ خمسة", PANGO_DIRECTION_LTR, "\0\0\0\0\0\0\0\0\1\1\1\1\1\1\2\2\2\2\1\1\1\1\1", PANGO_DIRECTION_LTR },
132 { "abאב12cd", PANGO_DIRECTION_LTR, "\0\0\1\1\2\2\0\0" },
133 { "abאב‪xy‬cd", PANGO_DIRECTION_LTR, "\0\0\1\1\1\2\2\2\0\0" },
134
135 };
136
137 for (int i = 0; i < G_N_ELEMENTS (tests); i++)
138 {
139 const char *text = tests[i].text;
140 PangoDirection dir = tests[i].dir;
141 guint8 *levels;
142 gsize len;
143
144 levels = pango_log2vis_get_embedding_levels (text, length: -1, pbase_dir: &dir);
145
146 len = g_utf8_strlen (p: text, max: -1);
147
148 if (memcmp (s1: levels, s2: tests[i].levels, n: sizeof (guint8) * len) != 0)
149 {
150 for (int j = 0; j < len; j++)
151 g_print (format: "\\%d", levels[j]);
152 g_print (format: "\n");
153 }
154 g_assert_true (memcmp (levels, tests[i].levels, sizeof (guint8) * len) == 0);
155 g_assert_true (dir == tests[i].out_dir);
156
157 g_free (mem: levels);
158 }
159}
160
161/* Some basic tests for pango_layout_move_cursor_visually inside
162 * a single PangoLayoutLine:
163 * - check that we actually move the cursor in the right direction
164 * - check that we get through the line with at most n steps
165 * - check that we don't skip legitimate cursor positions
166 */
167static void
168test_move_cursor_line (void)
169{
170 const char *tests[] = {
171 "abc😂️def",
172 "abcאבגdef",
173 "אבabcב",
174 "aאב12b",
175 "pa­ra­graph", // soft hyphens
176 };
177 PangoLayout *layout;
178 gboolean fail = FALSE;
179
180 layout = pango_layout_new (context);
181
182 for (int i = 0; i < G_N_ELEMENTS (tests); i++)
183 {
184 const char *text;
185 gsize n_chars;
186 int index;
187 int start_index;
188 gboolean trailing;
189 PangoRectangle s_pos, old_s_pos;
190 PangoRectangle w_pos, old_w_pos;
191 PangoLayoutLine *line;
192 struct {
193 int direction;
194 gboolean strong;
195 } params[] = {
196 { 1, TRUE },
197 { 1, FALSE },
198 { -1, TRUE },
199 { -1, FALSE },
200 };
201 int *strong_cursor;
202 int *weak_cursor;
203 gboolean *met_cursor;
204 const PangoLogAttr *attrs;
205 int n_attrs;
206 int j;
207 const char *p;
208
209 pango_layout_set_text (layout, text: tests[i], length: -1);
210
211 text = pango_layout_get_text (layout);
212 line = pango_layout_get_line_readonly (layout, line: 0);
213
214 n_chars = g_utf8_strlen (p: text, max: -1);
215
216 attrs = pango_layout_get_log_attrs_readonly (layout, n_attrs: &n_attrs);
217 strong_cursor = g_new (int, n_attrs);
218 weak_cursor = g_new (int, n_attrs);
219 met_cursor = g_new (gboolean, n_attrs);
220 for (j = 0, p = text; j < n_attrs; j++, p = g_utf8_next_char (p))
221 {
222 if (attrs[j].is_cursor_position)
223 {
224 pango_layout_get_cursor_pos (layout, index_: p - text, strong_pos: &s_pos, weak_pos: &w_pos);
225 strong_cursor[j] = s_pos.x;
226 weak_cursor[j] = w_pos.x;
227 }
228 else
229 strong_cursor[j] = weak_cursor[j] = -1;
230 }
231
232 for (j = 0; j < G_N_ELEMENTS (params); j++)
233 {
234 gboolean ok;
235
236 if (g_test_verbose ())
237 g_print (format: "'%s' %s %s :\t",
238 text,
239 params[j].direction > 0 ? "->" : "<-",
240 params[j].strong ? "strong" : "weak");
241
242 if ((pango_layout_line_get_resolved_direction (line) == PANGO_DIRECTION_LTR) == (params[j].direction > 0))
243 start_index = 0;
244 else
245 start_index = strlen (s: text);
246
247 index = start_index;
248
249 memset (s: met_cursor, c: 0, n: sizeof (gboolean) * n_attrs);
250
251 pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &s_pos, weak_pos: &w_pos);
252 for (int l = 0; l < n_attrs; l++)
253 {
254 if ((params[j].strong && strong_cursor[l] == s_pos.x) ||
255 (!params[j].strong && weak_cursor[l] == w_pos.x))
256 met_cursor[l] = TRUE;
257 }
258
259 ok = TRUE;
260
261 for (int k = 0; k <= n_chars; k++)
262 {
263 old_s_pos = s_pos;
264 old_w_pos = w_pos;
265 pango_layout_move_cursor_visually (layout, strong: params[j].strong,
266 old_index: index, old_trailing: 0,
267 direction: params[j].direction,
268 new_index: &index, new_trailing: &trailing);
269
270 while (trailing--)
271 index = g_utf8_next_char (text + index) - text;
272
273 g_assert (index == -1 || index == G_MAXINT ||
274 (0 <= index && index <= strlen (tests[i])));
275
276 if (index == -1 || index == G_MAXINT)
277 break;
278
279 pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &s_pos, weak_pos: &w_pos);
280 for (int l = 0; l < n_attrs; l++)
281 {
282 if ((params[j].strong && strong_cursor[l] == s_pos.x) ||
283 (!params[j].strong && weak_cursor[l] == w_pos.x))
284 met_cursor[l] = TRUE;
285 }
286
287 if ((params[j].direction > 0 && params[j].strong && old_s_pos.x >= s_pos.x) ||
288 (params[j].direction < 0 && params[j].strong && old_s_pos.x <= s_pos.x) ||
289 (params[j].direction > 0 && !params[j].strong && old_w_pos.x >= w_pos.x) ||
290 (params[j].direction < 0 && !params[j].strong && old_w_pos.x <= w_pos.x))
291 {
292 if (g_test_verbose ())
293 g_print (format: "(wrong move)\t");
294 ok = FALSE;
295 break;
296 }
297 }
298
299 if (ok)
300 for (int l = 0; l < n_attrs; l++)
301 {
302 if (!(met_cursor[l] ||
303 (params[j].strong && strong_cursor[l] == -1) ||
304 (!params[j].strong && weak_cursor[l] == -1)))
305 {
306 if (g_test_verbose ())
307 g_print (format: "(missed cursor)\t");
308 ok = FALSE;
309 }
310 }
311
312 if (ok)
313 if (!(index >= strlen (s: text) || index <= 0))
314 {
315 if (g_test_verbose ())
316 g_print (format: "(got stuck)\t");
317 ok = FALSE;
318 }
319
320 if (g_test_verbose ())
321 g_print (format: "%s\n", ok ? "ok": "not ok");
322
323 fail = fail || !ok;
324 }
325
326 g_free (mem: strong_cursor);
327 g_free (mem: weak_cursor);
328 g_free (mem: met_cursor);
329 }
330
331 g_object_unref (object: layout);
332
333 if (fail)
334 g_test_fail ();
335}
336
337static void
338test_move_cursor_para (void)
339{
340 struct {
341 const char *text;
342 int width;
343 } tests[] = {
344 { "This paragraph should ac­tual­ly have multiple lines, unlike all the other wannabe äöü pa­ra­graph tests in this ugh test-case. Grow some lines!\n", 188 },
345 { "你好 Hello שלום Γειά σας", 40 },
346 { "你好 Hello שלום Γειά σας", 60 },
347 { "你好 Hello שלום Γειά σας", 80 },
348 { "line 1
line 2
line 3\nline 4\r\nline 5", -1 }, // various separators
349 { "some text, some more text,\n\n even more text", 60 },
350 { "long word", 40 },
351 { "זוהי השורה הראשונה" "\n" "זוהי השורה השנייה" "\n" "זוהי השורה השלישית" , 200 },
352 };
353 PangoLayout *layout;
354 PangoRectangle pos, old_pos;
355 int index;
356 int trailing;
357 const char *text;
358 int line_no;
359 PangoLayoutLine *line;
360 PangoRectangle ext;
361 PangoLayoutIter *iter;
362
363 layout = pango_layout_new (context);
364
365 for (int i = 0; i < G_N_ELEMENTS (tests); i++)
366 {
367 pango_layout_set_text (layout, text: tests[i].text, length: -1);
368 text = pango_layout_get_text (layout);
369 if (tests[i].width > 0)
370 pango_layout_set_width (layout, width: tests[i].width * PANGO_SCALE);
371 else
372 pango_layout_set_width (layout, width: -1);
373
374 index = 0;
375 pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &pos, NULL);
376
377 while (index < G_MAXINT)
378 {
379 old_pos = pos;
380
381 pango_layout_index_to_line_x (layout, index_: index, FALSE, line: &line_no, NULL);
382 line = pango_layout_get_line (layout, line: line_no);
383 iter = pango_layout_get_iter (layout);
384 while (pango_layout_iter_get_line (iter) != line)
385 pango_layout_iter_next_line (iter);
386 pango_layout_iter_get_line_extents (iter, NULL, logical_rect: &ext);
387 pango_layout_iter_free (iter);
388
389 pango_layout_move_cursor_visually (layout, TRUE,
390 old_index: index, old_trailing: 0,
391 direction: 1,
392 new_index: &index, new_trailing: &trailing);
393 while (trailing--)
394 index = g_utf8_next_char (text + index) - text;
395
396 g_assert (index == -1 || index == G_MAXINT ||
397 (0 <= index && index <= strlen (tests[i].text)));
398
399 if (index == -1 || index == G_MAXINT)
400 break;
401
402 if (index >= strlen (s: tests[i].text) - 1)
403 continue;
404
405 pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &pos, NULL);
406
407 // assert that we are either moving to the right
408 // or jumping to the next line
409 g_assert_true (pos.y >= ext.y + ext.height || pos.x > old_pos.x);
410 // no invisible cursors, please
411 g_assert_true (pos.height > 1024);
412 }
413
414 /* and now backwards */
415 index = strlen (s: text);
416 pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &pos, NULL);
417
418 while (index > -1)
419 {
420 old_pos = pos;
421
422 pango_layout_index_to_line_x (layout, index_: index, FALSE, line: &line_no, NULL);
423 line = pango_layout_get_line (layout, line: line_no);
424 iter = pango_layout_get_iter (layout);
425 while (pango_layout_iter_get_line (iter) != line)
426 pango_layout_iter_next_line (iter);
427 pango_layout_iter_get_line_extents (iter, NULL, logical_rect: &ext);
428 pango_layout_iter_free (iter);
429
430 pango_layout_move_cursor_visually (layout, TRUE,
431 old_index: index, old_trailing: 0,
432 direction: -1,
433 new_index: &index, new_trailing: &trailing);
434 while (trailing--)
435 index = g_utf8_next_char (text + index) - text;
436
437 g_assert (index == -1 || index == G_MAXINT ||
438 (0 <= index && index <= strlen (tests[i].text)));
439
440 if (index == -1 || index == G_MAXINT)
441 break;
442
443 pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &pos, NULL);
444
445 // assert that we are either moving to the left
446 // or jumping to the previous line
447 g_assert_true (pos.y < ext.y || pos.x < old_pos.x);
448 // no invisible cursors, please
449 g_assert_true (pos.height > 1024);
450 }
451 }
452
453 g_object_unref (object: layout);
454}
455
456static void
457test_sinhala_cursor (void)
458{
459 const char *text = "ර් á ";
460 PangoLayout *layout;
461 const char *p;
462 const PangoLogAttr *attrs;
463 int n, i;
464
465 layout = pango_layout_new (context);
466
467 pango_layout_set_text (layout, text, length: -1);
468
469 if (pango_layout_get_unknown_glyphs_count (layout) > 0)
470 {
471 g_object_unref (object: layout);
472 g_test_skip (msg: "missing Sinhala fonts");
473 return;
474 }
475
476 attrs = pango_layout_get_log_attrs_readonly (layout, n_attrs: &n);
477
478 for (i = 0, p = text; *p; i++, p = g_utf8_next_char (p))
479 {
480 int index = p - text;
481 PangoRectangle strong, weak;
482
483 if (!attrs[i].is_cursor_position)
484 continue;
485
486 g_assert_true (pango_layout_get_direction (layout, index) == PANGO_DIRECTION_LTR);
487
488 pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &strong, weak_pos: &weak);
489 g_assert_true (strong.x == weak.x);
490 g_assert_true (strong.width == weak.width);
491 }
492}
493
494int
495main (int argc, char *argv[])
496{
497 PangoFontMap *fontmap;
498
499 setlocale (LC_ALL, locale: "");
500
501 fontmap = pango_cairo_font_map_get_default ();
502 context = pango_font_map_create_context (fontmap);
503
504 g_test_init (argc: &argc, argv: &argv, NULL);
505
506 g_test_add_func (testpath: "/bidi/mirror-char", test_func: test_mirror_char);
507 g_test_add_func (testpath: "/bidi/type-for-unichar", test_func: test_bidi_type_for_unichar);
508 g_test_add_func (testpath: "/bidi/unichar-direction", test_func: test_unichar_direction);
509 g_test_add_func (testpath: "/bidi/embedding-levels", test_func: test_bidi_embedding_levels);
510 g_test_add_func (testpath: "/bidi/move-cursor-line", test_func: test_move_cursor_line);
511 g_test_add_func (testpath: "/bidi/move-cursor-para", test_func: test_move_cursor_para);
512 g_test_add_func (testpath: "/bidi/sinhala-cursor", test_func: test_sinhala_cursor);
513
514 return g_test_run ();
515}
516

source code of gtk/subprojects/pango/tests/test-bidi.c