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 }