1 /* Output stream for CSS styled text, producing ANSI escape sequences.
2 Copyright (C) 2006-2007, 2015 Free Software Foundation, Inc.
3 Written by Bruno Haible <bruno@clisp.org>, 2006.
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.
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.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>. */
21 #include "term-styled-ostream.h"
25 #include <cr-om-parser.h>
26 #include <cr-sel-eng.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>
33 #include <cr-string.h>
35 #include "term-ostream.h"
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:
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. */
52 /* Attributes that can be set on a character. */
58 term_posture_t posture;
59 term_underline_t underline;
62 struct term_styled_ostream : struct styled_ostream
65 /* The destination stream. */
66 term_ostream_t destination;
67 /* The CSS document. */
68 CRCascade *css_document;
69 /* The CSS matching engine. */
71 /* The list of active XML elements, with a space before each.
72 For example, in above example, it is " table header". */
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
79 /* The current attributes. */
80 attributes_t *curr_attr;
83 /* Implementation of ostream_t methods. */
86 term_styled_ostream::write_mem (term_styled_ostream_t stream,
87 const void *data, size_t len)
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);
95 term_ostream_write_mem (stream->destination, data, len);
99 term_styled_ostream::flush (term_styled_ostream_t stream)
101 term_ostream_flush (stream->destination);
105 term_styled_ostream::free (term_styled_ostream_t stream)
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);
117 while (hash_iterate (&stream->cache, &ptr, &key, &keylen, &data) == 0)
122 hash_destroy (&stream->cache);
126 /* Implementation of styled_ostream_t methods. */
128 /* CRStyle doesn't contain a value for the 'text-decoration' property.
129 So we have to extend it. */
131 enum CRXTextDecorationType
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
141 typedef struct _CRXStyle
143 struct _CRXStyle *parent_style;
145 enum CRXTextDecorationType text_decoration;
148 /* An extended version of cr_style_new. */
150 crx_style_new (gboolean a_set_props_to_initial_values)
155 base = cr_style_new (a_set_props_to_initial_values);
159 result = XMALLOC (CRXStyle);
161 if (a_set_props_to_initial_values)
162 result->text_decoration = TEXT_DECORATION_NONE;
164 result->text_decoration = TEXT_DECORATION_INHERIT;
169 /* An extended version of cr_style_destroy. */
171 crx_style_destroy (CRXStyle *a_style)
173 cr_style_destroy (a_style->base);
177 /* An extended version of cr_sel_eng_get_matched_style. */
179 crx_sel_eng_get_matched_style (CRSelEng * a_this, CRCascade * a_cascade,
181 CRXStyle * a_parent_style, CRXStyle ** a_style,
182 gboolean a_set_props_to_initial_values)
184 enum CRStatus status;
185 CRPropList *props = NULL;
187 if (!(a_this && a_cascade && a_node && a_style))
188 return CR_BAD_PARAM_ERROR;
190 status = cr_sel_eng_get_matched_properties_from_cascade (a_this, a_cascade,
192 if (!(status == CR_OK))
201 *a_style = crx_style_new (a_set_props_to_initial_values);
207 if (a_set_props_to_initial_values)
209 cr_style_set_props_to_initial_values ((*a_style)->base);
210 (*a_style)->text_decoration = TEXT_DECORATION_NONE;
214 cr_style_set_props_to_default_values ((*a_style)->base);
215 (*a_style)->text_decoration = TEXT_DECORATION_INHERIT;
219 style->parent_style = a_parent_style;
220 style->base->parent_style =
221 (a_parent_style != NULL ? a_parent_style->base : NULL);
226 for (cur = props; cur != NULL; cur = cr_prop_list_get_next (cur))
228 CRDeclaration *decl = NULL;
230 cr_prop_list_get_decl (cur, &decl);
231 cr_style_set_style_from_decl (style->base, decl);
233 && decl->property != NULL
234 && decl->property->stryng != NULL
235 && decl->property->stryng->str != NULL)
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)
243 cr_string_peek_raw_str (decl->value->content.str);
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;
265 cr_prop_list_destroy (props);
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. */
282 style_compute_color_value (CRStyle *style, enum CRRgbProp which,
283 term_ostream_t stream)
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;
302 cr_rgb_copy (&rgb, &style->rgb_props[which].sv);
303 if (cr_rgb_compute_from_percentage (&rgb) != CR_OK)
306 g = rgb.green & 0xff;
308 return term_ostream_rgb_to_color (stream, r, g, b);
314 style_compute_font_weight_value (const CRStyle *style)
322 switch (style->font_weight)
324 case FONT_WEIGHT_INHERIT:
325 style = style->parent_style;
327 case FONT_WEIGHT_BOLDER:
329 style = style->parent_style;
331 case FONT_WEIGHT_LIGHTER:
333 style = style->parent_style;
335 case FONT_WEIGHT_100:
338 case FONT_WEIGHT_200:
341 case FONT_WEIGHT_300:
344 case FONT_WEIGHT_400: case FONT_WEIGHT_NORMAL:
347 case FONT_WEIGHT_500:
350 case FONT_WEIGHT_600:
353 case FONT_WEIGHT_700: case FONT_WEIGHT_BOLD:
356 case FONT_WEIGHT_800:
359 case FONT_WEIGHT_900:
365 /* Value >= 600 -> WEIGHT_BOLD. Value <= 500 -> WEIGHT_NORMAL. */
366 return (value >= 6 ? WEIGHT_BOLD : WEIGHT_NORMAL);
370 static term_posture_t
371 style_compute_font_posture_value (const CRStyle *style)
376 return POSTURE_DEFAULT;
377 switch (style->font_style)
379 case FONT_STYLE_INHERIT:
380 style = style->parent_style;
382 case FONT_STYLE_NORMAL:
383 return POSTURE_NORMAL;
384 case FONT_STYLE_ITALIC:
385 case FONT_STYLE_OBLIQUE:
386 return POSTURE_ITALIC;
393 static term_underline_t
394 style_compute_text_underline_value (const CRXStyle *style)
399 return UNDERLINE_DEFAULT;
400 switch (style->text_decoration)
402 case TEXT_DECORATION_INHERIT:
403 style = style->parent_style;
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:
418 /* Match the current list of CSS classes to the CSS and return the result. */
419 static attributes_t *
420 match (term_styled_ostream_t stream)
426 CRXStyle *curr_style;
427 CRStyle *curr_style_base;
430 /* Create a hierarchy of XML nodes. */
431 root = xmlNewNode (NULL, (const xmlChar *) "__root__");
432 root->type = XML_ELEMENT_NODE;
434 p_end = &stream->curr_classes[stream->curr_classes_length];
435 p_start = stream->curr_classes;
436 while (p_start < p_end)
441 if (!(*p_start == ' '))
444 for (p = p_start; p < p_end && *p != ' '; p++)
447 /* Temporarily replace the ' ' by '\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);
454 if (xmlAddChild (curr, child) == NULL)
455 /* Error! Shouldn't happen. */
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. */
467 for (curr = root; curr != NULL; curr = curr->children)
469 CRXStyle *parent_style = curr_style;
472 if (crx_sel_eng_get_matched_style (stream->css_engine,
473 stream->css_document,
475 parent_style, &curr_style,
478 if (curr_style == NULL)
479 /* No declarations matched this node. Inherit all values. */
480 curr_style = parent_style;
482 /* curr_style is a new style, inheriting from parent_style. */
485 curr_style_base = (curr_style != NULL ? curr_style->base : NULL);
487 /* Extract the CSS declarations that we can use. */
488 attr = XMALLOC (attributes_t);
490 style_compute_color_value (curr_style_base, RGB_PROP_COLOR,
491 stream->destination);
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);
499 /* Free the style chain. */
500 while (curr_style != NULL)
502 CRXStyle *parent_style = curr_style->parent_style;
504 crx_style_destroy (curr_style);
505 curr_style = parent_style;
508 /* Free the XML nodes. */
509 xmlFreeNodeList (root);
514 /* Match the current list of CSS classes to the CSS and store the result in
515 stream->curr_attr and in the cache. */
517 match_and_cache (term_styled_ostream_t stream)
519 attributes_t *attr = match (stream);
520 if (hash_insert_entry (&stream->cache,
521 stream->curr_classes, stream->curr_classes_length,
524 stream->curr_attr = attr;
528 term_styled_ostream::begin_use_class (term_styled_ostream_t stream,
529 const char *classname)
531 size_t classname_len;
535 if (classname[0] == '\0' || strchr (classname, ' ') != NULL)
536 /* Invalid classname argument. */
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)
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;
548 stream->curr_classes = xrealloc (stream->curr_classes, new_allocated);
549 stream->curr_classes_allocated = new_allocated;
551 p = &stream->curr_classes[stream->curr_classes_length];
553 memcpy (p, classname, classname_len);
554 stream->curr_classes_length += 1 + classname_len;
556 /* Uodate stream->curr_attr. */
557 if (hash_find_entry (&stream->cache,
558 stream->curr_classes, stream->curr_classes_length,
560 match_and_cache (stream);
562 stream->curr_attr = (attributes_t *) found;
566 term_styled_ostream::end_use_class (term_styled_ostream_t stream,
567 const char *classname)
574 if (stream->curr_classes_length == 0)
575 /* No matching call to begin_use_class. */
578 /* Remove the trailing classname. */
579 p_end = &stream->curr_classes[stream->curr_classes_length];
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. */
588 stream->curr_classes_length = p - stream->curr_classes;
590 /* Update stream->curr_attr. */
591 if (hash_find_entry (&stream->cache,
592 stream->curr_classes, stream->curr_classes_length,
595 stream->curr_attr = (attributes_t *) found;
600 term_styled_ostream_t
601 term_styled_ostream_create (int fd, const char *filename,
602 const char *css_filename)
604 term_styled_ostream_t stream =
605 XMALLOC (struct term_styled_ostream_representation);
606 CRStyleSheet *css_file_contents;
608 stream->base.base.vtable = &term_styled_ostream_vtable;
609 stream->destination = term_ostream_create (fd, filename);
611 if (cr_om_parser_simply_parse_file ((const guchar *) css_filename,
612 CR_UTF_8, /* CR_AUTO is not supported */
613 &css_file_contents) != CR_OK)
615 term_ostream_free (stream->destination);
619 stream->css_document = cr_cascade_new (NULL, css_file_contents, NULL);
620 stream->css_engine = cr_sel_eng_new ();
622 stream->curr_classes_allocated = 60;
623 stream->curr_classes = XNMALLOC (stream->curr_classes_allocated, char);
624 stream->curr_classes_length = 0;
626 hash_init (&stream->cache, 10);
628 match_and_cache (stream);