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 }