1/* Output stream for CSS styled text, producing ANSI escape sequences. 2 Copyright (C) 2006-2007 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 <http://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 "hash.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. */ 53typedef 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 62struct term_styled_ostream : struct styled_ostream 63{ 64fields: 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 85static void 86term_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 98static void 99term_styled_ostream::flush (term_styled_ostream_t stream) 100{ 101 term_ostream_flush (stream->destination); 102} 103 104static void 105term_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 131enum 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 141typedef 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. */ 149static CRXStyle * 150crx_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. */ 170static void 171crx_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. */ 178static enum CRStatus 179crx_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 281static term_color_t 282style_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 313static term_weight_t 314style_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 370static term_posture_t 371style_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 393static term_underline_t 394style_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. */ 419static attributes_t * 420match (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. */ 516static void 517match_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 527static void 528term_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 565static void 566term_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/* Constructor. */ 599 600term_styled_ostream_t 601term_styled_ostream_create (int fd, const char *filename, 602 const char *css_filename) 603{ 604 term_styled_ostream_t stream = 605 XMALLOC (struct term_styled_ostream_representation); 606 CRStyleSheet *css_file_contents; 607 608 stream->base.base.vtable = &term_styled_ostream_vtable; 609 stream->destination = term_ostream_create (fd, filename); 610 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) 614 { 615 term_ostream_free (stream->destination); 616 free (stream); 617 return NULL; 618 } 619 stream->css_document = cr_cascade_new (NULL, css_file_contents, NULL); 620 stream->css_engine = cr_sel_eng_new (); 621 622 stream->curr_classes_allocated = 60; 623 stream->curr_classes = XNMALLOC (stream->curr_classes_allocated, char); 624 stream->curr_classes_length = 0; 625 626 hash_init (&stream->cache, 10); 627 628 match_and_cache (stream); 629 630 return stream; 631} 632