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 }