From 4fbdfee7332566258627e4b7093c12ba87f455d1 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Thu, 4 Feb 2016 17:37:31 -0500 Subject: [PATCH 1/4] 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. --- src/css/PropertyValueIterator.js | 8 + src/css/Validation.js | 2 +- src/css/ValidationTypes.js | 256 ++++++++++++++++++++++++++++++- 3 files changed, 263 insertions(+), 3 deletions(-) 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 dbb5725b..c09a37f0 100644 --- a/src/css/ValidationTypes.js +++ b/src/css/ValidationTypes.js @@ -1,6 +1,249 @@ //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 expression.hasNext() && 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. + * This will backtrack through even successful matches to try to + * maximize the number of items matched. + */ +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 = [], max = 0, pass = 0; + var success = function(matchCount) { + if (pass === 0) { + max = Math.max(matchCount, max); + return matchCount === ms.length; + } else { + return matchCount === max; + } + }; + var tryMatch = function(matchCount) { + for (var i = 0; i < ms.length; i++) { + if (seen[i]) { continue; } + expression.mark(); + if (ms[i].match(expression)) { + seen[i] = true; + // Increase matchCount iff this was a required element + // (or if all the elements are optional) + if (tryMatch(matchCount + ((required === false || required[i]) ? 1 : 0))) { + expression.drop(); + return true; + } + // Backtrack: try *not* matching using this rule, and + // let's see if it leads to a better overall match. + expression.restore(); + seen[i] = false; + } else { + expression.drop(); + } + } + return success(matchCount); + }; + if (!tryMatch(0)) { + // Couldn't get a complete match, retrace our steps to make the + // match with the maximum # of required elements. + pass++; + tryMatch(0); + } + + if (required === false) { + return (max > 0); + } + // Use finer-grained specification of which matchers are required. + for (var 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 m1 = this, m2 = optSep ? optSep.then(this) : this; + if (!marker) { + marker = "{" + min + "," + max + "}"; + } + return new Matcher(function(expression) { + var result = true, i; + for (i = 0; i < max; i++) { + if (i > 0 && optSep) { + result = m2.match(expression); + } else { + result = m1.match(expression); + } + if (!result) { break; } + } + return (i >= min); + }, function() { return m1.toString(Matcher.prec.MOD) + marker; }); + } +}; + +ValidationTypes = { isLiteral: function (part, literals) { var text = part.text.toString().toLowerCase(), @@ -24,6 +267,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 +322,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); } From c7fef82b145d148a99c09f92a897b168f0f0fcd4 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Fri, 5 Feb 2016 10:48:15 -0500 Subject: [PATCH 2/4] Simplify ValidationTypes using Matcher framework. As a side-effect, we also pushed handling the `inherit` option out of most ValidationTypes and moved it into the specification in the Properties table. I expect to refactor this further in the future to make handling of inherit/initial/unset automatic and implicit. --- src/css/Properties.js | 12 +- src/css/ValidationTypes.js | 340 ++++++++----------------------------- tests/css/Validation.js | 30 ++-- 3 files changed, 92 insertions(+), 290 deletions(-) diff --git a/src/css/Properties.js b/src/css/Properties.js index 6d781d23..e38d135c 100644 --- a/src/css/Properties.js +++ b/src/css/Properties.js @@ -90,7 +90,7 @@ var Properties = { "background-color" : " | inherit", "background-image" : { multi: "", comma: true }, "background-origin" : { multi: "", comma: true }, - "background-position" : { multi: "", comma: true }, + "background-position" : " | inherit", "background-repeat" : { multi: "", comma: true }, "background-size" : { multi: "", comma: true }, "baseline-shift" : "baseline | sub | super | | ", @@ -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 }, @@ -420,7 +420,7 @@ var Properties = { //O "object-fit" : "fill | contain | cover | none | scale-down", - "object-position" : "", + "object-position" : " | inherit", "opacity" : " | inherit", "order" : "", "-webkit-order" : "", diff --git a/src/css/ValidationTypes.js b/src/css/ValidationTypes.js index c09a37f0..8937cffe 100644 --- a/src/css/ValidationTypes.js +++ b/src/css/ValidationTypes.js @@ -509,275 +509,75 @@ ValidationTypes = { complex: { - "": function(expression){ - var result = false, - numeric = " | ", - xDir = "left | right", - yDir = "top | bottom", - count = 0; - - while (expression.peek(count) && expression.peek(count).text !== ",") { - count++; - } - -/* - = [ - [ left | center | right | top | bottom | | ] -| - [ left | center | right | | ] - [ top | center | bottom | | ] -| - [ center | [ left | right ] [ | ]? ] && - [ center | [ top | bottom ] [ | ]? ] -] -*/ - - if (count < 3) { - if (ValidationTypes.isAny(expression, xDir + " | center | " + numeric)) { - result = true; - ValidationTypes.isAny(expression, yDir + " | center | " + numeric); - } else if (ValidationTypes.isAny(expression, yDir)) { - result = true; - ValidationTypes.isAny(expression, xDir + " | center"); - } - } else { - if (ValidationTypes.isAny(expression, xDir)) { - if (ValidationTypes.isAny(expression, yDir)) { - result = true; - ValidationTypes.isAny(expression, numeric); - } else if (ValidationTypes.isAny(expression, numeric)) { - if (ValidationTypes.isAny(expression, yDir)) { - result = true; - ValidationTypes.isAny(expression, numeric); - } else if (ValidationTypes.isAny(expression, "center")) { - result = true; - } - } - } else if (ValidationTypes.isAny(expression, yDir)) { - if (ValidationTypes.isAny(expression, xDir)) { - result = true; - ValidationTypes.isAny(expression, numeric); - } else if (ValidationTypes.isAny(expression, numeric)) { - if (ValidationTypes.isAny(expression, xDir)) { - result = true; - ValidationTypes.isAny(expression, numeric); - } else if (ValidationTypes.isAny(expression, "center")) { - result = true; - } - } - } else if (ValidationTypes.isAny(expression, "center")) { - if (ValidationTypes.isAny(expression, xDir + " | " + yDir)) { - result = true; - ValidationTypes.isAny(expression, numeric); - } - } - } - - 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; - } + "": Matcher.cast("").hash(), + + "": + // = [ | | auto ]{1,2} | cover | contain + Matcher.alt("cover", "contain", Matcher.cast(" | | auto").braces(1,2)), + + "": Matcher.cast(""), + + "": + // || + Matcher.cast("").oror(""), + + "": + // [ | ]+ + Matcher.cast(" | ").plus(), + + "": + // = [ + // [ left | center | right | top | bottom | | ] + // | + // [ left | center | right | | ] + // [ top | center | bottom | | ] + // | + // [ center | [ left | right ] [ | ]? ] && + // [ center | [ top | bottom ] [ | ]? ] + //] + Matcher.alt( + // Because `alt` combinator is ordered, we need to test these + // in order from longest possible match to shortest. + Matcher.andand( + Matcher.cast("center").or( + Matcher.seq("left | right", + Matcher.cast(" | ").question())), + Matcher.cast("center").or( + Matcher.seq("top | bottom", + Matcher.cast(" | ").question()))), + Matcher.seq("left | center | right | | ", + "top | center | bottom | | "), + "left | center | right | top | bottom | | " + ), + + "": + //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..7001b7ae 100644 --- a/tests/css/Validation.js +++ b/tests/css/Validation.js @@ -197,6 +197,8 @@ "right top 5%", "top 3em center", "center top 3em", + "right 3% center", + "center right 3%", "top 3em right 10%", "top, bottom", "left 10px, left 10px", @@ -204,10 +206,10 @@ ], invalid: { - "foo" : "Expected () but found 'foo'.", + "foo" : "Expected ( | inherit) but found 'foo'.", "10% left" : "Expected end of value but found 'left'.", - "left center right" : "Expected end of value but found 'center'.", - "center 3em right 10%": "Expected end of value but found '3em'.", + "left center right" : "Expected end of value but found 'right'.", + "center 3em right 10%": "Expected end of value but found 'right'.", } })); @@ -372,7 +374,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 +390,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 +459,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 +474,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 +745,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 +766,7 @@ ], invalid: { - "foo": "Expected (none | [ ? || ]) but found 'foo'." + "foo": "Expected (none | inherit | ? || ) but found 'foo'." } })); }); @@ -965,9 +967,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'." } })); @@ -1026,10 +1028,10 @@ ], invalid: { - "foo" : "Expected () but found 'foo'.", + "foo" : "Expected ( | inherit) but found 'foo'.", "10% left" : "Expected end of value but found 'left'.", - "left center right" : "Expected end of value but found 'center'.", - "center 3em right 10%": "Expected end of value but found '3em'.", + "left center right" : "Expected end of value but found 'right'.", + "center 3em right 10%": "Expected end of value but found 'right'.", "top, bottom" : "Expected end of value but found ','." } })); From 42cee205e03eb109cc0a058db34652b19ee478a6 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Fri, 5 Feb 2016 00:56:49 -0500 Subject: [PATCH 3/4] Allow matching functions as literals. I've updated several simple ValidationType functions to show how this can be used to simplify common matchers. It will also be used in a follow-up patch to validate the `font-variant-alternates` property which involves a large number of CSS function options. --- src/css/ValidationTypes.js | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/css/ValidationTypes.js b/src/css/ValidationTypes.js index 8937cffe..fb6ac0eb 100644 --- a/src/css/ValidationTypes.js +++ b/src/css/ValidationTypes.js @@ -251,7 +251,10 @@ ValidationTypes = { i, len, found = false; for (i=0,len=args.length; i < len && !found; i++){ - if (text === args[i].toLowerCase()){ + if (args[i].slice(-2) === "()"){ + found = (part.type === "function" && + part.name === args[i].slice(0, -2)); + } else if (text === args[i].toLowerCase()){ found = true; } } @@ -344,7 +347,7 @@ ValidationTypes = { }, "": function(part){ - return part.type === "function" && part.name === "attr"; + return ValidationTypes.isLiteral(part, "attr()"); }, "": function(part){ @@ -360,7 +363,7 @@ ValidationTypes = { }, "": function(part){ - return part.type === "function" && part.name === "content"; + return ValidationTypes.isLiteral(part, "content()"); }, "": function(part){ @@ -438,7 +441,7 @@ ValidationTypes = { }, "": function(part){ - return part.type === "function" && (part.name === "rect" || part.name === "inset-rect"); + return ValidationTypes.isLiteral(part, "rect() | inset-rect()"); }, "": function(part){ @@ -446,9 +449,7 @@ ValidationTypes = { // circle() = circle( []? [at ]? ) // ellipse() = ellipse( [{2}]? [at ]? ) // polygon() = polygon( [,]? [ ]# ) - return part.type === "function" && ( - part.name === "inset" || part.name === "circle" || part.name === "ellipse" || part.name === "polygon" - ); + return ValidationTypes.isLiteral(part, "inset() | circle() | ellipse() | polygon()"); }, "": function(part) { @@ -492,18 +493,11 @@ ValidationTypes = { }, "": function(part){ - return part.type === "function" && ( - part.name === 'blur' || - part.name === 'brightness' || - part.name === 'contrast' || - part.name === 'custom' || // Not actually in formal spec. - part.name === 'drop-shadow' || - part.name === 'grayscale' || - part.name === 'hue-rotate' || - part.name === 'invert' || - part.name === 'opacity' || - part.name === 'saturate' || - part.name === 'sepia'); + // custom() isn't actually in the spec + return ValidationTypes.isLiteral( + part, "blur() | brightness() | contrast() | custom() | " + + "drop-shadow() | grayscale() | hue-rotate() | invert() | " + + "opacity() | saturate() | sepia()"); } }, From 30b3261566d86bbcee33d993a880dbd3ab3fbad0 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Fri, 5 Feb 2016 15:59:45 -0500 Subject: [PATCH 4/4] Validate fill, stroke, and opacity properties. Originally based on PR #162 by Onno van der Zee . Fixes: #162 --- src/css/Properties.js | 14 ++++- src/css/ValidationTypes.js | 50 ++++++++++++++++ tests/css/Validation.js | 118 ++++++++++++++++++++++++++++++++++++- 3 files changed, 179 insertions(+), 3 deletions(-) diff --git a/src/css/Properties.js b/src/css/Properties.js index e38d135c..6cad177f 100644 --- a/src/css/Properties.js +++ b/src/css/Properties.js @@ -285,7 +285,9 @@ var Properties = { "empty-cells" : "show | hide | inherit", //F - "fill" : " | inherit", + "fill" : " | inherit", + "fill-opacity" : " | inherit", + "fill-rule" : "nonzero | evenodd | inherit", "filter" : " | none", "fit" : "fill | hidden | meet | slice", "fit-position" : 1, @@ -421,7 +423,7 @@ var Properties = { //O "object-fit" : "fill | contain | cover | none | scale-down", "object-position" : " | inherit", - "opacity" : " | inherit", + "opacity" : " | inherit", "order" : "", "-webkit-order" : "", "orphans" : " | inherit", @@ -489,6 +491,14 @@ var Properties = { "src" : 1, "stress" : 1, "string-set" : 1, + "stroke" : " | inherit", + "stroke-dasharray" : "none | | inherit", + "stroke-dashoffset" : " | | inherit", + "stroke-linecap" : "butt | round | square | inherit", + "stroke-linejoin" : "miter | round | bevel | inherit", + "stroke-miterlimit" : " | inherit", + "stroke-opacity" : " | inherit", + "stroke-width" : " | | inherit", "table-layout" : "auto | fixed | inherit", "tab-size" : " | ", diff --git a/src/css/ValidationTypes.js b/src/css/ValidationTypes.js index fb6ac0eb..3a161662 100644 --- a/src/css/ValidationTypes.js +++ b/src/css/ValidationTypes.js @@ -387,10 +387,39 @@ ValidationTypes = { return part.type === "color" || part == "transparent" || part == "currentColor"; }, + // The SVG spec doesn't include "currentColor" or "transparent" as a color. + "": function(part) { + return part.type === "color"; + }, + + "": function(part){ + /* ex.: + https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/local + icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00) + cielab(62.253188, 23.950124, 48.410653) + cielch(62.253188, 54.011108, 63.677091) + icc-color(FooColors, Sandy23C) + http://www.w3.org/TR/2009/WD-SVGColor12-20091001/#iccnamedcolor + ~"icc-color(" name (comma-wsp number)+ ")" + ~"icc-named-color(" name comma-wsp namedColor ")" + ~"cielab(" lightness comma-wsp a-value comma-wsp b-value ")" + ~"cielchab(" lightness comma-wsp chroma comma-wsp hue ")" + */ + return ValidationTypes.isLiteral(part, "cielab() | cielch() | cielchab() | icc-color() | icc-named-color()"); + }, + "": function(part){ return part.type === "number" || this[""](part); }, + "": function(part){ + return this[""](part) && part.value >= 1; + }, + + "": function(part){ + return this[""](part) && part.value >= 0 && part.value <= 1; + }, + "": function(part){ return part.type === "integer"; }, @@ -515,10 +544,31 @@ ValidationTypes = { // || Matcher.cast("").oror(""), + "": + // "list of comma and/or white space separated s and + // s". We use to enforce the + // nonnegative constraint. + Matcher.cast("") + .braces(1, Infinity, "#", Matcher.cast(",").question()), + "": // [ | ]+ Matcher.cast(" | ").plus(), + "": + // none | currentColor | []? | + // [ none | currentColor | []? ]? + + // Note that here is "as defined in the SVG spec", which + // is more restrictive that the defined in the CSS spec. + Matcher.alt("", + Matcher.seq("", Matcher.cast("").question())), + // Helper definition for above. + "": + Matcher.alt("none", "currentColor", + Matcher.seq("", + Matcher.cast("").question())), + "": // = [ // [ left | center | right | top | bottom | | ] diff --git a/tests/css/Validation.js b/tests/css/Validation.js index 7001b7ae..8b2d41e5 100644 --- a/tests/css/Validation.js +++ b/tests/css/Validation.js @@ -729,6 +729,46 @@ } })); + // test + suite.add(new ValidationTestCase({ + property: "fill", + + valid: [ + "url('myGradient')", + "url('myGradient') darkred", + "url('myGradient') darkred icc-color(myCmykDarkRed)", + "currentColor", + "darkred icc-color(myCmykDarkRed)", + "none", + "inherit" + ], + + invalid: { + "url('myGradient') inherit" : "Expected end of value but found 'inherit'.", + "url('myGradient') icc-color(myCmykDarkRed)" : "Expected end of value but found 'icc-color(myCmykDarkRed)'.", + "currentColor icc-color(myCmykDarkRed)" : "Expected end of value but found 'icc-color(myCmykDarkRed)'.", + "icc-color(myCmykDarkRed) darkred" : "Expected ( | inherit) but found 'icc-color(myCmykDarkRed) darkred'.", + "icc-color(myCmykDarkRed)" : "Expected ( | inherit) but found 'icc-color(myCmykDarkRed)'.", + "icc-color(myCmykDarkRed) inherit" : "Expected ( | inherit) but found 'icc-color(myCmykDarkRed) inherit'.", + "inherit icc-color(myCmykDarkRed)" : "Expected end of value but found 'icc-color(myCmykDarkRed)'.", + "none inherit" : "Expected end of value but found 'inherit'." + } + })); + + suite.add(new ValidationTestCase({ + property: "fill-rule", + + valid: [ + "nonzero", + "evenodd", + "inherit" + ], + + invalid: { + "foo" : "Expected (nonzero | evenodd | inherit) but found 'foo'." + } + })); + suite.add(new ValidationTestCase({ property: "filter", @@ -1040,11 +1080,15 @@ property: "opacity", valid: [ + "0", + "0.5", "1" ], invalid: { - "foo" : "Expected ( | inherit) but found 'foo'." + "-0.75" : "Expected ( | inherit) but found '-0.75'.", + "12" : "Expected ( | inherit) but found '12'.", + "foo" : "Expected ( | inherit) but found 'foo'." } })); @@ -1070,6 +1114,78 @@ } })); + suite.add(new ValidationTestCase({ + property: "stroke-dasharray", + + valid: [ + "0", + "4", + "20px", + "20px 40px 30px", + "20px, 40px, 30px", + "calc(1px + 2px) calc(3px + 1em)", + "none", + "inherit" + ], + + invalid: { + "-20px" : "Expected (none | | inherit) but found '-20px'.", + "20px," : "Expected end of value but found ','.", + "20px, -20px": "Expected end of value but found ','.", + "auto" : "Expected (none | | inherit) but found 'auto'." + } + })); + + suite.add(new ValidationTestCase({ + property: "stroke-linecap", + + valid: [ + "butt", + "round", + "square", + "inherit" + ], + + invalid: { + "auto" : "Expected (butt | round | square | inherit) but found 'auto'.", + "none" : "Expected (butt | round | square | inherit) but found 'none'." + } + })); + + suite.add(new ValidationTestCase({ + property: "stroke-linejoin", + + valid: [ + "miter", + "round", + "bevel", + "inherit" + ], + + invalid: { + "auto" : "Expected (miter | round | bevel | inherit) but found 'auto'.", + "none" : "Expected (miter | round | bevel | inherit) but found 'none'." + } + })); + + suite.add(new ValidationTestCase({ + property: "stroke-miterlimit", + + valid: [ + "1", + "1.4", + "20", + "10", + "inherit" + ], + + invalid: { + "-10" : "Expected ( | inherit) but found '-10'.", + "0.5" : "Expected ( | inherit) but found '0.5'.", + "foo" : "Expected ( | inherit) but found 'foo'." + } + })); + suite.add(new ValidationTestCase({ property: "-ms-touch-action",