1 // Written in the D programming language. 2 3 /** 4 This module contains implementation of CSS support - Cascading Style Sheets. 5 6 Port of CoolReader Engine written in C++. 7 8 Supports subset of CSS standards. 9 10 11 Synopsis: 12 13 ---- 14 import dlangui.core.css; 15 16 ---- 17 18 Copyright: Vadim Lopatin, 2015 19 License: Boost License 1.0 20 Authors: Vadim Lopatin, coolreader.org@gmail.com 21 */ 22 module dlangui.core.css; 23 24 import std.traits; 25 import std.conv : to; 26 import std.string; 27 import std.array : empty; 28 import std.algorithm : equal; 29 import std.ascii : isAlpha; 30 31 import dlangui.core.dom; 32 33 /// display property values 34 enum CssDisplay : ubyte { 35 inherit, 36 inline, 37 block, 38 list_item, 39 run_in, 40 compact, 41 marker, 42 table, 43 inline_table, 44 table_row_group, 45 table_header_group, 46 table_footer_group, 47 table_row, 48 table_column_group, 49 table_column, 50 table_cell, 51 table_caption, 52 none 53 } 54 55 /// white-space property values 56 enum CssWhiteSpace : ubyte { 57 inherit, 58 normal, 59 pre, 60 nowrap 61 } 62 63 /// text-align property values 64 enum CssTextAlign : ubyte { 65 inherit, 66 left, 67 right, 68 center, 69 justify 70 } 71 72 /// vertical-align property values 73 enum CssVerticalAlign : ubyte { 74 inherit, 75 baseline, 76 sub, 77 super_, 78 top, 79 text_top, 80 middle, 81 bottom, 82 text_bottom 83 } 84 85 /// text-decoration property values 86 enum CssTextDecoration : ubyte { 87 // TODO: support multiple flags 88 inherit = 0, 89 none = 1, 90 underline = 2, 91 overline = 3, 92 line_through = 4, 93 blink = 5 94 } 95 96 /// hyphenate property values 97 enum CssHyphenate : ubyte { 98 inherit = 0, 99 none = 1, 100 auto_ = 2 101 } 102 103 /// font-style property values 104 enum CssFontStyle : ubyte { 105 inherit, 106 normal, 107 italic, 108 oblique 109 } 110 111 /// font-weight property values 112 enum CssFontWeight : ubyte { 113 inherit, 114 normal, 115 bold, 116 bolder, 117 lighter, 118 fw_100, 119 fw_200, 120 fw_300, 121 fw_400, 122 fw_500, 123 fw_600, 124 fw_700, 125 fw_800, 126 fw_900 127 } 128 129 /// font-family property values 130 enum CssFontFamily : ubyte { 131 inherit, 132 serif, 133 sans_serif, 134 cursive, 135 fantasy, 136 monospace 137 } 138 139 /// page split property values 140 enum CssPageBreak : ubyte { 141 inherit, 142 auto_, 143 always, 144 avoid, 145 left, 146 right 147 } 148 149 /// list-style-type property values 150 enum CssListStyleType : ubyte { 151 inherit, 152 disc, 153 circle, 154 square, 155 decimal, 156 lower_roman, 157 upper_roman, 158 lower_alpha, 159 upper_alpha, 160 none 161 } 162 163 /// list-style-position property values 164 enum CssListStylePosition : ubyte { 165 inherit, 166 inside, 167 outside 168 } 169 170 /// css length value types 171 enum CssValueType : ubyte { 172 inherited, 173 unspecified, 174 px, 175 em, 176 ex, 177 in_, // 2.54 cm 178 cm, 179 mm, 180 pt, // 1/72 in 181 pc, // 12 pt 182 percent, 183 color 184 } 185 186 /// css length value 187 struct CssValue { 188 189 int value = 0; ///< value (*256 for all types except % and px) 190 CssValueType type = CssValueType.px; ///< type of value 191 192 this(int px_value ) { 193 value = px_value; 194 } 195 this(CssValueType n_type, int n_value) { 196 type = n_type; 197 value = n_value; 198 } 199 bool opEqual(CssValue v) const 200 { 201 return type == v.type 202 && value == v.value; 203 } 204 205 static const CssValue inherited = CssValue(CssValueType.inherited, 0); 206 } 207 208 enum CssDeclType : ubyte { 209 unknown, 210 display, 211 white_space, 212 text_align, 213 text_align_last, 214 text_decoration, 215 hyphenate, // hyphenate 216 _webkit_hyphens, // -webkit-hyphens 217 adobe_hyphenate, // adobe-hyphenate 218 adobe_text_layout, // adobe-text-layout 219 color, 220 background_color, 221 vertical_align, 222 font_family, // id families like serif, sans-serif 223 //font_names, // string font name like Arial, Courier 224 font_size, 225 font_style, 226 font_weight, 227 text_indent, 228 line_height, 229 letter_spacing, 230 width, 231 height, 232 margin_left, 233 margin_right, 234 margin_top, 235 margin_bottom, 236 margin, 237 padding_left, 238 padding_right, 239 padding_top, 240 padding_bottom, 241 padding, 242 page_break_before, 243 page_break_after, 244 page_break_inside, 245 list_style, 246 list_style_type, 247 list_style_position, 248 list_style_image 249 } 250 251 class CssStyle { 252 CssDisplay display = CssDisplay.block; 253 CssWhiteSpace whiteSpace = CssWhiteSpace.inherit; 254 CssTextAlign textAlign = CssTextAlign.inherit; 255 CssTextAlign textAlignLast = CssTextAlign.inherit; 256 CssTextDecoration textDecoration = CssTextDecoration.inherit; 257 CssHyphenate hyphenate = CssHyphenate.inherit; 258 CssVerticalAlign verticalAlign = CssVerticalAlign.inherit; 259 CssFontFamily fontFamily = CssFontFamily.inherit; 260 CssFontStyle fontStyle = CssFontStyle.inherit; 261 CssPageBreak pageBreakBefore = CssPageBreak.inherit; 262 CssPageBreak pageBreakInside = CssPageBreak.inherit; 263 CssPageBreak pageBreakAfter = CssPageBreak.inherit; 264 CssListStyleType listStyleType = CssListStyleType.inherit; 265 CssListStylePosition listStylePosition = CssListStylePosition.inherit; 266 CssFontWeight fontWeight = CssFontWeight.inherit; 267 string fontFaces; 268 CssValue color = CssValue.inherited; 269 CssValue backgroundColor = CssValue.inherited; 270 CssValue lineHeight = CssValue.inherited; 271 CssValue letterSpacing = CssValue.inherited; 272 CssValue width = CssValue.inherited; 273 CssValue height = CssValue.inherited; 274 CssValue marginLeft = CssValue.inherited; 275 CssValue marginRight = CssValue.inherited; 276 CssValue marginTop = CssValue.inherited; 277 CssValue marginBottom = CssValue.inherited; 278 CssValue paddingLeft = CssValue.inherited; 279 CssValue paddingRight = CssValue.inherited; 280 CssValue paddingTop = CssValue.inherited; 281 CssValue paddingBottom = CssValue.inherited; 282 CssValue fontSize = CssValue.inherited; 283 CssValue textIndent = CssValue.inherited; 284 } 285 286 /// selector rule type 287 enum CssSelectorRuleType : ubyte { 288 universal, // * 289 parent, // E > F 290 ancessor, // E F 291 predecessor, // E + F 292 attrset, // E[foo] 293 attreq, // E[foo="value"] 294 attrhas, // E[foo~="value"] 295 attrstarts, // E[foo|="value"] 296 id, // E#id 297 class_ // E.class 298 } 299 300 class CssSelectorRule 301 { 302 private: 303 CssSelectorRuleType _type; 304 elem_id _id; 305 attr_id _attrid; 306 CssSelectorRule _next; 307 string _value; 308 public: 309 this(CssSelectorRuleType type) { 310 _type = type; 311 } 312 this(const CssSelectorRule v) { 313 _type = v._type; 314 _id = v._id; 315 _attrid = v._attrid; 316 _value = v._value; 317 } 318 ~this() { 319 //if (_next) 320 // destroy(_next); 321 } 322 323 @property elem_id id() { return _id; } 324 @property void id(elem_id newid) { _id = newid; } 325 @property attr_id attrid() { return _attrid; } 326 @property void setAttr(attr_id newid, string value) { _attrid = newid; _value = value; } 327 @property CssSelectorRule next() { return _next; } 328 @property void next(CssSelectorRule v) { _next = v; } 329 /// check condition for node 330 bool check(ref Node node) const { 331 if (!node || !node.parent) 332 return false; 333 switch (_type) with (CssSelectorRuleType) { 334 case parent: // E > F 335 node = node.parent; 336 if (!node) 337 return false; 338 return node.id == _id; 339 340 case ancessor: // E F 341 for (;;) { 342 node = node.parent; 343 if (!node) 344 return false; 345 if (node.id == _id) 346 return true; 347 } 348 349 case predecessor: // E + F 350 int index = node.index; 351 // while 352 if (index > 0) { 353 Node elem = node.parent.childElement(index-1, _id); 354 if ( elem ) { 355 node = elem; 356 //CRLog::trace("+ selector: found pred element"); 357 return true; 358 } 359 //index--; 360 } 361 return false; 362 363 case attrset: // E[foo] 364 return node.hasAttr(_attrid); 365 366 case attreq: // E[foo="value"] 367 string val = node.attrValue(Ns.any, _attrid); 368 return (val == _value); 369 370 case attrhas: // E[foo~="value"] 371 // one of space separated values 372 string val = node.attrValue(Ns.any, _attrid); 373 int p = cast(int)val.indexOf(_value); 374 if (p < 0) 375 return false; 376 if ( (p > 0 && val[p - 1] != ' ') 377 || ( p + _value.length < val.length && val[p + _value.length] != ' ')) 378 return false; 379 return true; 380 381 case attrstarts: // E[foo|="value"] 382 string val = node.attrValue(Ns.any, _attrid); 383 if (_value.length > val.length) 384 return false; 385 return val[0 .. _value.length] == _value; 386 387 case id: // E#id 388 string val = node.attrValue(Ns.any, Attr.id); 389 return val == _value; 390 391 case class_: // E.class 392 string val = node.attrValue(Ns.any, Attr.class_); 393 return !val.icmp(_value); 394 395 case universal: // * 396 return true; 397 398 default: 399 return true; 400 } 401 } 402 } 403 404 import dlangui.core.cssparser; 405 406 /** simple CSS selector 407 408 Currently supports only element name and universal selector. 409 410 - * { } - universal selector 411 - element-name { } - selector by element name 412 - element1, element2 { } - several selectors delimited by comma 413 */ 414 class CssSelector { 415 private: 416 uint _id; 417 CssDeclaration _decl; 418 int _specificity; 419 CssSelector _next; 420 CssSelectorRule _rules; 421 public: 422 /// get element tag id (0 - any tag) 423 @property elem_id id() { return _id; } 424 /// set element tag id (0 - any tag) 425 @property void id(elem_id id) { _id = id; } 426 427 this() { } 428 429 ~this() { 430 //if (_next) 431 // destroy(_next); 432 } 433 434 void insertRuleStart(CssSelectorRule rule) { 435 rule.next = _rules; 436 _rules = rule; 437 } 438 439 void insertRuleAfterStart(CssSelectorRule rule) { 440 if (!_rules) { 441 _rules = rule; 442 } else { 443 rule.next = _rules.next; 444 _rules.next = rule; 445 } 446 } 447 448 /// check if selector rules match this node 449 bool check(Node node) const { 450 CssSelectorRule rule = cast(CssSelectorRule)_rules; 451 while (rule && node) { 452 if (!rule.check(node)) 453 return false; 454 rule = rule.next; 455 } 456 return true; 457 } 458 459 /// apply to style if selector matches 460 void apply(Node node, CssStyle style) const { 461 if (check(node)) 462 _decl.apply(style); 463 } 464 465 void setDeclaration(CssDeclaration decl) { 466 _decl = decl; 467 } 468 } 469 470 struct CssDeclItem { 471 CssDeclType type = CssDeclType.unknown; 472 union { 473 int value; 474 CssValue length; 475 } 476 string str; 477 478 void apply(CssStyle style) const { 479 switch (type) with (CssDeclType) { 480 case display: style.display = cast(CssDisplay)value; break; 481 case white_space: style.whiteSpace = cast(CssWhiteSpace)value; break; 482 case text_align: style.textAlign = cast(CssTextAlign)value; break; 483 case text_align_last: style.textAlignLast = cast(CssTextAlign)value; break; 484 case text_decoration: style.textDecoration = cast(CssTextDecoration)value; break; 485 486 case _webkit_hyphens: // -webkit-hyphens 487 case adobe_hyphenate: // adobe-hyphenate 488 case adobe_text_layout: // adobe-text-layout 489 case hyphenate: 490 style.hyphenate = cast(CssHyphenate)value; 491 break; // hyphenate 492 493 case color: style.color = length; break; 494 case background_color: style.backgroundColor = length; break; 495 case vertical_align: style.verticalAlign = cast(CssVerticalAlign)value; break; 496 case font_family: 497 if (value >= 0) 498 style.fontFamily = cast(CssFontFamily)value; 499 if (!str.empty) 500 style.fontFaces = str; 501 break; // id families like serif, sans-serif 502 //case font_names: break; // string font name like Arial, Courier 503 case font_style: style.fontStyle = cast(CssFontStyle)value; break; 504 case font_weight: style.fontWeight = cast(CssFontWeight)value; break; 505 case text_indent: style.textIndent = length; break; 506 case font_size: style.fontSize = length; break; 507 case line_height: style.lineHeight = length; break; 508 case letter_spacing: style.letterSpacing = length; break; 509 case width: style.width = length; break; 510 case height: style.height = length; break; 511 case margin_left: style.marginLeft = length; break; 512 case margin_right: style.marginRight = length; break; 513 case margin_top: style.marginTop = length; break; 514 case margin_bottom: style.marginBottom = length; break; 515 case padding_left: style.paddingLeft = length; break; 516 case padding_right: style.paddingRight = length; break; 517 case padding_top: style.paddingTop = length; break; 518 case padding_bottom: style.paddingBottom = length; break; 519 case page_break_before: style.pageBreakBefore = cast(CssPageBreak)value; break; 520 case page_break_after: style.pageBreakAfter = cast(CssPageBreak)value; break; 521 case page_break_inside: style.pageBreakInside = cast(CssPageBreak)value; break; 522 case list_style: break; // TODO 523 case list_style_type: style.listStyleType = cast(CssListStyleType)value; break; 524 case list_style_position: style.listStylePosition = cast(CssListStylePosition)value; break; 525 case list_style_image: break; // TODO 526 default: 527 break; 528 } 529 } 530 } 531 532 /// css declaration like { display: block; margin-top: 10px } 533 class CssDeclaration { 534 private CssDeclItem[] _list; 535 536 @property bool empty() { 537 return _list.length == 0; 538 } 539 540 void addLengthDecl(CssDeclType type, CssValue len) { 541 CssDeclItem item; 542 item.type = type; 543 item.length = len; 544 _list ~= item; 545 } 546 547 void addDecl(CssDeclType type, int value, string str) { 548 CssDeclItem item; 549 item.type = type; 550 item.value = value; 551 item.str = str; 552 _list ~= item; 553 } 554 555 void apply(CssStyle style) const { 556 foreach(item; _list) 557 item.apply(style); 558 } 559 } 560 561 /// CSS Style Sheet 562 class StyleSheet { 563 private: 564 CssSelector[elem_id] _selectorMap; 565 int _len; 566 public: 567 /// clears stylesheet 568 void clear() { 569 _selectorMap = null; 570 _len = 0; 571 } 572 573 /// count of selectors in stylesheet 574 @property int length() { return _len; } 575 576 /// add selector to stylesheet 577 void add(CssSelector selector) { 578 elem_id id = selector.id; 579 if (auto p = id in _selectorMap) { 580 for (;;) { 581 if (!(*p) || (*p)._specificity < selector._specificity) { 582 selector._next = (*p); 583 (*p) = selector; 584 _len++; 585 break; 586 } 587 p = &((*p)._next); 588 } 589 } else { 590 // first selector for this id 591 _selectorMap[id] = selector; 592 _len++; 593 } 594 } 595 596 /// apply stylesheet to node style 597 void apply(Node node, CssStyle style) { 598 elem_id id = node.id; 599 CssSelector selector_0, selector_id; 600 if (auto p = 0 in _selectorMap) 601 selector_0 = *p; 602 if (id) { 603 if (auto p = id in _selectorMap) 604 selector_id = *p; 605 } 606 for (;;) { 607 if (selector_0) { 608 if (!selector_id || selector_id._specificity < selector_0._specificity) { 609 selector_0.apply(node, style); 610 selector_0 = selector_0._next; 611 } else { 612 selector_id.apply(node, style); 613 selector_id = selector_id._next; 614 } 615 } else if (selector_id) { 616 selector_id.apply(node, style); 617 selector_id = selector_id._next; 618 } else { 619 // end of lists 620 break; 621 } 622 } 623 } 624 } 625 626 unittest { 627 CssStyle style = new CssStyle(); 628 CssWhiteSpace whiteSpace = CssWhiteSpace.inherit; 629 CssTextAlign textAlign = CssTextAlign.inherit; 630 CssTextAlign textAlignLast = CssTextAlign.inherit; 631 CssTextDecoration textDecoration = CssTextDecoration.inherit; 632 CssHyphenate hyphenate = CssHyphenate.inherit; 633 string src = "{ display: inline; text-decoration: underline; white-space: pre; text-align: right; text-align-last: left; " ~ 634 "hyphenate: auto; width: 70%; height: 1.5pt; margin-left: 2.0em; " ~ 635 "font-family: Arial, 'Times New Roman', sans-serif; font-size: 18pt; line-height: 120%; letter-spacing: 2px; font-weight: 300; " ~ 636 " }tail"; 637 CssDeclaration decl = parseCssDeclaration(src, true); 638 assert(decl !is null); 639 assert(!src.empty && src[0] == 't'); 640 assert(style.display == CssDisplay.block); 641 assert(style.textDecoration == CssTextDecoration.inherit); 642 assert(style.whiteSpace == CssWhiteSpace.inherit); 643 assert(style.textAlign == CssTextAlign.inherit); 644 assert(style.textAlignLast == CssTextAlign.inherit); 645 assert(style.hyphenate == CssHyphenate.inherit); 646 assert(style.width == CssValue.inherited); 647 decl.apply(style); 648 assert(style.display == CssDisplay.inline); 649 assert(style.textDecoration == CssTextDecoration.underline); 650 assert(style.whiteSpace == CssWhiteSpace.pre); 651 assert(style.textAlign == CssTextAlign.right); 652 assert(style.textAlignLast == CssTextAlign.left); 653 assert(style.hyphenate == CssHyphenate.auto_); 654 assert(style.width == CssValue(CssValueType.percent, 70)); 655 assert(style.height == CssValue(CssValueType.pt, 1*256 + 5*256/10)); // 1.5 656 assert(style.marginLeft == CssValue(CssValueType.em, 2*256 + 0*256/10)); // 2.0 657 assert(style.lineHeight == CssValue(CssValueType.percent, 120)); // 120% 658 assert(style.letterSpacing == CssValue(CssValueType.px, 2)); // 2px 659 assert(style.fontFamily == CssFontFamily.sans_serif); 660 assert(style.fontFaces == "\"Arial\", \"Times New Roman\""); 661 assert(style.fontWeight == CssFontWeight.fw_300); 662 }