diff --git a/src/css/Properties.js b/src/css/Properties.js index 6d781d23..c781845f 100644 --- a/src/css/Properties.js +++ b/src/css/Properties.js @@ -104,8 +104,8 @@ var Properties = { "border" : " || || ", "border-bottom" : " || || ", "border-bottom-color" : " | inherit", - "border-bottom-left-radius" : "", - "border-bottom-right-radius" : "", + "border-bottom-left-radius" : " | inherit", + "border-bottom-right-radius" : " | inherit", "border-bottom-style" : "", "border-bottom-width" : "", "border-collapse" : "collapse | separate | inherit", @@ -198,8 +198,8 @@ var Properties = { "border-style" : { multi: "", max: 4 }, "border-top" : " || || ", "border-top-color" : " | inherit", - "border-top-left-radius" : "", - "border-top-right-radius" : "", + "border-top-left-radius" : " | inherit", + "border-top-right-radius" : " | inherit", "border-top-style" : "", "border-top-width" : "", "border-width" : { multi: "", max: 4 }, diff --git a/src/css/PropertyValueIterator.js b/src/css/PropertyValueIterator.js index 01cbdb9b..45439605 100644 --- a/src/css/PropertyValueIterator.js +++ b/src/css/PropertyValueIterator.js @@ -122,3 +122,11 @@ PropertyValueIterator.prototype.restore = function(){ } }; +/** + * Drops the last saved bookmark. + * @return {void} + * @method drop + */ +PropertyValueIterator.prototype.drop = function() { + this._marks.pop(); +}; diff --git a/src/css/Validation.js b/src/css/Validation.js index 038f650c..23bd4ce2 100644 --- a/src/css/Validation.js +++ b/src/css/Validation.js @@ -53,7 +53,7 @@ var Validation = { part = expression.peek(); throw new ValidationError("Expected end of value but found '" + part + "'.", part.line, part.col); } else { - throw new ValidationError("Expected (" + types + ") but found '" + value + "'.", value.line, value.col); + throw new ValidationError("Expected (" + ValidationTypes.describe(types) + ") but found '" + value + "'.", value.line, value.col); } } else if (expression.hasNext()) { part = expression.next(); diff --git a/src/css/ValidationTypes.js b/src/css/ValidationTypes.js index 33ef3eaf..d3126323 100644 --- a/src/css/ValidationTypes.js +++ b/src/css/ValidationTypes.js @@ -1,6 +1,230 @@ //This file will likely change a lot! Very experimental! -/*global ValidationError */ -var ValidationTypes = { +var ValidationTypes; + +/** + * This class implements a combinator library for matcher functions. + * The combinators are described at: + * https://developer.mozilla.org/en-US/docs/Web/CSS/Value_definition_syntax#Component_value_combinators + */ +var Matcher = function(matchFunc, toString) { + this.match = function(expression) { + // Save/restore marks to ensure that failed matches always restore + // the original location in the expression. + var result; + expression.mark(); + result = matchFunc(expression); + if (result) { + expression.drop(); + } else { + expression.restore(); + } + return result; + }; + this.toString = typeof toString === "function" ? toString : function() { + return toString; + }; +}; + +/** Precedence table of combinators. */ +Matcher.prec = { + MOD: 5, + SEQ: 4, + ANDAND: 3, + OROR: 2, + ALT: 1 +}; + +/** + * Convert a string to a matcher (parsing simple alternations), + * or do nothing if the argument is already a matcher. + */ +Matcher.cast = function(m) { + if (m instanceof Matcher) { + return m; + } + if (/ \| /.test(m)) { + return Matcher.alt.apply(Matcher, m.split(" | ")); + } + return Matcher.fromType(m); +}; + +/** + * Create a matcher for a single type. + */ +Matcher.fromType = function(type) { + return new Matcher(function(expression) { + return ValidationTypes.isType(expression, type); + }, type); +}; + +/** + * Create a matcher for one or more juxtaposed words, which all must + * occur, in the given order. + */ +Matcher.seq = function() { + var ms = Array.prototype.slice.call(arguments).map(Matcher.cast); + if (ms.length === 1) { return ms[0]; } + return new Matcher(function(expression) { + var i, result = true; + for (i = 0; result && i < ms.length; i++) { + result = ms[i].match(expression); + } + return result; + }, function(prec) { + var p = Matcher.prec.SEQ; + var s = ms.map(function(m) { return m.toString(p); }).join(" "); + if (prec > p) { s = "[ " + s + " ]"; } + return s; + }); +}; + +/** + * Create a matcher for one or more alternatives, where exactly one + * must occur. + */ +Matcher.alt = function() { + var ms = Array.prototype.slice.call(arguments).map(Matcher.cast); + if (ms.length === 1) { return ms[0]; } + return new Matcher(function(expression) { + var i, result = false; + for (i = 0; !result && i < ms.length; i++) { + result = ms[i].match(expression); + } + return result; + }, function(prec) { + var p = Matcher.prec.ALT; + var s = ms.map(function(m) { return m.toString(p); }).join(" | "); + if (prec > p) { s = "[ " + s + " ]"; } + return s; + }); +}; + +/** + * Create a matcher for two or more options. This implements the + * double bar (||) and double ampersand (&&) operators, as well as + * variants of && where some of the alternatives are optional. + */ +Matcher.many = function(required) { + var ms = Array.prototype.slice.call(arguments, 1).reduce(function(acc, v) { + if (v.expand) { + // Insert all of the options for the given complex rule as + // individual options. + acc.push.apply(acc, ValidationTypes.complex[v.expand].options); + } else { + acc.push(Matcher.cast(v)); + } + return acc; + }, []); + if (required === true) { required = ms.map(function() { return true; }); } + var result = new Matcher(function(expression) { + var seen = [], i, j; + for (i = 0; expression.hasNext() && i < ms.length; i++) { + for (j = 0; j < ms.length; j++) { + if (!seen[j] && ms[j].match(expression)) { + seen[j] = true; + break; + } + } + if (j === ms.length) { + break; + } + } + if (required === false) { + return (i > 0); + } + // Finer-grained specification of which are required. + for (i = 0; i < ms.length; i++) { + if (required[i] && !seen[i]) { + return false; + } + } + return true; + }, function(prec) { + var p = (required === false) ? Matcher.prec.OROR : Matcher.prec.ANDAND; + var s = ms.map(function(m, i) { + if (required !== false && !required[i]) { + return m.toString(Matcher.prec.MOD) + "?"; + } + return m.toString(p); + }).join(required === false ? " || " : " && "); + if (prec > p) { s = "[ " + s + " ]"; } + return s; + }); + result.options = ms; + return result; +}; + +/** + * Create a matcher for two or more options, where all options are + * mandatory but they may appear in any order. + */ +Matcher.andand = function() { + var args = Array.prototype.slice.call(arguments); + args.unshift(true); + return Matcher.many.apply(Matcher, args); +}; + +/** + * Create a matcher for two or more options, where options are + * optional and may appear in any order, but at least one must be + * present. + */ +Matcher.oror = function() { + var args = Array.prototype.slice.call(arguments); + args.unshift(false); + return Matcher.many.apply(Matcher, args); +}; + +/** Instance methods on Matchers. */ +Matcher.prototype = { + constructor: Matcher, + // These are expected to be overridden in every instance. + match: function(expression) { throw new Error("unimplemented"); }, + toString: function() { throw new Error("unimplemented"); }, + // This returns a standalone function to do the matching. + func: function() { return this.match.bind(this); }, + // Basic combinators + then: function(m) { return Matcher.seq(this, m); }, + or: function(m) { return Matcher.alt(this, m); }, + andand: function(m) { return Matcher.many(true, this, m); }, + oror: function(m) { return Matcher.many(false, this, m); }, + // Component value multipliers + star: function() { return this.braces(0, Infinity, "*"); }, + plus: function() { return this.braces(1, Infinity, "+"); }, + question: function() { return this.braces(0, 1, "?"); }, + hash: function() { + return this.braces(1, Infinity, "#", Matcher.cast(",")); + }, + braces: function(min, max, marker, optSep) { + var m = this; + if (!marker) { + marker = "{" + min + "," + max + "}"; + } + return new Matcher(function(expression) { + var result, i; + for (i = 0, result = true; result && i < max && expression.hasNext(); ) { + result = m.match(expression); + if (result) { + i++; + if (optSep && i < max && expression.hasNext()) { + expression.mark(); + result = optSep.match(expression); + if (result && !expression.hasNext()) { + // Trailing separator, boo. Back up. + expression.restore(); + break; + } else { + expression.drop(); + } + } + } + } + return (i >= min); + }, function() { return m.toString(Matcher.prec.MOD) + marker; }); + } +}; + +ValidationTypes = { isLiteral: function (part, literals) { var text = part.text.toString().toLowerCase(), @@ -24,6 +248,13 @@ var ValidationTypes = { return !!this.complex[type]; }, + describe: function(type) { + if (this.complex[type] instanceof Matcher) { + return this.complex[type].toString(0); + } + return type; + }, + /** * Determines if the next part(s) of the given expression * are any of the given types. @@ -72,6 +303,8 @@ var ValidationTypes = { if (result) { expression.next(); } + } else if (this.complex[type] instanceof Matcher) { + result = this.complex[type].match(expression); } else { result = this.complex[type](expression); } @@ -324,208 +557,48 @@ var ValidationTypes = { return result; }, - "": function(expression){ - // = [ | | auto ]{1,2} | cover | contain - var result = false, - numeric = " | | auto"; - - if (ValidationTypes.isAny(expression, "cover | contain")) { - result = true; - } else if (ValidationTypes.isAny(expression, numeric)) { - result = true; - ValidationTypes.isAny(expression, numeric); - } - - return result; - }, - - "": function(expression){ - return ValidationTypes.isAny(expression, ""); - }, - - "": function(expression) { - // || - var result = false; - - if (ValidationTypes.isType(expression, "")) { - result = true; - if (expression.hasNext()) { - result = ValidationTypes.isType(expression, ""); - } - } else if (ValidationTypes.isType(expression, "")) { - result = true; - if (expression.hasNext()) { - result = ValidationTypes.isType(expression, ""); - } - } - - return result && !expression.hasNext(); - - }, - - "": function(expression){ - var result, part, i; - for (i = 0, result = true; result && expression.hasNext(); i++) { - result = ValidationTypes.isAny(expression, " | "); - } - - if (i > 1 && !result) { - // More precise error message if we fail after the first - // parsed . - part = expression.peek(); - throw new ValidationError("Expected ( | ) but found '" + part.text + "'.", part.line, part.col); - } - - return result; - - }, - - "": function(expression){ - //repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2} - var result = false, - values = "repeat | space | round | no-repeat", - part; - - if (expression.hasNext()){ - part = expression.next(); - - if (ValidationTypes.isLiteral(part, "repeat-x | repeat-y")) { - result = true; - } else if (ValidationTypes.isLiteral(part, values)) { - result = true; - - if (expression.hasNext() && ValidationTypes.isLiteral(expression.peek(), values)) { - expression.next(); - } - } - } - - return result; - - }, - - "": function(expression) { - //inset? && [ {2,4} && ? ] - var result = false, - count = 0, - inset = false, - color = false; - - if (expression.hasNext()) { - - if (ValidationTypes.isAny(expression, "inset")){ - inset = true; - } - - if (ValidationTypes.isAny(expression, "")) { - color = true; - } - - while (ValidationTypes.isAny(expression, "") && count < 4) { - count++; - } - - - if (expression.hasNext()) { - if (!color) { - ValidationTypes.isAny(expression, ""); - } - - if (!inset) { - ValidationTypes.isAny(expression, "inset"); - } - - } - - result = (count >= 2 && count <= 4); - - } - - return result; - }, - - "": function(expression) { - //[ | ] [ | ]? - var result = false, - simple = " | | inherit"; - - if (ValidationTypes.isAny(expression, simple)){ - result = true; - ValidationTypes.isAny(expression, simple); - } - - return result; - }, - - "": function(expression) { - // http://www.w3.org/TR/2014/WD-css-flexbox-1-20140325/#flex-property - // none | [ ? || ] - // Valid syntaxes, according to https://developer.mozilla.org/en-US/docs/Web/CSS/flex#Syntax - // * none - // * - // * - // * - // * - // * - // * inherit - var part, - result = false; - if (ValidationTypes.isAny(expression, "none | inherit")) { - result = true; - } else { - if (ValidationTypes.isType(expression, "")) { - if (expression.peek()) { - if (ValidationTypes.isType(expression, "")) { - if (expression.peek()) { - result = ValidationTypes.isType(expression, ""); - } else { - result = true; - } - } else if (ValidationTypes.isType(expression, "")) { - result = expression.peek() === null; - } - } else { - result = true; - } - } else if (ValidationTypes.isType(expression, "")) { - result = true; - } - } - - if (!result) { - // Generate a more verbose error than "Expected ..." - part = expression.peek(); - throw new ValidationError("Expected (none | [ ? || ]) but found '" + expression.value.text + "'.", part.line, part.col); - } - - return result; - }, - - "": function(expression) { - // none | [ underline || overline || line-through || blink ] | inherit - var part, - result, - someOf = "[ underline || overline || line-through || blink ]", - identifiers = {}, - found; - - do { - part = expression.next(); - found = 0; - if (someOf.indexOf(part) > -1) { - if (!identifiers[part]) { - identifiers[part] = 0; - } - identifiers[part]++; - found = identifiers[part]; - } - } while (found == 1 && expression.hasNext()); - - result = found == 1 && !expression.hasNext(); - if (found === 0 && JSON.stringify(identifiers) == '{}') { - expression.previous(); - } - return result; - } + "": + // = [ | | auto ]{1,2} | cover | contain + Matcher.alt("cover", "contain", Matcher.cast(" | | auto").braces(1,2)), + + "": Matcher.cast(""), + + "": + // || + Matcher.cast("").oror(""), + + "": + // [ | ]+ + Matcher.cast(" | ").plus(), + + "": + //repeat-x | repeat-y | [repeat | space | round | no-repeat]{1,2} + Matcher.alt("repeat-x | repeat-y", Matcher.cast("repeat | space | round | no-repeat").braces(1,2)), + + "": + //inset? && [ {2,4} && ? ] + Matcher.many([true /* length is required */], + Matcher.cast("").braces(2,4), "inset", ""), + + "": + //[ | ] [ | ]? + Matcher.cast(" | ").braces(1,2), + + "": + // http://www.w3.org/TR/2014/WD-css-flexbox-1-20140325/#flex-property + // none | [ ? || ] + // Valid syntaxes, according to https://developer.mozilla.org/en-US/docs/Web/CSS/flex#Syntax + // * none + // * + // * + // * + // * + // * + // * inherit + Matcher.alt("none", "inherit", Matcher.cast("").then(Matcher.cast("").question()).oror("")), + + "": + // none | [ underline || overline || line-through || blink ] | inherit + Matcher.oror("underline", "overline", "line-through", "blink") } }; diff --git a/tests/css/Validation.js b/tests/css/Validation.js index 8d2d976c..aa5c5f80 100644 --- a/tests/css/Validation.js +++ b/tests/css/Validation.js @@ -372,7 +372,7 @@ ], invalid: { - "foo" : "Expected () but found 'foo'.", + "foo" : "Expected ( | inherit) but found 'foo'.", "5px 5px 7px" : "Expected end of value but found '7px'.", } })); @@ -388,7 +388,7 @@ ], invalid: { - "foo" : "Expected () but found 'foo'.", + "foo" : "Expected ( | inherit) but found 'foo'.", "5px 5px 7px" : "Expected end of value but found '7px'.", } })); @@ -457,7 +457,7 @@ ], invalid: { - "foo" : "Expected () but found 'foo'.", + "foo" : "Expected ( | inherit) but found 'foo'.", "5px 5px 7px" : "Expected end of value but found '7px'.", } })); @@ -472,7 +472,7 @@ ], invalid: { - "foo" : "Expected () but found 'foo'.", + "foo" : "Expected ( | inherit) but found 'foo'.", "5px 5px 7px" : "Expected end of value but found '7px'.", } })); @@ -743,7 +743,7 @@ invalid: { "circle(50% at 0 0)" : "Expected ( | none) but found 'circle(50% at 0 0)'.", "foo" : "Expected ( | none) but found 'foo'.", - "blur(30px 30px) none" : "Expected ( | ) but found 'none'." + "blur(30px 30px) none" : "Expected end of value but found 'none'." } })); @@ -764,7 +764,7 @@ ], invalid: { - "foo": "Expected (none | [ ? || ]) but found 'foo'." + "foo": "Expected (none | inherit | ? || ) but found 'foo'." } })); }); @@ -965,9 +965,9 @@ invalid: { "none underline" : "Expected end of value but found 'underline'.", - "line-through none" : "Expected (none | | inherit) but found 'line-through none'.", + "line-through none" : "Expected end of value but found 'none'.", "inherit blink" : "Expected end of value but found 'blink'.", - "overline inherit" : "Expected (none | | inherit) but found 'overline inherit'.", + "overline inherit" : "Expected end of value but found 'inherit'.", "foo" : "Expected (none | | inherit) but found 'foo'." } }));