Skip to content

Commit d7f7d8c

Browse files
committed
Use Matcher combinators to greatly simplify complex ValidationTypes.
This allows concise description of complex ValidationTypes, using syntax parallel to that in the official CSS specification. This patch just implements the mechanism; a follow-up patch uses it to simplify existing ValidationTypes.
1 parent 739bf0a commit d7f7d8c

File tree

3 files changed

+244
-3
lines changed

3 files changed

+244
-3
lines changed

src/css/PropertyValueIterator.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,11 @@ PropertyValueIterator.prototype.restore = function(){
122122
}
123123
};
124124

125+
/**
126+
* Drops the last saved bookmark.
127+
* @return {void}
128+
* @method drop
129+
*/
130+
PropertyValueIterator.prototype.drop = function() {
131+
this._marks.pop();
132+
};

src/css/Validation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ var Validation = {
5353
part = expression.peek();
5454
throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col);
5555
} else {
56-
throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col);
56+
throw new ValidationError("Expected (" + ValidationTypes.describe(types) + ") but found '" + value + "'.", value.line, value.col);
5757
}
5858
} else if (expression.hasNext()) {
5959
part = expression.next();

src/css/ValidationTypes.js

Lines changed: 235 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,230 @@
11
//This file will likely change a lot! Very experimental!
2-
/*global ValidationError */
3-
var ValidationTypes = {
2+
var ValidationTypes;
3+
4+
/**
5+
* This class implements a combinator library for matcher functions.
6+
* The combinators are described at:
7+
* https://developer.mozilla.org/en-US/docs/Web/CSS/Value_definition_syntax#Component_value_combinators
8+
*/
9+
var Matcher = function(matchFunc, toString) {
10+
this.match = function(expression) {
11+
// Save/restore marks to ensure that failed matches always restore
12+
// the original location in the expression.
13+
var result;
14+
expression.mark();
15+
result = matchFunc(expression);
16+
if (result) {
17+
expression.drop();
18+
} else {
19+
expression.restore();
20+
}
21+
return result;
22+
};
23+
this.toString = typeof toString === "function" ? toString : function() {
24+
return toString;
25+
};
26+
};
27+
28+
/** Precedence table of combinators. */
29+
Matcher.prec = {
30+
MOD: 5,
31+
SEQ: 4,
32+
ANDAND: 3,
33+
OROR: 2,
34+
ALT: 1,
35+
};
36+
37+
/**
38+
* Convert a string to a matcher (parsing simple alternations),
39+
* or do nothing if the argument is already a matcher.
40+
*/
41+
Matcher.cast = function(m) {
42+
if (m instanceof Matcher) {
43+
return m;
44+
}
45+
if (/ \| /.test(m)) {
46+
return Matcher.alt.apply(Matcher, m.split(" | "));
47+
}
48+
return Matcher.fromType(m);
49+
};
50+
51+
/**
52+
* Create a matcher for a single type.
53+
*/
54+
Matcher.fromType = function(type) {
55+
return new Matcher(function(expression) {
56+
return ValidationTypes.isType(expression, type);
57+
}, type);
58+
};
59+
60+
/**
61+
* Create a matcher for one or more juxtaposed words, which all must
62+
* occur, in the given order.
63+
*/
64+
Matcher.seq = function() {
65+
var ms = Array.prototype.slice.call(arguments).map(Matcher.cast);
66+
if (ms.length === 1) { return ms[0]; }
67+
return new Matcher(function(expression) {
68+
var i, result = true;
69+
for (i = 0; result && i < ms.length; i++) {
70+
result = ms[i].match(expression);
71+
}
72+
return result;
73+
}, function(prec) {
74+
var p = Matcher.prec.SEQ;
75+
var s = ms.map(function(m) { return m.toString(p); }).join(" ");
76+
if (prec > p) { s = "[ " + s + " ]"; }
77+
return s;
78+
});
79+
};
80+
81+
/**
82+
* Create a matcher for one or more alternatives, where exactly one
83+
* must occur.
84+
*/
85+
Matcher.alt = function() {
86+
var ms = Array.prototype.slice.call(arguments).map(Matcher.cast);
87+
if (ms.length === 1) { return ms[0]; }
88+
return new Matcher(function(expression) {
89+
var i, result = false;
90+
for (i = 0; !result && i < ms.length; i++) {
91+
result = ms[i].match(expression);
92+
}
93+
return result;
94+
}, function(prec) {
95+
var p = Matcher.prec.ALT;
96+
var s = ms.map(function(m) { return m.toString(p); }).join(" | ");
97+
if (prec > p) { s = "[ " + s + " ]"; }
98+
return s;
99+
});
100+
};
101+
102+
/**
103+
* Create a matcher for two or more options. This implements the
104+
* double bar (||) and double ampersand (&&) operators, as well as
105+
* variants of && where some of the alternatives are optional.
106+
*/
107+
Matcher.many = function(required) {
108+
var ms = Array.prototype.slice.call(arguments, 1).reduce(function(acc, v) {
109+
if (v.expand) {
110+
// Insert all of the options for the given complex rule as
111+
// individual options.
112+
acc.push.apply(acc, ValidationTypes.complex[v.expand].options);
113+
} else {
114+
acc.push(Matcher.cast(v));
115+
}
116+
return acc;
117+
}, []);
118+
if (required === true) { required = ms.map(function() { return true; }); }
119+
var result = new Matcher(function(expression) {
120+
var seen = [], i, j;
121+
for (i = 0; expression.hasNext() && i < ms.length; i++) {
122+
for (j = 0; j < ms.length; j++) {
123+
if (!seen[j] && ms[j].match(expression)) {
124+
seen[j] = true;
125+
break;
126+
}
127+
}
128+
if (j === ms.length) {
129+
break;
130+
}
131+
}
132+
if (required === false) {
133+
return (i > 0);
134+
}
135+
// Finer-grained specification of which are required.
136+
for (i = 0; i < ms.length; i++) {
137+
if (required[i] && !seen[i]) {
138+
return false;
139+
}
140+
}
141+
return true;
142+
}, function(prec) {
143+
var p = (required === false) ? Matcher.prec.OROR : Matcher.prec.ANDAND;
144+
var s = ms.map(function(m, i) {
145+
if (required !== false && !required[i]) {
146+
return m.toString(Matcher.prec.MOD) + "?";
147+
}
148+
return m.toString(p);
149+
}).join(required === false ? " || " : " && ");
150+
if (prec > p) { s = "[ " + s + " ]"; }
151+
return s;
152+
});
153+
result.options = ms;
154+
return result;
155+
};
156+
157+
/**
158+
* Create a matcher for two or more options, where all options are
159+
* mandatory but they may appear in any order.
160+
*/
161+
Matcher.andand = function() {
162+
var args = Array.prototype.slice.call(arguments);
163+
args.unshift(true);
164+
return Matcher.many.apply(Matcher, args);
165+
};
166+
167+
/**
168+
* Create a matcher for two or more options, where options are
169+
* optional and may appear in any order, but at least one must be
170+
* present.
171+
*/
172+
Matcher.oror = function() {
173+
var args = Array.prototype.slice.call(arguments);
174+
args.unshift(false);
175+
return Matcher.many.apply(Matcher, args);
176+
};
177+
178+
/** Instance methods on Matchers. */
179+
Matcher.prototype = {
180+
constructor: Matcher,
181+
// These are expected to be overridden in every instance.
182+
match: function(expression) { throw new Error("unimplemented"); },
183+
toString: function() { throw new Error("unimplemented"); },
184+
// This returns a standalone function to do the matching.
185+
func: function() { return this.match.bind(this); },
186+
// Basic combinators
187+
then: function(m) { return Matcher.seq(this, m); },
188+
or: function(m) { return Matcher.alt(this, m); },
189+
andand: function(m) { return Matcher.many(true, this, m); },
190+
oror: function(m) { return Matcher.many(false, this, m); },
191+
// Component value multipliers
192+
star: function() { return this.braces(0, Infinity, "*"); },
193+
plus: function() { return this.braces(1, Infinity, "+"); },
194+
question: function() { return this.braces(0, 1, "?"); },
195+
hash: function() {
196+
return this.braces(1, Infinity, "#", Matcher.cast(","));
197+
},
198+
braces: function(min, max, marker, optSep) {
199+
var m = this;
200+
if (!marker) {
201+
marker = "{" + min + "," + max + "}";
202+
}
203+
return new Matcher(function(expression) {
204+
var result, i;
205+
for (i = 0, result = true; result && i < max && expression.hasNext(); ) {
206+
result = m.match(expression);
207+
if (result) {
208+
i++;
209+
if (optSep && i < max && expression.hasNext()) {
210+
expression.mark();
211+
result = optSep.match(expression);
212+
if (result && !expression.hasNext()) {
213+
// Trailing separator, boo. Back up.
214+
expression.restore();
215+
break;
216+
} else {
217+
expression.drop();
218+
}
219+
}
220+
}
221+
}
222+
return (i >= min);
223+
}, function() { return m.toString(Matcher.prec.MOD) + marker; });
224+
}
225+
};
226+
227+
ValidationTypes = {
4228

5229
isLiteral: function (part, literals) {
6230
var text = part.text.toString().toLowerCase(),
@@ -24,6 +248,13 @@ var ValidationTypes = {
24248
return !!this.complex[type];
25249
},
26250

251+
describe: function(type) {
252+
if (this.complex[type] instanceof Matcher) {
253+
return this.complex[type].toString(0);
254+
}
255+
return type;
256+
},
257+
27258
/**
28259
* Determines if the next part(s) of the given expression
29260
* are any of the given types.
@@ -72,6 +303,8 @@ var ValidationTypes = {
72303
if (result) {
73304
expression.next();
74305
}
306+
} else if (this.complex[type] instanceof Matcher) {
307+
result = this.complex[type].match(expression);
75308
} else {
76309
result = this.complex[type](expression);
77310
}

0 commit comments

Comments
 (0)