mirror of https://github.com/buggins/dlangui.git
css parser
This commit is contained in:
parent
9f768c85af
commit
4f0cced6b4
|
@ -5,6 +5,9 @@ This module contains implementation of CSS support - Cascading Style Sheets.
|
|||
|
||||
Port of CoolReader Engine written in C++.
|
||||
|
||||
Supports subset of CSS standards.
|
||||
|
||||
|
||||
Synopsis:
|
||||
|
||||
----
|
||||
|
@ -324,7 +327,7 @@ public:
|
|||
@property CssSelectorRule next() { return _next; }
|
||||
@property void next(CssSelectorRule v) { _next = v; }
|
||||
/// check condition for node
|
||||
bool check(ref Node node) {
|
||||
bool check(ref Node node) const {
|
||||
if (!node || !node.parent)
|
||||
return false;
|
||||
switch (_type) with (CssSelectorRuleType) {
|
||||
|
@ -422,17 +425,17 @@ public:
|
|||
@property void id(elem_id id) { _id = id; }
|
||||
|
||||
this() { }
|
||||
|
||||
~this() {
|
||||
//if (_next)
|
||||
// destroy(_next);
|
||||
//if (_rules)
|
||||
// destroy(_rules);
|
||||
}
|
||||
|
||||
void insertRuleStart(CssSelectorRule rule) {
|
||||
rule.next = _rules;
|
||||
_rules = rule;
|
||||
}
|
||||
|
||||
void insertRuleAfterStart(CssSelectorRule rule) {
|
||||
if (!_rules) {
|
||||
_rules = rule;
|
||||
|
@ -442,26 +445,26 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
@property uint tagId() { return _id; }
|
||||
bool check(const Node node) const {
|
||||
// TODO
|
||||
return false;
|
||||
/// check if selector rules match this node
|
||||
bool check(Node node) const {
|
||||
CssSelectorRule rule = cast(CssSelectorRule)_rules;
|
||||
while (rule && node) {
|
||||
if (!rule.check(node))
|
||||
return false;
|
||||
rule = rule.next;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// apply to style if selector matches
|
||||
void apply(const Node node, CssStyle style) const
|
||||
{
|
||||
void apply(Node node, CssStyle style) const {
|
||||
if (check(node))
|
||||
_decl.apply(style);
|
||||
}
|
||||
void setDeclaration(CssDeclaration decl) {
|
||||
|
||||
void setDeclaration(CssDeclaration decl) {
|
||||
_decl = decl;
|
||||
}
|
||||
int getSpecificity() {
|
||||
return _specificity;
|
||||
}
|
||||
@property CssSelector next() { return _next; }
|
||||
@property void next(CssSelector next) { _next = next; }
|
||||
//lUInt32 getHash();
|
||||
}
|
||||
|
||||
struct CssDeclItem {
|
||||
|
@ -555,6 +558,71 @@ class CssDeclaration {
|
|||
}
|
||||
}
|
||||
|
||||
/// CSS Style Sheet
|
||||
class StyleSheet {
|
||||
private:
|
||||
CssSelector[elem_id] _selectorMap;
|
||||
int _len;
|
||||
public:
|
||||
/// clears stylesheet
|
||||
void clear() {
|
||||
_selectorMap = null;
|
||||
_len = 0;
|
||||
}
|
||||
|
||||
/// count of selectors in stylesheet
|
||||
@property int length() { return _len; }
|
||||
|
||||
/// add selector to stylesheet
|
||||
void add(CssSelector selector) {
|
||||
elem_id id = selector.id;
|
||||
if (auto p = id in _selectorMap) {
|
||||
for (;;) {
|
||||
if (!(*p) || (*p)._specificity < selector._specificity) {
|
||||
selector._next = (*p);
|
||||
(*p) = selector;
|
||||
_len++;
|
||||
break;
|
||||
}
|
||||
p = &((*p)._next);
|
||||
}
|
||||
} else {
|
||||
// first selector for this id
|
||||
_selectorMap[id] = selector;
|
||||
_len++;
|
||||
}
|
||||
}
|
||||
|
||||
/// apply stylesheet to node style
|
||||
void apply(Node node, CssStyle style) {
|
||||
elem_id id = node.id;
|
||||
CssSelector selector_0, selector_id;
|
||||
if (auto p = 0 in _selectorMap)
|
||||
selector_0 = *p;
|
||||
if (id) {
|
||||
if (auto p = id in _selectorMap)
|
||||
selector_id = *p;
|
||||
}
|
||||
for (;;) {
|
||||
if (selector_0) {
|
||||
if (!selector_id || selector_id._specificity < selector_0._specificity) {
|
||||
selector_0.apply(node, style);
|
||||
selector_0 = selector_0._next;
|
||||
} else {
|
||||
selector_id.apply(node, style);
|
||||
selector_id = selector_id._next;
|
||||
}
|
||||
} else if (selector_id) {
|
||||
selector_id.apply(node, style);
|
||||
selector_id = selector_id._next;
|
||||
} else {
|
||||
// end of lists
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unittest {
|
||||
CssStyle style = new CssStyle();
|
||||
CssWhiteSpace whiteSpace = CssWhiteSpace.inherit;
|
||||
|
|
|
@ -20,27 +20,42 @@ private char skip(ref string src, int count = 1) {
|
|||
return src[0];
|
||||
}
|
||||
|
||||
/// returns first char of string or 0 if end of string reached
|
||||
private char peek(string str) {
|
||||
return str.empty ? 0 : str[0];
|
||||
/// returns char of string at specified position (first by default) or 0 if end of string reached
|
||||
private char peek(string str, int offset = 0) {
|
||||
return offset >= str.length ? 0 : str[offset];
|
||||
}
|
||||
|
||||
/// skip spaces, move to new location, return first character in string, 0 if end of string reached
|
||||
private char skipSpaces(ref string src) {
|
||||
for(;;) {
|
||||
if (src.empty) {
|
||||
src = null;
|
||||
private char skipSpaces(ref string str)
|
||||
{
|
||||
string oldpos = str;
|
||||
for (;;) {
|
||||
char ch = str.peek;
|
||||
if (!ch)
|
||||
return 0;
|
||||
while (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r')
|
||||
ch = str.skip;
|
||||
if (str.peek == '/' && str.peek(1) == '*') {
|
||||
// comment found
|
||||
str.skip(2);
|
||||
while (str.peek && (str.peek(0) != '*' || str.peek(1) != '/'))
|
||||
str.skip;
|
||||
if (str.peek == '*' && str.peek(1) == '/' )
|
||||
str.skip(2);
|
||||
}
|
||||
char ch = src[0];
|
||||
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
|
||||
src.skip;
|
||||
} else {
|
||||
return src.empty ? 0 : src[0];
|
||||
}
|
||||
ch = str.peek;
|
||||
while (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r')
|
||||
ch = str.skip;
|
||||
if (oldpos.ptr is str.ptr)
|
||||
break;
|
||||
if (str.empty)
|
||||
return 0;
|
||||
oldpos = str;
|
||||
}
|
||||
return str.peek;
|
||||
}
|
||||
|
||||
|
||||
private bool isIdentChar(char ch) {
|
||||
return (ch >= 'A' && ch <='Z') || (ch >= 'a' && ch <='z') || (ch == '-') || (ch == '_');
|
||||
}
|
||||
|
@ -62,7 +77,7 @@ private string parseIdent(ref string src) {
|
|||
|
||||
private bool skipChar(ref string src, char ch) {
|
||||
src.skipSpaces;
|
||||
if (src.length > 0 && src[0] == ch) {
|
||||
if (src.peek == ch) {
|
||||
src.skip;
|
||||
src.skipSpaces;
|
||||
return true;
|
||||
|
@ -506,6 +521,7 @@ private CssSelectorRule parseAttr(ref string str, Document doc)
|
|||
return rule;
|
||||
}
|
||||
|
||||
/// 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
|
||||
CssDeclaration parseCssDeclaration(ref string src, bool mustBeInBrackets = true) {
|
||||
if (!src.skipSpaces)
|
||||
return null;
|
||||
|
@ -688,7 +704,7 @@ CssDeclaration parseCssDeclaration(ref string src, bool mustBeInBrackets = true)
|
|||
return res;
|
||||
}
|
||||
|
||||
/// parse Css selector, return selector object if parsed ok
|
||||
/// parse Css selector, return selector object if parsed ok, null if error occured
|
||||
CssSelector parseCssSelector(ref string str, Document doc) {
|
||||
if (str.empty)
|
||||
return null;
|
||||
|
@ -756,6 +772,17 @@ CssSelector parseCssSelector(ref string str, Document doc) {
|
|||
}
|
||||
}
|
||||
|
||||
/// skips until } or end of string, returns true if some characters left in string
|
||||
private bool skipUntilEndOfRule(ref string str)
|
||||
{
|
||||
while (str.length && str[0] != '}')
|
||||
str.skip;
|
||||
if (str.peek == '}')
|
||||
str.skip;
|
||||
return !str.empty;
|
||||
}
|
||||
|
||||
|
||||
unittest {
|
||||
Document doc = new Document();
|
||||
string str;
|
||||
|
@ -768,4 +795,72 @@ unittest {
|
|||
str = ".myclass + div { }";
|
||||
assert(parseCssSelector(str, doc) !is null);
|
||||
assert(parseCssDeclaration(str, true) is null); // empty property decl
|
||||
}
|
||||
destroy(doc);
|
||||
}
|
||||
|
||||
/// parse stylesheet text
|
||||
bool parseStyleSheet(StyleSheet sheet, Document doc, string str) {
|
||||
bool res = false;
|
||||
for(;;) {
|
||||
if (!str.skipSpaces)
|
||||
break;
|
||||
CssSelector[] selectors;
|
||||
for(;;) {
|
||||
CssSelector selector = parseCssSelector(str, doc);
|
||||
if (!selector)
|
||||
break;
|
||||
selectors ~= selector;
|
||||
str.skipChar(',');
|
||||
}
|
||||
if (selectors.length) {
|
||||
if (CssDeclaration decl = parseCssDeclaration(str, true)) {
|
||||
foreach(item; selectors) {
|
||||
item.setDeclaration(decl);
|
||||
sheet.add(item);
|
||||
res = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!skipUntilEndOfRule(str))
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
unittest {
|
||||
string src = q{
|
||||
body { width: 50%; color: blue }
|
||||
body > div, body > section {
|
||||
/* some comment
|
||||
goes here */
|
||||
font-family: serif;
|
||||
background-color: yellow;
|
||||
}
|
||||
section {
|
||||
margin-top: 5px
|
||||
}
|
||||
};
|
||||
Document doc = new Document();
|
||||
StyleSheet sheet = new StyleSheet();
|
||||
assert(parseStyleSheet(sheet, doc, src));
|
||||
assert(sheet.length == 2);
|
||||
// check appending of additional source text
|
||||
assert(parseStyleSheet(sheet, doc, "pre { white-space: pre }"));
|
||||
assert(sheet.length == 3);
|
||||
destroy(doc);
|
||||
}
|
||||
|
||||
unittest {
|
||||
Document doc = new Document();
|
||||
StyleSheet sheet = new StyleSheet();
|
||||
assert(parseStyleSheet(sheet, doc, "* { color: #aaa }"));
|
||||
assert(sheet.length == 1);
|
||||
assert(parseStyleSheet(sheet, doc, "div, p { display: block }"));
|
||||
assert(sheet.length == 3);
|
||||
// check appending of additional source text
|
||||
assert(parseStyleSheet(sheet, doc, "pre { white-space: pre }"));
|
||||
assert(sheet.length == 4);
|
||||
assert(parseStyleSheet(sheet, doc, "pre { font-size: 120% }"));
|
||||
assert(sheet.length == 5);
|
||||
destroy(doc);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue