1 module dlangui.core.cssparser; 2 3 import std.traits; 4 import std.conv : to; 5 import std.string; 6 import std.array : empty; 7 import std.algorithm : equal; 8 import std.ascii : isAlpha, isWhite; 9 10 import dlangui.core.dom; 11 import dlangui.core.css; 12 import dlangui.core.types : parseHexDigit; 13 14 /// skip specified count of chars of string, returns next available character, or 0 if end of string reached 15 private char skip(ref string src, int count = 1) { 16 if (count >= src.length) { 17 src = null; 18 return 0; 19 } 20 src = src[count .. $]; 21 return src[0]; 22 } 23 24 /// returns char of string at specified position (first by default) or 0 if end of string reached 25 private char peek(string str, int offset = 0) { 26 return offset >= str.length ? 0 : str[offset]; 27 } 28 29 /// skip spaces, move to new location, return first character in string, 0 if end of string reached 30 private char skipSpaces(ref string str) 31 { 32 string oldpos = str; 33 for (;;) { 34 char ch = str.peek; 35 if (!ch) 36 return 0; 37 while (isWhite(ch)) 38 ch = str.skip; 39 if (str.peek == '/' && str.peek(1) == '*') { 40 // comment found 41 str.skip(2); 42 while (str.peek && (str.peek(0) != '*' || str.peek(1) != '/')) 43 str.skip; 44 if (str.peek == '*' && str.peek(1) == '/' ) 45 str.skip(2); 46 } 47 ch = str.peek; 48 while (isWhite(ch)) 49 ch = str.skip; 50 if (oldpos.ptr is str.ptr) 51 break; 52 if (str.empty) 53 return 0; 54 oldpos = str; 55 } 56 return str.peek; 57 } 58 59 60 private bool isIdentChar(char ch) { 61 return isAlpha(ch) || (ch == '-') || (ch == '_'); 62 } 63 64 /// parse css identifier 65 private string parseIdent(ref string src) { 66 int pos = 0; 67 for ( ; pos < src.length; pos++) { 68 if (!src[pos].isIdentChar) 69 break; 70 } 71 if (!pos) 72 return null; 73 string res = src[0 .. pos]; 74 src.skip(pos); 75 src.skipSpaces; 76 return res; 77 } 78 79 private bool skipChar(ref string src, char ch) { 80 src.skipSpaces; 81 if (src.peek == ch) { 82 src.skip; 83 src.skipSpaces; 84 return true; 85 } 86 return false; 87 } 88 89 private string replaceChar(string s, char from, char to) { 90 foreach(ch; s) { 91 if (ch == from) { 92 char[] buf; 93 foreach(c; s) 94 if (c == from) 95 buf ~= to; 96 else 97 buf ~= c; 98 return buf.dup; 99 } 100 } 101 return s; 102 } 103 104 /// remove trailing _ from string, e.g. "body_" -> "body" 105 private string removeTrailingUnderscore(string s) { 106 if (s.endsWith("_")) 107 return s[0..$-1]; 108 return s; 109 } 110 111 private int parseEnumItem(E)(ref string src, int defValue = -1) if (is(E == enum)) { 112 string ident = replaceChar(parseIdent(src), '-', '_'); 113 foreach(member; EnumMembers!E) { 114 if (ident == removeTrailingUnderscore(member.to!string)) { 115 return member.to!int; 116 } 117 } 118 return defValue; 119 } 120 121 private CssDeclType parseCssDeclType(ref string src) { 122 int n = parseEnumItem!CssDeclType(src, -1); 123 if (n < 0) 124 return CssDeclType.unknown; 125 if (!skipChar(src, ':')) // no : after identifier 126 return CssDeclType.unknown; 127 return cast(CssDeclType)n; 128 } 129 130 private bool nextProperty(ref string str) { 131 int pos = 0; 132 for (; pos < str.length; pos++) { 133 char ch = str[pos]; 134 if (ch == '}') 135 break; 136 if (ch == ';') { 137 pos++; 138 break; 139 } 140 } 141 str.skip(pos); 142 str.skipSpaces; 143 return !str.empty && str[0] != '}'; 144 } 145 146 147 private int parseStandardColor(string ident) { 148 switch(ident) { 149 case "black": return 0x000000; 150 case "green": return 0x008000; 151 case "silver": return 0xC0C0C0; 152 case "lime": return 0x00FF00; 153 case "gray": return 0x808080; 154 case "olive": return 0x808000; 155 case "white": return 0xFFFFFF; 156 case "yellow": return 0xFFFF00; 157 case "maroon": return 0x800000; 158 case "navy": return 0x000080; 159 case "red": return 0xFF0000; 160 case "blue": return 0x0000FF; 161 case "purple": return 0x800080; 162 case "teal": return 0x008080; 163 case "fuchsia": return 0xFF00FF; 164 case "aqua": return 0x00FFFF; 165 default: return -1; 166 } 167 } 168 169 private bool parseColor(ref string src, ref CssValue value) 170 { 171 value.type = CssValueType.unspecified; 172 value.value = 0; 173 if (!src.skipSpaces) 174 return false; 175 string ident = parseIdent(src); 176 if (!ident.empty) { 177 switch(ident) { 178 case "inherited": 179 value.type = CssValueType.inherited; 180 return true; 181 case "none": 182 return true; 183 default: 184 int v = parseStandardColor(ident); 185 if (v >= 0) { 186 value.value = v; 187 value.type = CssValueType.color; 188 return true; 189 } 190 return false; 191 } 192 } 193 char ch = src[0]; 194 if (ch == '#') { 195 // #rgb or #rrggbb colors 196 ch = src.skip; 197 int nDigits = 0; 198 for ( ; nDigits < src.length && parseHexDigit(src[nDigits]) != uint.max; nDigits++ ) { 199 } 200 if ( nDigits==3 ) { 201 int r = parseHexDigit( src[0] ); 202 int g = parseHexDigit( src[1] ); 203 int b = parseHexDigit( src[2] ); 204 value.type = CssValueType.color; 205 value.value = (((r + r*16) * 256) | (g + g*16)) * 256 | (b + b*16); 206 src.skip(3); 207 return true; 208 } else if ( nDigits==6 ) { 209 int r = parseHexDigit( src[0] ) * 16; 210 r += parseHexDigit( src[1] ); 211 int g = parseHexDigit( src[2] ) * 16; 212 g += parseHexDigit( src[3] ); 213 int b = parseHexDigit( src[4] ) * 16; 214 b += parseHexDigit( src[5] ); 215 value.type = CssValueType.color; 216 value.value = ((r * 256) | g) * 256 | b; 217 src.skip(6); 218 return true; 219 } 220 } 221 return false; 222 } 223 224 private bool parseLength(ref string src, ref CssValue value) 225 { 226 value.type = CssValueType.unspecified; 227 value.value = 0; 228 src.skipSpaces; 229 string ident = parseIdent(src); 230 if (!ident.empty) { 231 switch(ident) { 232 case "inherited": 233 value.type = CssValueType.inherited; 234 return true; 235 default: 236 return false; 237 } 238 } 239 if (src.empty) 240 return false; 241 int n = 0; 242 char ch = src[0]; 243 if (ch != '.') { 244 if (ch < '0' || ch > '9') { 245 return false; // not a number 246 } 247 while (ch >= '0' && ch <= '9') { 248 n = n*10 + (ch - '0'); 249 ch = src.skip; 250 if (!ch) 251 break; 252 } 253 } 254 int frac = 0; 255 int frac_div = 1; 256 if (ch == '.') { 257 src.skip; 258 if (!src.empty) { 259 ch = src[0]; 260 while (ch >= '0' && ch <= '9') { 261 frac = frac*10 + (ch - '0'); 262 frac_div *= 10; 263 ch = src.skip; 264 if (!ch) 265 break; 266 } 267 } 268 } 269 if (ch == '%') { 270 value.type = CssValueType.percent; 271 src.skip; 272 } else { 273 ident = parseIdent(src); 274 if (!ident.empty) { 275 switch(ident) { 276 case "em": 277 case "m": // for DML - cannot add suffix which starts from 'e' 278 value.type = CssValueType.em; break; 279 case "pt": value.type = CssValueType.pt; break; 280 case "ex": value.type = CssValueType.ex; break; 281 case "px": value.type = CssValueType.px; break; 282 case "in": value.type = CssValueType.in_; break; 283 case "cm": value.type = CssValueType.cm; break; 284 case "mm": value.type = CssValueType.mm; break; 285 case "pc": value.type = CssValueType.pc; break; 286 default: 287 return false; 288 } 289 } else { 290 value.type = CssValueType.px; 291 } 292 } 293 if ( value.type == CssValueType.px || value.type == CssValueType.percent ) 294 value.value = n; // normal 295 else 296 value.value = n * 256 + 256 * frac / frac_div; // *256 297 return true; 298 } 299 300 private void appendItem(ref string[] list, ref char[] item) { 301 if (!item.empty) { 302 list ~= item.dup; 303 item.length = 0; 304 } 305 } 306 307 /// splits string like "Arial", Times New Roman, Courier; into list, stops on ; and } 308 /// returns true if at least one item added to list; moves str to new position 309 bool splitPropertyValueList(ref string str, ref string[] list) 310 { 311 int i=0; 312 char quote_char = 0; 313 char[] name; 314 bool last_space = false; 315 for (i=0; i < str.length; i++) { 316 char ch = str[i]; 317 switch(ch) { 318 case '\'': 319 case '\"': 320 if (quote_char == 0) { 321 if (!name.empty) 322 appendItem(list, name); 323 quote_char = ch; 324 } else if (quote_char == ch) { 325 if (!name.empty) 326 appendItem(list, name); 327 quote_char = 0; 328 } else { 329 // append char 330 name ~= ch; 331 } 332 last_space = false; 333 break; 334 case ',': 335 { 336 if (quote_char==0) { 337 if (!name.empty) 338 appendItem(list, name); 339 } else { 340 // inside quotation: append char 341 name ~= ch; 342 } 343 last_space = false; 344 } 345 break; 346 case '\t': 347 case ' ': 348 { 349 if (quote_char != 0) 350 name ~= ch; 351 last_space = true; 352 } 353 break; 354 case ';': 355 case '}': 356 if (quote_char==0) { 357 if (!name.empty) 358 appendItem(list, name); 359 str = i < str.length ? str[i .. $] : null; 360 return list.length > 0; 361 } else { 362 // inside quotation: append char 363 name ~= ch; 364 last_space = false; 365 } 366 break; 367 default: 368 if (last_space && !name.empty && quote_char == 0) 369 name ~= ' '; 370 name ~= ch; 371 last_space = false; 372 break; 373 } 374 } 375 if (!name.empty) 376 appendItem(list, name); 377 str = i < str.length ? str[i .. $] : null; 378 return list.length > 0; 379 } 380 381 unittest { 382 string src = "Arial, 'Times New Roman', \"Arial Black\", sans-serif; next-property: }"; 383 string[] list; 384 assert(splitPropertyValueList(src, list)); 385 assert(list.length == 4); 386 assert(list[0] == "Arial"); 387 assert(list[1] == "Times New Roman"); 388 assert(list[2] == "Arial Black"); 389 assert(list[3] == "sans-serif"); 390 } 391 392 /// joins list of items into comma separated string, each item in quotation marks 393 string joinPropertyValueList(string[] list) { 394 if (list.empty) 395 return null; 396 char[] res; 397 398 for (int i = 0; i < list.length; i++) { 399 if (i > 0) 400 res ~= ", "; 401 res ~= "\""; 402 res ~= list[i]; 403 res ~= "\""; 404 } 405 406 return res.dup; 407 } 408 409 unittest { 410 assert(joinPropertyValueList(["item1", "item 2"]) == "\"item1\", \"item 2\""); 411 } 412 413 414 private bool parseAttrValue(ref string str, ref string attrvalue) 415 { 416 char[] buf; 417 int pos = 0; 418 if (!str.skipSpaces) 419 return false; 420 char ch = str[0]; 421 if (ch == '\"') { 422 str.skip; 423 for ( ; pos < str.length && str[pos] != '\"'; pos++) { 424 if (pos >= 1000) 425 return false; 426 } 427 if (pos >= str.length || str[pos] != '\"') 428 return false; 429 buf ~= str[0 .. pos]; 430 str.skip(pos + 1); 431 if (!str.skipSpaces) 432 return false; 433 if (str[0] != ']') 434 return false; 435 str.skip; 436 attrvalue = buf.dup; 437 return true; 438 } else { 439 for ( ; pos < str.length && str[pos] != ' ' && str[pos] != '\t' && str[pos] != ']'; pos++) { 440 if (pos >= 1000) 441 return false; 442 } 443 if (pos >= str.length || str[pos] != ']') 444 return false; 445 buf ~= str[0 .. pos]; 446 str.skip(pos + 1); 447 attrvalue = buf.dup; 448 return true; 449 } 450 } 451 452 private CssSelectorRule parseAttr(ref string str, Document doc) 453 { 454 CssSelectorRuleType st = CssSelectorRuleType.universal; 455 char ch = str[0]; 456 if (ch == '.') { 457 // E.class 458 str.skip; 459 str.skipSpaces; 460 string attrvalue = parseIdent(str); 461 if (attrvalue.empty) 462 return null; 463 CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.class_); 464 rule.setAttr(Attr.class_, attrvalue.toLower); 465 return rule; 466 } else if (ch == '#') { 467 // E#id 468 str.skip; 469 str.skipSpaces; 470 string attrvalue = parseIdent(str); 471 if (attrvalue.empty) 472 return null; 473 CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.id); 474 rule.setAttr(Attr.id, attrvalue.toLower); 475 return rule; 476 } else if (ch != '[') 477 return null; 478 // [.....] rule 479 str.skip; // skip [ 480 str.skipSpaces; 481 string attrname = parseIdent(str); 482 if (attrname.empty) 483 return null; 484 if (!str.skipSpaces) 485 return null; 486 string attrvalue = null; 487 ch = str[0]; 488 if (ch == ']') { 489 // empty [] 490 st = CssSelectorRuleType.attrset; 491 str.skip; // skip ] 492 } else if (ch == '=') { 493 str.skip; // skip = 494 if (!parseAttrValue(str, attrvalue)) 495 return null; 496 st = CssSelectorRuleType.attreq; 497 } else if (ch == '~' && str.length > 1 && str[1] == '=') { 498 str.skip(2); // skip ~= 499 if (!parseAttrValue(str, attrvalue)) 500 return null; 501 st = CssSelectorRuleType.attrhas; 502 } else if (ch == '|' && str.length > 1 && str[1] == '=') { 503 str.skip(2); // skip |= 504 if (!parseAttrValue(str, attrvalue)) 505 return null; 506 st = CssSelectorRuleType.attrstarts; 507 } else { 508 return null; 509 } 510 CssSelectorRule rule = new CssSelectorRule(st); 511 attr_id id = doc.attrId(attrname); 512 rule.setAttr(id, attrvalue); 513 return rule; 514 } 515 516 /// Parse css properties declaration either in {} or w/o {} - e.g. { width: 40%; margin-top: 3px } -- returns null if parse error occured or property list is empty 517 CssDeclaration parseCssDeclaration(ref string src, bool mustBeInBrackets = true) { 518 if (!src.skipSpaces) 519 return null; 520 if (mustBeInBrackets && !skipChar(src, '{')) 521 return null; // decl must start with { 522 CssDeclaration res = new CssDeclaration(); 523 for (;;) { 524 CssDeclType propId = parseCssDeclType(src); 525 if (src.empty) 526 break; 527 if (propId != CssDeclType.unknown) { 528 int n = -1; 529 string s = null; 530 switch(propId) with(CssDeclType) { 531 case display: n = parseEnumItem!CssDisplay(src, -1); break; 532 case white_space: n = parseEnumItem!CssWhiteSpace(src, -1); break; 533 case text_align: n = parseEnumItem!CssTextAlign(src, -1); break; 534 case text_align_last: n = parseEnumItem!CssTextAlign(src, -1); break; 535 case text_decoration: n = parseEnumItem!CssTextDecoration(src, -1); break; 536 case hyphenate: 537 case _webkit_hyphens: // -webkit-hyphens 538 case adobe_hyphenate: // adobe-hyphenate 539 case adobe_text_layout: // adobe-text-layout 540 n = parseEnumItem!CssHyphenate(src, -1); 541 break; // hyphenate 542 case color: 543 case background_color: 544 CssValue v; 545 if (parseColor(src, v)) { 546 res.addLengthDecl(propId, v); 547 } 548 break; 549 case vertical_align: n = parseEnumItem!CssVerticalAlign(src, -1); break; 550 case font_family: // id families like serif, sans-serif 551 string[] list; 552 string[] faceList; 553 if (splitPropertyValueList(src, list)) { 554 foreach(item; list) { 555 string name = item; 556 int family = parseEnumItem!CssFontFamily(name, -1); 557 if (family != -1) { 558 // family name, e.g. sans-serif 559 n = family; 560 } else { 561 faceList ~= item; 562 } 563 } 564 } 565 s = joinPropertyValueList(faceList); 566 break; 567 case font_style: n = parseEnumItem!CssFontStyle(src, -1); break; 568 case font_weight: 569 n = parseEnumItem!CssFontWeight(src, -1); 570 if (n < 0) { 571 CssValue value; 572 if (parseLength(src, value)) { 573 if (value.type == CssValueType.px) { 574 if (value.value < 150) 575 n = CssFontWeight.fw_100; 576 else if (value.value < 250) 577 n = CssFontWeight.fw_200; 578 else if (value.value < 350) 579 n = CssFontWeight.fw_300; 580 else if (value.value < 450) 581 n = CssFontWeight.fw_400; 582 else if (value.value < 550) 583 n = CssFontWeight.fw_500; 584 else if (value.value < 650) 585 n = CssFontWeight.fw_600; 586 else if (value.value < 750) 587 n = CssFontWeight.fw_700; 588 else if (value.value < 850) 589 n = CssFontWeight.fw_800; 590 else 591 n = CssFontWeight.fw_900; 592 } 593 } 594 } 595 596 //n = parseEnumItem!Css(src, -1); 597 break; 598 case text_indent: 599 { 600 // read length 601 CssValue len; 602 bool negative = false; 603 if (src[0] == '-') { 604 src.skip; 605 negative = true; 606 } 607 if (parseLength(src, len)) { 608 // read optional "hanging" flag 609 src.skipSpaces; 610 string attr = parseIdent(src); 611 if (attr == "hanging") 612 len.value = -len.value; 613 res.addLengthDecl(propId, len); 614 } 615 } 616 break; 617 case line_height: 618 case letter_spacing: 619 case font_size: 620 case width: 621 case height: 622 case margin_left: 623 case margin_right: 624 case margin_top: 625 case margin_bottom: 626 case padding_left: 627 case padding_right: 628 case padding_top: 629 case padding_bottom: 630 // parse length 631 CssValue value; 632 if (parseLength(src, value)) 633 res.addLengthDecl(propId, value); 634 break; 635 case margin: 636 case padding: 637 //n = parseEnumItem!Css(src, -1); 638 CssValue[4] len; 639 int i; 640 for (i = 0; i < 4; ++i) 641 if (!parseLength(src, len[i])) 642 break; 643 if (i) { 644 switch (i) { 645 case 1: 646 len[1] = len[0]; 647 goto case; /* fall through */ 648 case 2: 649 len[2] = len[0]; 650 goto case; /* fall through */ 651 case 3: 652 len[3] = len[1]; 653 break; 654 default: 655 break; 656 } 657 if (propId == margin) { 658 res.addLengthDecl(margin_left, len[0]); 659 res.addLengthDecl(margin_top, len[1]); 660 res.addLengthDecl(margin_right, len[2]); 661 res.addLengthDecl(margin_bottom, len[3]); 662 } else { 663 res.addLengthDecl(padding_left, len[0]); 664 res.addLengthDecl(padding_top, len[1]); 665 res.addLengthDecl(padding_right, len[2]); 666 res.addLengthDecl(padding_bottom, len[3]); 667 } 668 } 669 break; 670 case page_break_before: 671 case page_break_inside: 672 case page_break_after: 673 n = parseEnumItem!CssPageBreak(src, -1); 674 break; 675 case list_style: 676 //n = parseEnumItem!Css(src, -1); 677 break; 678 case list_style_type: n = parseEnumItem!CssListStyleType(src, -1); break; 679 case list_style_position: n = parseEnumItem!CssListStylePosition(src, -1); break; 680 case list_style_image: 681 //n = parseEnumItem!CssListStyleImage(src, -1); 682 break; 683 default: 684 break; 685 } 686 if (n >= 0 || !s.empty) 687 res.addDecl(propId, n, s); 688 } 689 if (!nextProperty(src)) 690 break; 691 } 692 if (mustBeInBrackets && !skipChar(src, '}')) 693 return null; 694 if (res.empty) 695 return null; 696 return res; 697 } 698 699 /// parse Css selector, return selector object if parsed ok, null if error occured 700 CssSelector parseCssSelector(ref string str, Document doc) { 701 if (str.empty) 702 return null; 703 CssSelector res = new CssSelector(); 704 for (;;) { 705 if (!str.skipSpaces) 706 return null; 707 char ch = str[0]; 708 string ident = parseIdent(str); 709 if (ch == '*') { // universal selector 710 str.skip; 711 str.skipSpaces; 712 res.id = 0; 713 } else if (ch == '.') { // classname follows 714 res.id = 0; 715 // will be parsed as attribute 716 } else if (!ident.empty) { 717 // ident 718 res.id = doc.tagId(ident); 719 } else { 720 return null; 721 } 722 if (!str.skipSpaces) 723 return null; 724 ch = str[0]; 725 if (ch == ',' || ch == '{') 726 return res; 727 // one or more attribute rules 728 bool attr_rule = false; 729 while (ch == '[' || ch == '.' || ch == '#') { 730 CssSelectorRule rule = parseAttr(str, doc); 731 if (!rule) 732 return null; 733 res.insertRuleStart(rule); //insertRuleAfterStart 734 ch = str.skipSpaces; 735 attr_rule = true; 736 //continue; 737 } 738 // element relation 739 if (ch == '>') { 740 str.skip; 741 CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.parent); 742 rule.id = res.id; 743 res.insertRuleStart(rule); 744 res.id = 0; 745 continue; 746 } else if (ch == '+') { 747 str.skip; 748 CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.predecessor); 749 rule.id = res.id; 750 res.insertRuleStart(rule); 751 res.id = 0; 752 continue; 753 } else if (ch.isAlpha) { 754 CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.ancessor); 755 rule.id = res.id; 756 res.insertRuleStart(rule); 757 res.id = 0; 758 continue; 759 } 760 if (!attr_rule) 761 return null; 762 else if (str.length > 0 && (str[0] == ',' || str[0] == '{')) 763 return res; 764 } 765 } 766 767 /// skips until } or end of string, returns true if some characters left in string 768 private bool skipUntilEndOfRule(ref string str) 769 { 770 while (str.length && str[0] != '}') 771 str.skip; 772 if (str.peek == '}') 773 str.skip; 774 return !str.empty; 775 } 776 777 778 unittest { 779 Document doc = new Document(); 780 string str; 781 str = "body { width: 50% }"; 782 assert(parseCssSelector(str, doc) !is null); 783 assert(parseCssDeclaration(str, true) !is null); 784 str = "body > p { font-family: sans-serif }"; 785 assert(parseCssSelector(str, doc) !is null); 786 assert(parseCssDeclaration(str, true) !is null); 787 str = ".myclass + div { }"; 788 assert(parseCssSelector(str, doc) !is null); 789 assert(parseCssDeclaration(str, true) is null); // empty property decl 790 destroy(doc); 791 } 792 793 /// parse stylesheet text 794 bool parseStyleSheet(StyleSheet sheet, Document doc, string str) { 795 bool res = false; 796 for(;;) { 797 if (!str.skipSpaces) 798 break; 799 CssSelector[] selectors; 800 for(;;) { 801 CssSelector selector = parseCssSelector(str, doc); 802 if (!selector) 803 break; 804 selectors ~= selector; 805 str.skipChar(','); 806 } 807 if (selectors.length) { 808 if (CssDeclaration decl = parseCssDeclaration(str, true)) { 809 foreach(item; selectors) { 810 item.setDeclaration(decl); 811 sheet.add(item); 812 res = true; 813 } 814 } 815 } 816 if (!skipUntilEndOfRule(str)) 817 break; 818 } 819 return res; 820 } 821 822 unittest { 823 string src = q{ 824 body { width: 50%; color: blue } 825 body > div, body > section { 826 /* some comment 827 goes here */ 828 font-family: serif; 829 background-color: yellow; 830 } 831 section { 832 margin-top: 5px 833 } 834 }; 835 Document doc = new Document(); 836 StyleSheet sheet = new StyleSheet(); 837 assert(parseStyleSheet(sheet, doc, src)); 838 assert(sheet.length == 2); 839 // check appending of additional source text 840 assert(parseStyleSheet(sheet, doc, "pre { white-space: pre }")); 841 assert(sheet.length == 3); 842 destroy(doc); 843 } 844 845 unittest { 846 Document doc = new Document(); 847 StyleSheet sheet = new StyleSheet(); 848 assert(parseStyleSheet(sheet, doc, "* { color: #aaa }")); 849 assert(sheet.length == 1); 850 assert(parseStyleSheet(sheet, doc, "div, p { display: block }")); 851 assert(sheet.length == 3); 852 // check appending of additional source text 853 assert(parseStyleSheet(sheet, doc, "pre { white-space: pre }")); 854 assert(sheet.length == 4); 855 assert(parseStyleSheet(sheet, doc, "pre { font-size: 120% }")); 856 assert(sheet.length == 5); 857 destroy(doc); 858 }