Skip to content

Commit af9ed3f

Browse files
committed
Merge pull request #203 from cscott/stringify
Represent most property validators as strings matching the CSS spec formal syntax
2 parents 43ea06c + 3e8bc4e commit af9ed3f

File tree

7 files changed

+898
-1134
lines changed

7 files changed

+898
-1134
lines changed

src/css/Matcher.js

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
/*global ValidationTypes, StringReader*/
2+
3+
/**
4+
* This class implements a combinator library for matcher functions.
5+
* The combinators are described at:
6+
* https://developer.mozilla.org/en-US/docs/Web/CSS/Value_definition_syntax#Component_value_combinators
7+
*/
8+
function Matcher(matchFunc, toString) {
9+
this.match = function(expression) {
10+
// Save/restore marks to ensure that failed matches always restore
11+
// the original location in the expression.
12+
var result;
13+
expression.mark();
14+
result = matchFunc(expression);
15+
if (result) {
16+
expression.drop();
17+
} else {
18+
expression.restore();
19+
}
20+
return result;
21+
};
22+
this.toString = typeof toString === "function" ? toString : function() {
23+
return toString;
24+
};
25+
}
26+
27+
/** Precedence table of combinators. */
28+
Matcher.prec = {
29+
MOD: 5,
30+
SEQ: 4,
31+
ANDAND: 3,
32+
OROR: 2,
33+
ALT: 1
34+
};
35+
36+
/** Simple recursive-descent grammar to build matchers from strings. */
37+
Matcher.parse = function(str) {
38+
var reader, eat, expr, oror, andand, seq, mod, term, result;
39+
reader = new StringReader(str);
40+
eat = function(matcher) {
41+
var result = reader.readMatch(matcher);
42+
if (result === null) {
43+
throw new SyntaxError(
44+
"Expected "+matcher, reader.getLine(), reader.getCol());
45+
}
46+
return result;
47+
};
48+
expr = function() {
49+
// expr = oror (" | " oror)*
50+
var m = [ oror() ];
51+
while (reader.readMatch(" | ") !== null) {
52+
m.push(oror());
53+
}
54+
return m.length === 1 ? m[0] : Matcher.alt.apply(Matcher, m);
55+
};
56+
oror = function() {
57+
// oror = andand ( " || " andand)*
58+
var m = [ andand() ];
59+
while (reader.readMatch(" || ") !== null) {
60+
m.push(andand());
61+
}
62+
return m.length === 1 ? m[0] : Matcher.oror.apply(Matcher, m);
63+
};
64+
andand = function() {
65+
// andand = seq ( " && " seq)*
66+
var m = [ seq() ];
67+
while (reader.readMatch(" && ") !== null) {
68+
m.push(seq());
69+
}
70+
return m.length === 1 ? m[0] : Matcher.andand.apply(Matcher, m);
71+
};
72+
seq = function() {
73+
// seq = mod ( " " mod)*
74+
var m = [ mod() ];
75+
while (reader.readMatch(/^ (?![&|\]])/) !== null) {
76+
m.push(mod());
77+
}
78+
return m.length === 1 ? m[0] : Matcher.seq.apply(Matcher, m);
79+
};
80+
mod = function() {
81+
// mod = term ( "?" | "*" | "+" | "#" | "{<num>,<num>}" )?
82+
var m = term();
83+
if (reader.readMatch("?") !== null) {
84+
return m.question();
85+
} else if (reader.readMatch("*") !== null) {
86+
return m.star();
87+
} else if (reader.readMatch("+") !== null) {
88+
return m.plus();
89+
} else if (reader.readMatch("#") !== null) {
90+
return m.hash();
91+
} else if (reader.readMatch(/^\{\s*/) !== null) {
92+
var min = eat(/^\d+/);
93+
eat(/^\s*,\s*/);
94+
var max = eat(/^\d+/);
95+
eat(/^\s*\}/);
96+
return m.braces(+min, +max);
97+
}
98+
return m;
99+
};
100+
term = function() {
101+
// term = <nt> | literal | "[ " expression " ]"
102+
if (reader.readMatch("[ ") !== null) {
103+
var m = expr();
104+
eat(" ]");
105+
return m;
106+
}
107+
return Matcher.fromType(eat(/^[^ ?*+#{]+/));
108+
};
109+
result = expr();
110+
if (!reader.eof()) {
111+
throw new SyntaxError(
112+
"Expected end of string", reader.getLine(), reader.getCol());
113+
}
114+
return result;
115+
};
116+
117+
/**
118+
* Convert a string to a matcher (parsing simple alternations),
119+
* or do nothing if the argument is already a matcher.
120+
*/
121+
Matcher.cast = function(m) {
122+
if (m instanceof Matcher) {
123+
return m;
124+
}
125+
return Matcher.parse(m);
126+
};
127+
128+
/**
129+
* Create a matcher for a single type.
130+
*/
131+
Matcher.fromType = function(type) {
132+
return new Matcher(function(expression) {
133+
return expression.hasNext() && ValidationTypes.isType(expression, type);
134+
}, type);
135+
};
136+
137+
/**
138+
* Create a matcher for one or more juxtaposed words, which all must
139+
* occur, in the given order.
140+
*/
141+
Matcher.seq = function() {
142+
var ms = Array.prototype.slice.call(arguments).map(Matcher.cast);
143+
if (ms.length === 1) { return ms[0]; }
144+
return new Matcher(function(expression) {
145+
var i, result = true;
146+
for (i = 0; result && i < ms.length; i++) {
147+
result = ms[i].match(expression);
148+
}
149+
return result;
150+
}, function(prec) {
151+
var p = Matcher.prec.SEQ;
152+
var s = ms.map(function(m) { return m.toString(p); }).join(" ");
153+
if (prec > p) { s = "[ " + s + " ]"; }
154+
return s;
155+
});
156+
};
157+
158+
/**
159+
* Create a matcher for one or more alternatives, where exactly one
160+
* must occur.
161+
*/
162+
Matcher.alt = function() {
163+
var ms = Array.prototype.slice.call(arguments).map(Matcher.cast);
164+
if (ms.length === 1) { return ms[0]; }
165+
return new Matcher(function(expression) {
166+
var i, result = false;
167+
for (i = 0; !result && i < ms.length; i++) {
168+
result = ms[i].match(expression);
169+
}
170+
return result;
171+
}, function(prec) {
172+
var p = Matcher.prec.ALT;
173+
var s = ms.map(function(m) { return m.toString(p); }).join(" | ");
174+
if (prec > p) { s = "[ " + s + " ]"; }
175+
return s;
176+
});
177+
};
178+
179+
/**
180+
* Create a matcher for two or more options. This implements the
181+
* double bar (||) and double ampersand (&&) operators, as well as
182+
* variants of && where some of the alternatives are optional.
183+
* This will backtrack through even successful matches to try to
184+
* maximize the number of items matched.
185+
*/
186+
Matcher.many = function(required) {
187+
var ms = Array.prototype.slice.call(arguments, 1).reduce(function(acc, v) {
188+
if (v.expand) {
189+
// Insert all of the options for the given complex rule as
190+
// individual options.
191+
acc.push.apply(acc, ValidationTypes.complex[v.expand].options);
192+
} else {
193+
acc.push(Matcher.cast(v));
194+
}
195+
return acc;
196+
}, []);
197+
if (required === true) { required = ms.map(function() { return true; }); }
198+
var result = new Matcher(function(expression) {
199+
var seen = [], max = 0, pass = 0;
200+
var success = function(matchCount) {
201+
if (pass === 0) {
202+
max = Math.max(matchCount, max);
203+
return matchCount === ms.length;
204+
} else {
205+
return matchCount === max;
206+
}
207+
};
208+
var tryMatch = function(matchCount) {
209+
for (var i = 0; i < ms.length; i++) {
210+
if (seen[i]) { continue; }
211+
expression.mark();
212+
if (ms[i].match(expression)) {
213+
seen[i] = true;
214+
// Increase matchCount iff this was a required element
215+
// (or if all the elements are optional)
216+
if (tryMatch(matchCount + ((required === false || required[i]) ? 1 : 0))) {
217+
expression.drop();
218+
return true;
219+
}
220+
// Backtrack: try *not* matching using this rule, and
221+
// let's see if it leads to a better overall match.
222+
expression.restore();
223+
seen[i] = false;
224+
} else {
225+
expression.drop();
226+
}
227+
}
228+
return success(matchCount);
229+
};
230+
if (!tryMatch(0)) {
231+
// Couldn't get a complete match, retrace our steps to make the
232+
// match with the maximum # of required elements.
233+
pass++;
234+
tryMatch(0);
235+
}
236+
237+
if (required === false) {
238+
return (max > 0);
239+
}
240+
// Use finer-grained specification of which matchers are required.
241+
for (var i = 0; i < ms.length; i++) {
242+
if (required[i] && !seen[i]) {
243+
return false;
244+
}
245+
}
246+
return true;
247+
}, function(prec) {
248+
var p = (required === false) ? Matcher.prec.OROR : Matcher.prec.ANDAND;
249+
var s = ms.map(function(m, i) {
250+
if (required !== false && !required[i]) {
251+
return m.toString(Matcher.prec.MOD) + "?";
252+
}
253+
return m.toString(p);
254+
}).join(required === false ? " || " : " && ");
255+
if (prec > p) { s = "[ " + s + " ]"; }
256+
return s;
257+
});
258+
result.options = ms;
259+
return result;
260+
};
261+
262+
/**
263+
* Create a matcher for two or more options, where all options are
264+
* mandatory but they may appear in any order.
265+
*/
266+
Matcher.andand = function() {
267+
var args = Array.prototype.slice.call(arguments);
268+
args.unshift(true);
269+
return Matcher.many.apply(Matcher, args);
270+
};
271+
272+
/**
273+
* Create a matcher for two or more options, where options are
274+
* optional and may appear in any order, but at least one must be
275+
* present.
276+
*/
277+
Matcher.oror = function() {
278+
var args = Array.prototype.slice.call(arguments);
279+
args.unshift(false);
280+
return Matcher.many.apply(Matcher, args);
281+
};
282+
283+
/** Instance methods on Matchers. */
284+
Matcher.prototype = {
285+
constructor: Matcher,
286+
// These are expected to be overridden in every instance.
287+
match: function(expression) { throw new Error("unimplemented"); },
288+
toString: function() { throw new Error("unimplemented"); },
289+
// This returns a standalone function to do the matching.
290+
func: function() { return this.match.bind(this); },
291+
// Basic combinators
292+
then: function(m) { return Matcher.seq(this, m); },
293+
or: function(m) { return Matcher.alt(this, m); },
294+
andand: function(m) { return Matcher.many(true, this, m); },
295+
oror: function(m) { return Matcher.many(false, this, m); },
296+
// Component value multipliers
297+
star: function() { return this.braces(0, Infinity, "*"); },
298+
plus: function() { return this.braces(1, Infinity, "+"); },
299+
question: function() { return this.braces(0, 1, "?"); },
300+
hash: function() {
301+
return this.braces(1, Infinity, "#", Matcher.cast(","));
302+
},
303+
braces: function(min, max, marker, optSep) {
304+
var m1 = this, m2 = optSep ? optSep.then(this) : this;
305+
if (!marker) {
306+
marker = "{" + min + "," + max + "}";
307+
}
308+
return new Matcher(function(expression) {
309+
var result = true, i;
310+
for (i = 0; i < max; i++) {
311+
if (i > 0 && optSep) {
312+
result = m2.match(expression);
313+
} else {
314+
result = m1.match(expression);
315+
}
316+
if (!result) { break; }
317+
}
318+
return (i >= min);
319+
}, function() { return m1.toString(Matcher.prec.MOD) + marker; });
320+
}
321+
};

0 commit comments

Comments
 (0)