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