1#include <errno.h>
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5
6#include "config.h"
7
8#include <gtk/gtk.h>
9#include <glib/gstdio.h>
10
11#ifdef HAVE_GIO_UNIX
12#include <gio/gunixoutputstream.h>
13#include <fcntl.h>
14#endif
15
16
17/* This is the guts of gtk_text_buffer_insert_markup,
18 * copied here so we can make an incremental version.
19 */
20static void
21insert_tags_for_attributes (GtkTextBuffer *buffer,
22 PangoAttrIterator *iter,
23 GtkTextIter *start,
24 GtkTextIter *end)
25{
26 GtkTextTagTable *table;
27 GSList *attrs, *l;
28 GtkTextTag *tag;
29 char name[256];
30 float fg_alpha, bg_alpha;
31
32 table = gtk_text_buffer_get_tag_table (buffer);
33
34#define LANGUAGE_ATTR(attr_name) \
35 { \
36 const char *language = pango_language_to_string (((PangoAttrLanguage*)attr)->value); \
37 g_snprintf (name, 256, "language=%s", language); \
38 tag = gtk_text_tag_table_lookup (table, name); \
39 if (!tag) \
40 { \
41 tag = gtk_text_tag_new (name); \
42 g_object_set (tag, #attr_name, language, NULL); \
43 gtk_text_tag_table_add (table, tag); \
44 g_object_unref (tag); \
45 } \
46 gtk_text_buffer_apply_tag (buffer, tag, start, end); \
47 }
48
49#define STRING_ATTR(attr_name) \
50 { \
51 const char *string = ((PangoAttrString*)attr)->value; \
52 g_snprintf (name, 256, #attr_name "=%s", string); \
53 tag = gtk_text_tag_table_lookup (table, name); \
54 if (!tag) \
55 { \
56 tag = gtk_text_tag_new (name); \
57 g_object_set (tag, #attr_name, string, NULL); \
58 gtk_text_tag_table_add (table, tag); \
59 g_object_unref (tag); \
60 } \
61 gtk_text_buffer_apply_tag (buffer, tag, start, end); \
62 }
63
64#define INT_ATTR(attr_name) \
65 { \
66 int value = ((PangoAttrInt*)attr)->value; \
67 g_snprintf (name, 256, #attr_name "=%d", value); \
68 tag = gtk_text_tag_table_lookup (table, name); \
69 if (!tag) \
70 { \
71 tag = gtk_text_tag_new (name); \
72 g_object_set (tag, #attr_name, value, NULL); \
73 gtk_text_tag_table_add (table, tag); \
74 g_object_unref (tag); \
75 } \
76 gtk_text_buffer_apply_tag (buffer, tag, start, end); \
77 }
78
79#define FONT_ATTR(attr_name) \
80 { \
81 PangoFontDescription *desc = ((PangoAttrFontDesc*)attr)->desc; \
82 char *str = pango_font_description_to_string (desc); \
83 g_snprintf (name, 256, "font-desc=%s", str); \
84 g_free (str); \
85 tag = gtk_text_tag_table_lookup (table, name); \
86 if (!tag) \
87 { \
88 tag = gtk_text_tag_new (name); \
89 g_object_set (tag, #attr_name, desc, NULL); \
90 gtk_text_tag_table_add (table, tag); \
91 g_object_unref (tag); \
92 } \
93 gtk_text_buffer_apply_tag (buffer, tag, start, end); \
94 }
95
96#define FLOAT_ATTR(attr_name) \
97 { \
98 float value = ((PangoAttrFloat*)attr)->value; \
99 g_snprintf (name, 256, #attr_name "=%g", value); \
100 tag = gtk_text_tag_table_lookup (table, name); \
101 if (!tag) \
102 { \
103 tag = gtk_text_tag_new (name); \
104 g_object_set (tag, #attr_name, value, NULL); \
105 gtk_text_tag_table_add (table, tag); \
106 g_object_unref (tag); \
107 } \
108 gtk_text_buffer_apply_tag (buffer, tag, start, end); \
109 }
110
111#define RGBA_ATTR(attr_name, alpha_value) \
112 { \
113 PangoColor *color; \
114 GdkRGBA rgba; \
115 color = &((PangoAttrColor*)attr)->color; \
116 rgba.red = color->red / 65535.; \
117 rgba.green = color->green / 65535.; \
118 rgba.blue = color->blue / 65535.; \
119 rgba.alpha = alpha_value; \
120 char *str = gdk_rgba_to_string (&rgba); \
121 g_snprintf (name, 256, #attr_name "=%s", str); \
122 g_free (str); \
123 tag = gtk_text_tag_table_lookup (table, name); \
124 if (!tag) \
125 { \
126 tag = gtk_text_tag_new (name); \
127 g_object_set (tag, #attr_name, &rgba, NULL); \
128 gtk_text_tag_table_add (table, tag); \
129 g_object_unref (tag); \
130 } \
131 gtk_text_buffer_apply_tag (buffer, tag, start, end); \
132 }
133
134#define VOID_ATTR(attr_name) \
135 { \
136 tag = gtk_text_tag_table_lookup (table, #attr_name); \
137 if (!tag) \
138 { \
139 tag = gtk_text_tag_new (#attr_name); \
140 g_object_set (tag, #attr_name, TRUE, NULL); \
141 gtk_text_tag_table_add (table, tag); \
142 g_object_unref (tag); \
143 } \
144 gtk_text_buffer_apply_tag (buffer, tag, start, end); \
145 }
146
147 fg_alpha = bg_alpha = 1.;
148
149 attrs = pango_attr_iterator_get_attrs (iterator: iter);
150 for (l = attrs; l; l = l->next)
151 {
152 PangoAttribute *attr = l->data;
153
154 switch ((int)attr->klass->type)
155 {
156 case PANGO_ATTR_FOREGROUND_ALPHA:
157 fg_alpha = ((PangoAttrInt*)attr)->value / 65535.;
158 break;
159
160 case PANGO_ATTR_BACKGROUND_ALPHA:
161 bg_alpha = ((PangoAttrInt*)attr)->value / 65535.;
162 break;
163
164 default:
165 break;
166 }
167 }
168
169 for (l = attrs; l; l = l->next)
170 {
171 PangoAttribute *attr = l->data;
172
173 switch (attr->klass->type)
174 {
175 case PANGO_ATTR_LANGUAGE:
176 LANGUAGE_ATTR (language);
177 break;
178
179 case PANGO_ATTR_FAMILY:
180 STRING_ATTR (family);
181 break;
182
183 case PANGO_ATTR_STYLE:
184 INT_ATTR (style);
185 break;
186
187 case PANGO_ATTR_WEIGHT:
188 INT_ATTR (weight);
189 break;
190
191 case PANGO_ATTR_VARIANT:
192 INT_ATTR (variant);
193 break;
194
195 case PANGO_ATTR_STRETCH:
196 INT_ATTR (stretch);
197 break;
198
199 case PANGO_ATTR_SIZE:
200 INT_ATTR (size);
201 break;
202
203 case PANGO_ATTR_FONT_DESC:
204 FONT_ATTR (font-desc);
205 break;
206
207 case PANGO_ATTR_FOREGROUND:
208 RGBA_ATTR (foreground_rgba, fg_alpha);
209 break;
210
211 case PANGO_ATTR_BACKGROUND:
212 RGBA_ATTR (background_rgba, bg_alpha);
213 break;
214
215 case PANGO_ATTR_UNDERLINE:
216 INT_ATTR (underline);
217 break;
218
219 case PANGO_ATTR_UNDERLINE_COLOR:
220 RGBA_ATTR (underline_rgba, fg_alpha);
221 break;
222
223 case PANGO_ATTR_OVERLINE:
224 INT_ATTR (overline);
225 break;
226
227 case PANGO_ATTR_OVERLINE_COLOR:
228 RGBA_ATTR (overline_rgba, fg_alpha);
229 break;
230
231 case PANGO_ATTR_STRIKETHROUGH:
232 INT_ATTR (strikethrough);
233 break;
234
235 case PANGO_ATTR_STRIKETHROUGH_COLOR:
236 RGBA_ATTR (strikethrough_rgba, fg_alpha);
237 break;
238
239 case PANGO_ATTR_RISE:
240 INT_ATTR (rise);
241 break;
242
243 case PANGO_ATTR_SCALE:
244 FLOAT_ATTR (scale);
245 break;
246
247 case PANGO_ATTR_FALLBACK:
248 INT_ATTR (fallback);
249 break;
250
251 case PANGO_ATTR_LETTER_SPACING:
252 INT_ATTR (letter_spacing);
253 break;
254
255 case PANGO_ATTR_FONT_FEATURES:
256 STRING_ATTR (font_features);
257 break;
258
259 case PANGO_ATTR_ALLOW_BREAKS:
260 INT_ATTR (allow_breaks);
261 break;
262
263 case PANGO_ATTR_SHOW:
264 INT_ATTR (show_spaces);
265 break;
266
267 case PANGO_ATTR_INSERT_HYPHENS:
268 INT_ATTR (insert_hyphens);
269 break;
270
271 case PANGO_ATTR_LINE_HEIGHT:
272 FLOAT_ATTR (line_height);
273 break;
274
275 case PANGO_ATTR_ABSOLUTE_LINE_HEIGHT:
276 break;
277
278 case PANGO_ATTR_WORD:
279 VOID_ATTR (word);
280 break;
281
282 case PANGO_ATTR_SENTENCE:
283 VOID_ATTR (sentence);
284 break;
285
286 case PANGO_ATTR_BASELINE_SHIFT:
287 INT_ATTR (baseline_shift);
288 break;
289
290 case PANGO_ATTR_FONT_SCALE:
291 INT_ATTR (font_scale);
292 break;
293
294 case PANGO_ATTR_SHAPE:
295 case PANGO_ATTR_ABSOLUTE_SIZE:
296 case PANGO_ATTR_GRAVITY:
297 case PANGO_ATTR_GRAVITY_HINT:
298 case PANGO_ATTR_FOREGROUND_ALPHA:
299 case PANGO_ATTR_BACKGROUND_ALPHA:
300 break;
301
302 case PANGO_ATTR_TEXT_TRANSFORM:
303 INT_ATTR (text_transform);
304 break;
305
306 case PANGO_ATTR_INVALID:
307 default:
308 g_assert_not_reached ();
309 break;
310 }
311 }
312
313 g_slist_free_full (list: attrs, free_func: (GDestroyNotify)pango_attribute_destroy);
314
315#undef LANGUAGE_ATTR
316#undef STRING_ATTR
317#undef INT_ATTR
318#undef FONT_ATTR
319#undef FLOAT_ATTR
320#undef RGBA_ATTR
321}
322
323typedef struct
324{
325 GMarkupParseContext *parser;
326 char *markup;
327 gsize pos;
328 gsize len;
329 GtkTextBuffer *buffer;
330 GtkTextIter iter;
331 GtkTextMark *mark;
332 PangoAttrList *attributes;
333 char *text;
334 PangoAttrIterator *attr;
335} MarkupData;
336
337static void
338free_markup_data (MarkupData *mdata)
339{
340 g_free (mem: mdata->markup);
341 g_clear_pointer (&mdata->parser, g_markup_parse_context_free);
342 gtk_text_buffer_delete_mark (buffer: mdata->buffer, mark: mdata->mark);
343 g_clear_pointer (&mdata->attr, pango_attr_iterator_destroy);
344 g_clear_pointer (&mdata->attributes, pango_attr_list_unref);
345 g_free (mem: mdata->text);
346 g_object_unref (object: mdata->buffer);
347 g_free (mem: mdata);
348}
349
350static gboolean
351insert_markup_idle (gpointer data)
352{
353 MarkupData *mdata = data;
354 gint64 begin;
355
356 begin = g_get_monotonic_time ();
357
358 do
359 {
360 int start, end;
361 int start_offset;
362 GtkTextIter start_iter;
363
364 if (g_get_monotonic_time () - begin > G_TIME_SPAN_MILLISECOND)
365 {
366 g_idle_add (function: insert_markup_idle, data);
367 return G_SOURCE_REMOVE;
368 }
369
370 pango_attr_iterator_range (iterator: mdata->attr, start: &start, end: &end);
371
372 if (end == G_MAXINT) /* last chunk */
373 end = start - 1; /* resulting in -1 to be passed to _insert */
374
375 start_offset = gtk_text_iter_get_offset (iter: &mdata->iter);
376 gtk_text_buffer_insert (buffer: mdata->buffer, iter: &mdata->iter, text: mdata->text + start, len: end - start);
377 gtk_text_buffer_get_iter_at_offset (buffer: mdata->buffer, iter: &start_iter, char_offset: start_offset);
378
379 insert_tags_for_attributes (buffer: mdata->buffer, iter: mdata->attr, start: &start_iter, end: &mdata->iter);
380
381 gtk_text_buffer_get_iter_at_mark (buffer: mdata->buffer, iter: &mdata->iter, mark: mdata->mark);
382 }
383 while (pango_attr_iterator_next (iterator: mdata->attr));
384
385 free_markup_data (mdata);
386 return G_SOURCE_REMOVE;
387}
388
389static gboolean
390parse_markup_idle (gpointer data)
391{
392 MarkupData *mdata = data;
393 gint64 begin;
394 GError *error = NULL;
395
396 begin = g_get_monotonic_time ();
397
398 do {
399 if (g_get_monotonic_time () - begin > G_TIME_SPAN_MILLISECOND)
400 {
401 g_idle_add (function: parse_markup_idle, data);
402 return G_SOURCE_REMOVE;
403 }
404
405 if (!g_markup_parse_context_parse (context: mdata->parser,
406 text: mdata->markup + mdata->pos,
407 MIN (4096, mdata->len - mdata->pos),
408 error: &error))
409 {
410 g_warning ("Invalid markup string: %s", error->message);
411 g_error_free (error);
412 free_markup_data (mdata);
413 return G_SOURCE_REMOVE;
414 }
415
416 mdata->pos += 4096;
417 } while (mdata->pos < mdata->len);
418
419 if (!pango_markup_parser_finish (context: mdata->parser,
420 attr_list: &mdata->attributes,
421 text: &mdata->text,
422 NULL,
423 error: &error))
424 {
425 g_warning ("Invalid markup string: %s", error->message);
426 g_error_free (error);
427 free_markup_data (mdata);
428 return G_SOURCE_REMOVE;
429 }
430
431 if (!mdata->attributes)
432 {
433 gtk_text_buffer_insert (buffer: mdata->buffer, iter: &mdata->iter, text: mdata->text, len: -1);
434 free_markup_data (mdata);
435 return G_SOURCE_REMOVE;
436 }
437
438 mdata->attr = pango_attr_list_get_iterator (list: mdata->attributes);
439 insert_markup_idle (data);
440
441 return G_SOURCE_REMOVE;
442}
443
444/* Takes a ref on @buffer while it is operating,
445 * and consumes @markup.
446 */
447static void
448insert_markup (GtkTextBuffer *buffer,
449 GtkTextIter *iter,
450 char *markup,
451 int len)
452{
453 MarkupData *data;
454
455 g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
456
457 data = g_new0 (MarkupData, 1);
458
459 data->buffer = g_object_ref (buffer);
460 data->iter = *iter;
461 data->markup = markup;
462 data->len = len;
463
464 data->parser = pango_markup_parser_new (accel_marker: 0);
465 data->pos = 0;
466
467 /* create mark with right gravity */
468 data->mark = gtk_text_buffer_create_mark (buffer, NULL, where: iter, FALSE);
469
470 parse_markup_idle (data);
471}
472
473static void
474fontify_finish (GObject *source,
475 GAsyncResult *result,
476 gpointer data)
477{
478 GSubprocess *subprocess = G_SUBPROCESS (source);
479 GtkTextBuffer *buffer = data;
480 GBytes *stdout_buf = NULL;
481 GBytes *stderr_buf = NULL;
482 GError *error = NULL;
483
484 if (!g_subprocess_communicate_finish (subprocess,
485 result,
486 stdout_buf: &stdout_buf,
487 stderr_buf: &stderr_buf,
488 error: &error))
489 {
490 g_clear_pointer (&stdout_buf, g_bytes_unref);
491 g_clear_pointer (&stderr_buf, g_bytes_unref);
492
493 g_warning ("%s", error->message);
494 g_clear_error (err: &error);
495
496 g_object_unref (object: subprocess);
497 g_object_unref (object: buffer);
498 return;
499 }
500
501 if (g_subprocess_get_exit_status (subprocess) != 0)
502 {
503 if (stderr_buf)
504 g_warning ("%s", (char *)g_bytes_get_data (stderr_buf, NULL));
505 g_clear_pointer (&stderr_buf, g_bytes_unref);
506 }
507
508 g_object_unref (object: subprocess);
509
510 g_clear_pointer (&stderr_buf, g_bytes_unref);
511
512 if (stdout_buf)
513 {
514 char *markup;
515 gsize len;
516 char *p;
517 GtkTextIter start;
518
519 gtk_text_buffer_set_text (buffer, text: "", len: 0);
520
521 /* highlight puts a span with font and size around its output,
522 * which we don't want.
523 */
524 markup = g_bytes_unref_to_data (bytes: stdout_buf, size: &len);
525 for (p = markup + strlen (s: "<span "); *p != '>'; p++) *p = ' ';
526
527 gtk_text_buffer_get_start_iter (buffer, iter: &start);
528 insert_markup (buffer, iter: &start, markup, len);
529 }
530
531 g_object_unref (object: buffer);
532}
533
534void
535fontify (const char *format,
536 GtkTextBuffer *source_buffer)
537{
538 GSubprocess *subprocess;
539 char *format_arg;
540 GtkSettings *settings;
541 char *theme;
542 gboolean prefer_dark;
543 const char *style_arg;
544 char *text;
545 GtkTextIter start, end;
546 GBytes *bytes;
547 GError *error = NULL;
548
549 settings = gtk_settings_get_default ();
550 g_object_get (object: settings,
551 first_property_name: "gtk-theme-name", &theme,
552 "gtk-application-prefer-dark-theme", &prefer_dark,
553 NULL);
554
555 if (prefer_dark || strcmp (s1: theme, s2: "HighContrastInverse") == 0)
556 style_arg = "--style=edit-vim-dark";
557 else
558 style_arg = "--style=edit-kwrite";
559
560 g_free (mem: theme);
561
562 format_arg = g_strconcat (string1: "--syntax=", format, NULL);
563 subprocess = g_subprocess_new (flags: G_SUBPROCESS_FLAGS_STDIN_PIPE |
564 G_SUBPROCESS_FLAGS_STDOUT_PIPE |
565 G_SUBPROCESS_FLAGS_STDERR_PIPE,
566 error: &error,
567 argv0: "highlight",
568 format_arg,
569 "--out-format=pango",
570 style_arg,
571 NULL);
572 g_free (mem: format_arg);
573
574 if (!subprocess)
575 {
576 if (g_error_matches (error, G_SPAWN_ERROR, code: G_SPAWN_ERROR_NOENT))
577 {
578 static gboolean warned = FALSE;
579
580 if (!warned)
581 {
582 warned = TRUE;
583 g_message ("For syntax highlighting, install the “highlight” program");
584 }
585 }
586 else
587 g_warning ("%s", error->message);
588
589 g_clear_error (err: &error);
590
591 return;
592 }
593
594 gtk_text_buffer_get_bounds (buffer: source_buffer, start: &start, end: &end);
595 text = gtk_text_buffer_get_text (buffer: source_buffer, start: &start, end: &end, TRUE);
596 bytes = g_bytes_new_take (data: text, size: strlen (s: text));
597
598#ifdef HAVE_GIO_UNIX
599 /* Work around https://gitlab.gnome.org/GNOME/glib/-/issues/2182 */
600 if (G_IS_UNIX_OUTPUT_STREAM (g_subprocess_get_stdin_pipe (subprocess)))
601 {
602 GOutputStream *stdin_pipe = g_subprocess_get_stdin_pipe (subprocess);
603 int fd = g_unix_output_stream_get_fd (G_UNIX_OUTPUT_STREAM (stdin_pipe));
604 fcntl (fd: fd, F_SETFL, O_NONBLOCK);
605 }
606#endif
607
608 g_subprocess_communicate_async (subprocess,
609 stdin_buf: bytes,
610 NULL,
611 callback: fontify_finish,
612 g_object_ref (source_buffer));
613 g_bytes_unref (bytes);
614}
615

source code of gtk/demos/gtk-demo/fontify.c