1 /* Output stream for CSS styled text, producing ANSI escape sequences.
2 Copyright (C) 2006-2007, 2019-2020 Free Software Foundation, Inc.
3 Written by Bruno Haible <bruno@clisp.org>, 2006.
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>. */
17
18 #include <config.h>
19
20 /* Specification. */
21 #include "term-styled-ostream.h"
22
23 #include <stdlib.h>
24
25 #include <cr-om-parser.h>
26 #include <cr-sel-eng.h>
27 #include <cr-style.h>
28 #include <cr-rgb.h>
29 /* <cr-fonts.h> has a broken double-inclusion guard in libcroco-0.6.1. */
30 #ifndef __CR_FONTS_H__
31 # include <cr-fonts.h>
32 #endif
33 #include <cr-string.h>
34
35 #include "term-ostream.h"
36 #include "mem-hash-map.h"
37 #include "xalloc.h"
38
39
40 /* CSS matching works as follows:
41 Suppose we have an element inside class "header" inside class "table".
42 We pretend to have an XML tree that looks like this:
43
44 (root)
45 +----table
46 +----header
47
48 For each of these XML nodes, the CSS matching engine can report the
49 matching CSS declarations. We extract the CSS property values that
50 matter for terminal styling and cache them. */
51
52 /* Attributes that can be set on a character. */
53 typedef struct
54 {
55 term_color_t color;
56 term_color_t bgcolor;
57 term_weight_t weight;
58 term_posture_t posture;
59 term_underline_t underline;
60 } attributes_t;
61
62 struct term_styled_ostream : struct styled_ostream
63 {
64 fields:
65 /* The destination stream. */
66 term_ostream_t destination;
67 /* The CSS filename. */
68 char *css_filename;
69 /* The CSS document. */
70 CRCascade *css_document;
71 /* The CSS matching engine. */
72 CRSelEng *css_engine;
73 /* The list of active XML elements, with a space before each.
74 For example, in above example, it is " table header". */
75 char *curr_classes;
76 size_t curr_classes_length;
77 size_t curr_classes_allocated;
78 /* A hash table mapping a list of classes (as a string) to an
79 'attributes_t *'. */
80 hash_table cache;
81 /* The current attributes. */
82 attributes_t *curr_attr;
83 };
84
85 /* Implementation of ostream_t methods. */
86
87 static void
88 term_styled_ostream::write_mem (term_styled_ostream_t stream,
89 const void *data, size_t len)
90 {
91 term_ostream_set_color (stream->destination, stream->curr_attr->color);
92 term_ostream_set_bgcolor (stream->destination, stream->curr_attr->bgcolor);
93 term_ostream_set_weight (stream->destination, stream->curr_attr->weight);
94 term_ostream_set_posture (stream->destination, stream->curr_attr->posture);
95 term_ostream_set_underline (stream->destination, stream->curr_attr->underline);
96
97 term_ostream_write_mem (stream->destination, data, len);
98 }
99
100 static void
101 term_styled_ostream::flush (term_styled_ostream_t stream, ostream_flush_scope_t scope)
102 {
103 term_ostream_flush (stream->destination, scope);
104 }
105
106 static void
107 term_styled_ostream::free (term_styled_ostream_t stream)
108 {
109 free (stream->css_filename);
110 term_ostream_free (stream->destination);
111 cr_cascade_destroy (stream->css_document);
112 cr_sel_eng_destroy (stream->css_engine);
113 free (stream->curr_classes);
114 {
115 void *ptr = NULL;
116 const void *key;
117 size_t keylen;
118 void *data;
119
120 while (hash_iterate (&stream->cache, &ptr, &key, &keylen, &data) == 0)
121 {
122 free (data);
123 }
124 }
125 hash_destroy (&stream->cache);
126 free (stream);
127 }
128
129 /* Implementation of styled_ostream_t methods. */
130
131 /* CRStyle doesn't contain a value for the 'text-decoration' property.
132 So we have to extend it. */
133
134 enum CRXTextDecorationType
135 {
136 TEXT_DECORATION_NONE,
137 TEXT_DECORATION_UNDERLINE,
138 TEXT_DECORATION_OVERLINE,
139 TEXT_DECORATION_LINE_THROUGH,
140 TEXT_DECORATION_BLINK,
141 TEXT_DECORATION_INHERIT
142 };
143
144 typedef struct _CRXStyle
145 {
146 struct _CRXStyle *parent_style;
147 CRStyle *base;
148 enum CRXTextDecorationType text_decoration;
149 } CRXStyle;
150
151 /* An extended version of cr_style_new. */
152 static CRXStyle *
153 crx_style_new (gboolean a_set_props_to_initial_values)
154 {
155 CRStyle *base;
156 CRXStyle *result;
157
158 base = cr_style_new (a_set_props_to_initial_values);
159 if (base == NULL)
160 return NULL;
161
162 result = XMALLOC (CRXStyle);
163 result->base = base;
164 if (a_set_props_to_initial_values)
165 result->text_decoration = TEXT_DECORATION_NONE;
166 else
167 result->text_decoration = TEXT_DECORATION_INHERIT;
168
169 return result;
170 }
171
172 /* An extended version of cr_style_destroy. */
173 static void
174 crx_style_destroy (CRXStyle *a_style)
175 {
176 cr_style_destroy (a_style->base);
177 free (a_style);
178 }
179
180 /* An extended version of cr_sel_eng_get_matched_style. */
181 static enum CRStatus
182 crx_sel_eng_get_matched_style (CRSelEng * a_this, CRCascade * a_cascade,
183 xmlNode * a_node,
184 CRXStyle * a_parent_style, CRXStyle ** a_style,
185 gboolean a_set_props_to_initial_values)
186 {
187 enum CRStatus status;
188 CRPropList *props = NULL;
189
190 if (!(a_this && a_cascade && a_node && a_style))
191 return CR_BAD_PARAM_ERROR;
192
193 status = cr_sel_eng_get_matched_properties_from_cascade (a_this, a_cascade,
194 a_node, &props);
195 if (!(status == CR_OK))
196 return status;
197
198 if (props)
199 {
200 CRXStyle *style;
201
202 if (!*a_style)
203 {
204 *a_style = crx_style_new (a_set_props_to_initial_values);
205 if (!*a_style)
206 return CR_ERROR;
207 }
208 else
209 {
210 if (a_set_props_to_initial_values)
211 {
212 cr_style_set_props_to_initial_values ((*a_style)->base);
213 (*a_style)->text_decoration = TEXT_DECORATION_NONE;
214 }
215 else
216 {
217 cr_style_set_props_to_default_values ((*a_style)->base);
218 (*a_style)->text_decoration = TEXT_DECORATION_INHERIT;
219 }
220 }
221 style = *a_style;
222 style->parent_style = a_parent_style;
223 style->base->parent_style =
224 (a_parent_style != NULL ? a_parent_style->base : NULL);
225
226 {
227 CRPropList *cur;
228
229 for (cur = props; cur != NULL; cur = cr_prop_list_get_next (cur))
230 {
231 CRDeclaration *decl = NULL;
232
233 cr_prop_list_get_decl (cur, &decl);
234 cr_style_set_style_from_decl (style->base, decl);
235 if (decl != NULL
236 && decl->property != NULL
237 && decl->property->stryng != NULL
238 && decl->property->stryng->str != NULL)
239 {
240 if (strcmp (decl->property->stryng->str, "text-decoration") == 0
241 && decl->value != NULL
242 && decl->value->type == TERM_IDENT
243 && decl->value->content.str != NULL)
244 {
245 const char *value =
246 cr_string_peek_raw_str (decl->value->content.str);
247
248 if (value != NULL)
249 {
250 if (strcmp (value, "none") == 0)
251 style->text_decoration = TEXT_DECORATION_NONE;
252 else if (strcmp (value, "underline") == 0)
253 style->text_decoration = TEXT_DECORATION_UNDERLINE;
254 else if (strcmp (value, "overline") == 0)
255 style->text_decoration = TEXT_DECORATION_OVERLINE;
256 else if (strcmp (value, "line-through") == 0)
257 style->text_decoration = TEXT_DECORATION_LINE_THROUGH;
258 else if (strcmp (value, "blink") == 0)
259 style->text_decoration = TEXT_DECORATION_BLINK;
260 else if (strcmp (value, "inherit") == 0)
261 style->text_decoration = TEXT_DECORATION_INHERIT;
262 }
263 }
264 }
265 }
266 }
267
268 cr_prop_list_destroy (props);
269 }
270
271 return CR_OK;
272 }
273
274 /* According to the CSS2 spec, sections 6.1 and 6.2, we need to do a
275 propagation: specified values -> computed values -> actual values.
276 The computed values are necessary. libcroco does not compute them for us.
277 The function cr_style_resolve_inherited_properties is also not sufficient:
278 it handles only the case of inheritance, not the case of non-inheritance.
279 So we write style accessors that fetch the computed value, doing the
280 inheritance on the fly.
281 We then compute the actual values from the computed values; for colors,
282 this is done through the rgb_to_color method. */
283
284 static term_color_t
285 style_compute_color_value (CRStyle *style, enum CRRgbProp which,
286 term_ostream_t stream)
287 {
288 for (;;)
289 {
290 if (style == NULL)
291 return COLOR_DEFAULT;
292 if (cr_rgb_is_set_to_inherit (&style->rgb_props[which].sv))
293 style = style->parent_style;
294 else if (cr_rgb_is_set_to_transparent (&style->rgb_props[which].sv))
295 /* A transparent color occurs as default background color, set by
296 cr_style_set_props_to_default_values. */
297 return COLOR_DEFAULT;
298 else
299 {
300 CRRgb rgb;
301 int r;
302 int g;
303 int b;
304
305 cr_rgb_copy (&rgb, &style->rgb_props[which].sv);
306 if (cr_rgb_compute_from_percentage (&rgb) != CR_OK)
307 abort ();
308 r = rgb.red & 0xff;
309 g = rgb.green & 0xff;
310 b = rgb.blue & 0xff;
311 return term_ostream_rgb_to_color (stream, r, g, b);
312 }
313 }
314 }
315
316 static term_weight_t
317 style_compute_font_weight_value (const CRStyle *style)
318 {
319 int value = 0;
320 for (;;)
321 {
322 if (style == NULL)
323 value += 4;
324 else
325 switch (style->font_weight)
326 {
327 case FONT_WEIGHT_INHERIT:
328 style = style->parent_style;
329 continue;
330 case FONT_WEIGHT_BOLDER:
331 value += 1;
332 style = style->parent_style;
333 continue;
334 case FONT_WEIGHT_LIGHTER:
335 value -= 1;
336 style = style->parent_style;
337 continue;
338 case FONT_WEIGHT_100:
339 value += 1;
340 break;
341 case FONT_WEIGHT_200:
342 value += 2;
343 break;
344 case FONT_WEIGHT_300:
345 value += 3;
346 break;
347 case FONT_WEIGHT_400: case FONT_WEIGHT_NORMAL:
348 value += 4;
349 break;
350 case FONT_WEIGHT_500:
351 value += 5;
352 break;
353 case FONT_WEIGHT_600:
354 value += 6;
355 break;
356 case FONT_WEIGHT_700: case FONT_WEIGHT_BOLD:
357 value += 7;
358 break;
359 case FONT_WEIGHT_800:
360 value += 8;
361 break;
362 case FONT_WEIGHT_900:
363 value += 9;
364 break;
365 default:
366 abort ();
367 }
368 /* Value >= 600 -> WEIGHT_BOLD. Value <= 500 -> WEIGHT_NORMAL. */
369 return (value >= 6 ? WEIGHT_BOLD : WEIGHT_NORMAL);
370 }
371 }
372
373 static term_posture_t
374 style_compute_font_posture_value (const CRStyle *style)
375 {
376 for (;;)
377 {
378 if (style == NULL)
379 return POSTURE_DEFAULT;
380 switch (style->font_style)
381 {
382 case FONT_STYLE_INHERIT:
383 style = style->parent_style;
384 break;
385 case FONT_STYLE_NORMAL:
386 return POSTURE_NORMAL;
387 case FONT_STYLE_ITALIC:
388 case FONT_STYLE_OBLIQUE:
389 return POSTURE_ITALIC;
390 default:
391 abort ();
392 }
393 }
394 }
395
396 static term_underline_t
397 style_compute_text_underline_value (const CRXStyle *style)
398 {
399 for (;;)
400 {
401 if (style == NULL)
402 return UNDERLINE_DEFAULT;
403 switch (style->text_decoration)
404 {
405 case TEXT_DECORATION_INHERIT:
406 style = style->parent_style;
407 break;
408 case TEXT_DECORATION_NONE:
409 case TEXT_DECORATION_OVERLINE:
410 case TEXT_DECORATION_LINE_THROUGH:
411 case TEXT_DECORATION_BLINK:
412 return UNDERLINE_OFF;
413 case TEXT_DECORATION_UNDERLINE:
414 return UNDERLINE_ON;
415 default:
416 abort ();
417 }
418 }
419 }
420
421 /* Match the current list of CSS classes to the CSS and return the result. */
422 static attributes_t *
423 match (term_styled_ostream_t stream)
424 {
425 xmlNodePtr root;
426 xmlNodePtr curr;
427 char *p_end;
428 char *p_start;
429 CRXStyle *curr_style;
430 CRStyle *curr_style_base;
431 attributes_t *attr;
432
433 /* Create a hierarchy of XML nodes. */
434 root = xmlNewNode (NULL, (const xmlChar *) "__root__");
435 root->type = XML_ELEMENT_NODE;
436 curr = root;
437 p_end = &stream->curr_classes[stream->curr_classes_length];
438 p_start = stream->curr_classes;
439 while (p_start < p_end)
440 {
441 char *p;
442 xmlNodePtr child;
443
444 if (!(*p_start == ' '))
445 abort ();
446 p_start++;
447 for (p = p_start; p < p_end && *p != ' '; p++)
448 ;
449
450 /* Temporarily replace the ' ' by '\0'. */
451 *p = '\0';
452 child = xmlNewNode (NULL, (const xmlChar *) p_start);
453 child->type = XML_ELEMENT_NODE;
454 xmlSetProp (child, (const xmlChar *) "class", (const xmlChar *) p_start);
455 *p = ' ';
456
457 if (xmlAddChild (curr, child) == NULL)
458 /* Error! Shouldn't happen. */
459 abort ();
460
461 curr = child;
462 p_start = p;
463 }
464
465 /* Retrieve the matching CSS declarations. */
466 /* Not curr_style = crx_style_new (TRUE); because that assumes that the
467 default foreground color is black and that the default background color
468 is white, which is not necessarily true in a terminal context. */
469 curr_style = NULL;
470 for (curr = root; curr != NULL; curr = curr->children)
471 {
472 CRXStyle *parent_style = curr_style;
473 curr_style = NULL;
474
475 if (crx_sel_eng_get_matched_style (stream->css_engine,
476 stream->css_document,
477 curr,
478 parent_style, &curr_style,
479 FALSE) != CR_OK)
480 abort ();
481 if (curr_style == NULL)
482 /* No declarations matched this node. Inherit all values. */
483 curr_style = parent_style;
484 else
485 /* curr_style is a new style, inheriting from parent_style. */
486 ;
487 }
488 curr_style_base = (curr_style != NULL ? curr_style->base : NULL);
489
490 /* Extract the CSS declarations that we can use. */
491 attr = XMALLOC (attributes_t);
492 attr->color =
493 style_compute_color_value (curr_style_base, RGB_PROP_COLOR,
494 stream->destination);
495 attr->bgcolor =
496 style_compute_color_value (curr_style_base, RGB_PROP_BACKGROUND_COLOR,
497 stream->destination);
498 attr->weight = style_compute_font_weight_value (curr_style_base);
499 attr->posture = style_compute_font_posture_value (curr_style_base);
500 attr->underline = style_compute_text_underline_value (curr_style);
501
502 /* Free the style chain. */
503 while (curr_style != NULL)
504 {
505 CRXStyle *parent_style = curr_style->parent_style;
506
507 crx_style_destroy (curr_style);
508 curr_style = parent_style;
509 }
510
511 /* Free the XML nodes. */
512 xmlFreeNodeList (root);
513
514 return attr;
515 }
516
517 /* Match the current list of CSS classes to the CSS and store the result in
518 stream->curr_attr and in the cache. */
519 static void
520 match_and_cache (term_styled_ostream_t stream)
521 {
522 attributes_t *attr = match (stream);
523 if (hash_insert_entry (&stream->cache,
524 stream->curr_classes, stream->curr_classes_length,
525 attr) == NULL)
526 abort ();
527 stream->curr_attr = attr;
528 }
529
530 static void
531 term_styled_ostream::begin_use_class (term_styled_ostream_t stream,
532 const char *classname)
533 {
534 size_t classname_len;
535 char *p;
536 void *found;
537
538 if (classname[0] == '\0' || strchr (classname, ' ') != NULL)
539 /* Invalid classname argument. */
540 abort ();
541
542 /* Push the classname onto the classname list. */
543 classname_len = strlen (classname);
544 if (stream->curr_classes_length + 1 + classname_len + 1
545 > stream->curr_classes_allocated)
546 {
547 size_t new_allocated = stream->curr_classes_length + 1 + classname_len + 1;
548 if (new_allocated < 2 * stream->curr_classes_allocated)
549 new_allocated = 2 * stream->curr_classes_allocated;
550
551 stream->curr_classes = xrealloc (stream->curr_classes, new_allocated);
552 stream->curr_classes_allocated = new_allocated;
553 }
554 p = &stream->curr_classes[stream->curr_classes_length];
555 *p++ = ' ';
556 memcpy (p, classname, classname_len);
557 stream->curr_classes_length += 1 + classname_len;
558
559 /* Uodate stream->curr_attr. */
560 if (hash_find_entry (&stream->cache,
561 stream->curr_classes, stream->curr_classes_length,
562 &found) < 0)
563 match_and_cache (stream);
564 else
565 stream->curr_attr = (attributes_t *) found;
566 }
567
568 static void
569 term_styled_ostream::end_use_class (term_styled_ostream_t stream,
570 const char *classname)
571 {
572 char *p_end;
573 char *p_start;
574 char *p;
575 void *found;
576
577 if (stream->curr_classes_length == 0)
578 /* No matching call to begin_use_class. */
579 abort ();
580
581 /* Remove the trailing classname. */
582 p_end = &stream->curr_classes[stream->curr_classes_length];
583 p = p_end;
584 while (*--p != ' ')
585 ;
586 p_start = p + 1;
587 if (!(p_end - p_start == strlen (classname)
588 && memcmp (p_start, classname, p_end - p_start) == 0))
589 /* The match ing call to begin_use_class used a different classname. */
590 abort ();
591 stream->curr_classes_length = p - stream->curr_classes;
592
593 /* Update stream->curr_attr. */
594 if (hash_find_entry (&stream->cache,
595 stream->curr_classes, stream->curr_classes_length,
596 &found) < 0)
597 abort ();
598 stream->curr_attr = (attributes_t *) found;
599 }
600
601 static const char *
602 term_styled_ostream::get_hyperlink_ref (term_styled_ostream_t stream)
603 {
604 return term_ostream_get_hyperlink_ref (stream->destination);
605 }
606
607 static const char *
608 term_styled_ostream::get_hyperlink_id (term_styled_ostream_t stream)
609 {
610 return term_ostream_get_hyperlink_id (stream->destination);
611 }
612
613 static void
614 term_styled_ostream::set_hyperlink (term_styled_ostream_t stream,
615 const char *ref, const char *id)
616 {
617 term_ostream_set_hyperlink (stream->destination, ref, id);
618 }
619
620 static void
621 term_styled_ostream::flush_to_current_style (term_styled_ostream_t stream)
622 {
623 term_ostream_set_color (stream->destination, stream->curr_attr->color);
624 term_ostream_set_bgcolor (stream->destination, stream->curr_attr->bgcolor);
625 term_ostream_set_weight (stream->destination, stream->curr_attr->weight);
626 term_ostream_set_posture (stream->destination, stream->curr_attr->posture);
627 term_ostream_set_underline (stream->destination, stream->curr_attr->underline);
628
629 term_ostream_flush_to_current_style (stream->destination);
630 }
631
632 /* Constructor. */
633
634 term_styled_ostream_t
635 term_styled_ostream_create (int fd, const char *filename, ttyctl_t tty_control,
636 const char *css_filename)
637 {
638 term_styled_ostream_t stream;
639 CRStyleSheet *css_file_contents;
640
641 /* If css_filename is NULL, no styling is desired. The code below would end
642 up returning NULL anyway. But it's better to not rely on such details of
643 libcroco behaviour. */
644 if (css_filename == NULL)
645 return NULL;
646
647 stream = XMALLOC (struct term_styled_ostream_representation);
648
649 stream->base.base.vtable = &term_styled_ostream_vtable;
650 stream->destination = term_ostream_create (fd, filename, tty_control);
651 stream->css_filename = xstrdup (css_filename);
652
653 if (cr_om_parser_simply_parse_file ((const guchar *) css_filename,
654 CR_UTF_8, /* CR_AUTO is not supported */
655 &css_file_contents) != CR_OK)
656 {
657 free (stream->css_filename);
658 term_ostream_free (stream->destination);
659 free (stream);
660 return NULL;
661 }
662 stream->css_document = cr_cascade_new (NULL, css_file_contents, NULL);
663 stream->css_engine = cr_sel_eng_new ();
664
665 stream->curr_classes_allocated = 60;
666 stream->curr_classes = XNMALLOC (stream->curr_classes_allocated, char);
667 stream->curr_classes_length = 0;
668
669 hash_init (&stream->cache, 10);
670
671 match_and_cache (stream);
672
673 return stream;
674 }
675
676 /* Accessors. */
677
678 static term_ostream_t
679 term_styled_ostream::get_destination (term_styled_ostream_t stream)
680 {
681 return stream->destination;
682 }
683
684 static const char *
685 term_styled_ostream::get_css_filename (term_styled_ostream_t stream)
686 {
687 return stream->css_filename;
688 }
689
690 /* Instanceof test. */
691
692 bool
693 is_instance_of_term_styled_ostream (ostream_t stream)
694 {
695 return IS_INSTANCE (stream, ostream, term_styled_ostream);
696 }