Skip to content

Commit 4fbdfee

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 411344d commit 4fbdfee

File tree

3 files changed

+263
-3
lines changed

3 files changed

+263
-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: 254 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,249 @@
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 expression.hasNext() && 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+
* This will backtrack through even successful matches to try to
107+
* maximize the number of items matched.
108+
*/
109+
Matcher.many = function(required) {
110+
var ms = Array.prototype.slice.call(arguments, 1).reduce(function(acc, v) {
111+
if (v.expand) {
112+
// Insert all of the options for the given complex rule as
113+
// individual options.
114+
acc.push.apply(acc, ValidationTypes.complex[v.expand].options);
115+
} else {
116+
acc.push(Matcher.cast(v));
117+
}
118+
return acc;
119+
}, []);
120+
if (required === true) { required = ms.map(function() { return true; }); }
121+
var result = new Matcher(function(expression) {
122+
var seen = [], max = 0, pass = 0;
123+
var success = function(matchCount) {
124+
if (pass === 0) {
125+
max = Math.max(matchCount, max);
126+
return matchCount === ms.length;
127+
} else {
128+
return matchCount === max;
129+
}
130+
};
131+
var tryMatch = function(matchCount) {
132+
for (var i = 0; i < ms.length; i++) {
133+
if (seen[i]) { continue; }
134+
expression.mark();
135+
if (ms[i].match(expression)) {
136+
seen[i] = true;
137+
// Increase matchCount iff this was a required element
138+
// (or if all the elements are optional)
139+
if (tryMatch(matchCount + ((required === false || required[i]) ? 1 : 0))) {
140+
expression.drop();
141+
return true;
142+
}
143+
// Backtrack: try *not* matching using this rule, and
144+
// let's see if it leads to a better overall match.
145+
expression.restore();
146+
seen[i] = false;
147+
} else {
148+
expression.drop();
149+
}
150+
}
151+
return success(matchCount);
152+
};
153+
if (!tryMatch(0)) {
154+
// Couldn't get a complete match, retrace our steps to make the
155+
// match with the maximum # of required elements.
156+
pass++;
157+
tryMatch(0);
158+
}
159+
160+
if (required === false) {
161+
return (max > 0);
162+
}
163+
// Use finer-grained specification of which matchers are required.
164+
for (var i = 0; i < ms.length; i++) {
165+
if (required[i] && !seen[i]) {
166+
return false;
167+
}
168+
}
169+
return true;
170+
}, function(prec) {
171+
var p = (required === false) ? Matcher.prec.OROR : Matcher.prec.ANDAND;
172+
var s = ms.map(function(m, i) {
173+
if (required !== false && !required[i]) {
174+
return m.toString(Matcher.prec.MOD) + "?";
175+
}
176+
return m.toString(p);
177+
}).join(required === false ? " || " : " && ");
178+
if (prec > p) { s = "[ " + s + " ]"; }
179+
return s;
180+
});
181+
result.options = ms;
182+
return result;
183+
};
184+
185+
/**
186+
* Create a matcher for two or more options, where all options are
187+
* mandatory but they may appear in any order.
188+
*/
189+
Matcher.andand = function() {
190+
var args = Array.prototype.slice.call(arguments);
191+
args.unshift(true);
192+
return Matcher.many.apply(Matcher, args);
193+
};
194+
195+
/**
196+
* Create a matcher for two or more options, where options are
197+
* optional and may appear in any order, but at least one must be
198+
* present.
199+
*/
200+
Matcher.oror = function() {
201+
var args = Array.prototype.slice.call(arguments);
202+
args.unshift(false);
203+
return Matcher.many.apply(Matcher, args);
204+
};
205+
206+
/** Instance methods on Matchers. */
207+
Matcher.prototype = {
208+
constructor: Matcher,
209+
// These are expected to be overridden in every instance.
210+
match: function(expression) { throw new Error("unimplemented"); },
211+
toString: function() { throw new Error("unimplemented"); },
212+
// This returns a standalone function to do the matching.
213+
func: function() { return this.match.bind(this); },
214+
// Basic combinators
215+
then: function(m) { return Matcher.seq(this, m); },
216+
or: function(m) { return Matcher.alt(this, m); },
217+
andand: function(m) { return Matcher.many(true, this, m); },
218+
oror: function(m) { return Matcher.many(false, this, m); },
219+
// Component value multipliers
220+
star: function() { return this.braces(0, Infinity, "*"); },
221+
plus: function() { return this.braces(1, Infinity, "+"); },
222+
question: function() { return this.braces(0, 1, "?"); },
223+
hash: function() {
224+
return this.braces(1, Infinity, "#", Matcher.cast(","));
225+
},
226+
braces: function(min, max, marker, optSep) {
227+
var m1 = this, m2 = optSep ? optSep.then(this) : this;
228+
if (!marker) {
229+
marker = "{" + min + "," + max + "}";
230+
}
231+
return new Matcher(function(expression) {
232+
var result = true, i;
233+
for (i = 0; i < max; i++) {
234+
if (i > 0 && optSep) {
235+
result = m2.match(expression);
236+
} else {
237+
result = m1.match(expression);
238+
}
239+
if (!result) { break; }
240+
}
241+
return (i >= min);
242+
}, function() { return m1.toString(Matcher.prec.MOD) + marker; });
243+
}
244+
};
245+
246+
ValidationTypes = {
4247

5248
isLiteral: function (part, literals) {
6249
var text = part.text.toString().toLowerCase(),
@@ -24,6 +267,13 @@ var ValidationTypes = {
24267
return !!this.complex[type];
25268
},
26269

270+
describe: function(type) {
271+
if (this.complex[type] instanceof Matcher) {
272+
return this.complex[type].toString(0);
273+
}
274+
return type;
275+
},
276+
27277
/**
28278
* Determines if the next part(s) of the given expression
29279
* are any of the given types.
@@ -72,6 +322,8 @@ var ValidationTypes = {
72322
if (result) {
73323
expression.next();
74324
}
325+
} else if (this.complex[type] instanceof Matcher) {
326+
result = this.complex[type].match(expression);
75327
} else {
76328
result = this.complex[type](expression);
77329
}

0 commit comments

Comments
 (0)