From dbdcf01b4265cff7f8c808ae0810d14e35fafae8 Mon Sep 17 00:00:00 2001 From: Ivan S Date: Sun, 11 Sep 2016 23:03:17 +0300 Subject: [PATCH] add clip-path validator --- CHANGELOG.md | 1 + README.md | 4 +- packages/css-values/index.js | 198 +++++++++++++++++++++++++-- packages/css-values/test.js | 53 ++++++- src/completed.js | 2 + src/data.js | 1 + src/fixtures/basicShape.js | 28 ++++ src/fixtures/clipPathProperty.js | 16 +++ src/fixtures/geometryBox.js | 4 +- src/fixtures/index.js | 1 + src/generators/property.js | 3 +- src/validators/index.js | 1 + src/validators/isBasicShape.js | 169 +++++++++++++++++++++++ src/validators/isClipPathProperty.js | 9 ++ 14 files changed, 478 insertions(+), 12 deletions(-) create mode 100644 src/fixtures/basicShape.js create mode 100644 src/fixtures/clipPathProperty.js create mode 100644 src/validators/isBasicShape.js create mode 100644 src/validators/isClipPathProperty.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9813817..72b744e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Head +* Adds `` validator. * Adds `` validator. * Adds support for the non-standard -webkit-mask-position property. * Adds support for the non-standard -webkit-mask-composite property. diff --git a/README.md b/README.md index 72f65f3..0e6f1cd 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ valid, even though they are not to specification. ## Property support -We support 252 of 361 CSS properties (69.81%). +We support 254 of 361 CSS properties (70.36%). * `-ms-overflow-style` * `-moz-appearance` @@ -56,6 +56,7 @@ We support 252 of 361 CSS properties (69.81%). * `-webkit-border-before-color` * `-webkit-border-before-style` * `-webkit-border-before-width` +* `-webkit-clip-path` * `-webkit-mask-attachment` * `-webkit-mask-composite` * `-webkit-mask-position` @@ -141,6 +142,7 @@ We support 252 of 361 CSS properties (69.81%). * `break-inside` * `caption-side` * `clear` +* `clip-path` * `color` * `column-count` * `column-fill` diff --git a/packages/css-values/index.js b/packages/css-values/index.js index 0f32ad1..286f356 100644 --- a/packages/css-values/index.js +++ b/packages/css-values/index.js @@ -881,6 +881,172 @@ function isBgSize(valueParserAST) { return getArguments(valueParserAST).every(validateGroup$1); } +var geometryBoxes = ['margin-box', 'fill-box', 'stroke-box', 'view-box']; + +var nonStandardKeywords = ['content', 'padding', 'border']; + +var isGeometryBox = (function (node) { + return isBox(node) || isKeyword(node, geometryBoxes) || isKeyword(node, nonStandardKeywords); +}); + +function isFillRule(node) { + return isKeyword(node, ['nonzero', 'evenodd']); +} + +function isShapeRadius(node) { + return isLengthPercentage(node) || isKeyword(node, ['closest-side', 'farthest-side']); +} + +function isInset(node) { + if (!isFunction(node, 'inset')) { + return false; + } + var valid = true; + walk(node.nodes, function (child, index) { + var even = isEven(index); + if (!even && !isSpace(child)) { + valid = false; + return false; + } + if (even && !isLengthPercentage(child)) { + valid = false; + return false; + } + }); + return valid; +} + +function isCircle(node) { + if (!isFunction(node, 'circle')) { + return false; + } + var valid = true; + var atIdx = 0; + var skip = false; + walk(node.nodes, function (child, index) { + if (skip) { + return false; + } + var even = isEven(index); + if (!even && !isSpace(child)) { + valid = false; + return false; + } + if (even) { + if (isAt(child)) { + skip = true; + atIdx = index; + return false; + } + + if (!isShapeRadius(child)) { + valid = false; + return false; + } + } + }); + if (skip && !isPositionNoRepeat({ nodes: node.nodes.slice(atIdx + 2) })) { + return false; + }; + return valid; +} + +function isEllipse(node) { + if (!isFunction(node, 'ellipse')) { + return false; + } + var valid = true; + var atIdx = 0; + var skip = false; + var expectShapeRadius = false; + walk(node.nodes, function (child, index) { + if (skip) { + return false; + } + if (index === 0) { + if (isShapeRadius(child)) { + expectShapeRadius = true; + } + }; + if (index === 2 && expectShapeRadius) { + if (!isShapeRadius(child)) { + valid = false; + return false; + } + }; + var even = isEven(index); + if (!even && !isSpace(child)) { + valid = false; + return false; + } + if (even) { + if (isAt(child)) { + skip = true; + atIdx = index; + return false; + } + }; + }); + if (skip && !isPositionNoRepeat({ nodes: node.nodes.slice(atIdx + 2) })) { + return false; + }; + return valid; +} + +function isPolygon(node) { + if (!isFunction(node, 'polygon')) { + return false; + } + var valid = true; + var commaIdx = void 0; + walk(node.nodes, function (child, index) { + if (index === node.nodes.length - 1) { + if (!isComma(node.nodes[index - 3])) { + valid = false; + return false; + }; + }; + if (index === 0) { + if (isFillRule(child)) { + commaIdx = 1; + return false; + } + if (isLengthPercentage(child)) { + commaIdx = 3; + return false; + } + valid = false; + return false; + }; + if (index === commaIdx) { + commaIdx += 4; + if (!isComma(child)) { + valid = false; + return false; + }; + } else { + var even = isEven(index); + if (even && !isLengthPercentage(child)) { + valid = false; + return false; + } + if (!even && !isSpace(child)) { + valid = false; + return false; + } + }; + }); + return valid; +} + +var isBasicShape = (function (node) { + return isInset(node) || isCircle(node) || isEllipse(node) || isPolygon(node); +}); + +var isClipPathProperty = (function (node) { + return isUrl(node) || isBasicShape(node) || isGeometryBox(node); +}); + var absoluteSizes = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large']; var isAbsoluteSize = (function (node) { @@ -978,14 +1144,6 @@ var isMaskingMode = (function (node) { return isKeyword(node, maskingModes); }); -var geometryBoxes = ['margin-box', 'fill-box', 'stroke-box', 'view-box']; - -var nonStandardKeywords = ['content', 'padding', 'border']; - -var isGeometryBox = (function (node) { - return isBox(node) || isKeyword(node, geometryBoxes) || isKeyword(node, nonStandardKeywords); -}); - function validateShadow(nodes) { var hasColor = false; var hasLength = 0; @@ -1663,6 +1821,28 @@ var webkitColumnBreakInsideValidator = isKeywordFactory(["auto", "avoid", "avoid var captionSideValidator = isKeywordFactory(["top", "bottom", "block-start", "block-end", "inline-start", "inline-end"]); var clearValidator = isKeywordFactory(["none", "left", "right", "both", "inline-start", "inline-end"]); +var clipPathValidator = function clipPathValidator(valueParserAST) { + var node = valueParserAST.nodes[0]; + + if (valueParserAST.nodes.length !== 1) { + return invalidMessage("Expected a single value to be passed."); + } + + var isKeywordResult = isKeyword(node, "none"); + + if (!!isKeywordResult !== false) { + return isKeywordResult; + } + + var isClipPathPropertyResult = isClipPathProperty(node); + + if (!!isClipPathPropertyResult !== false) { + return isClipPathPropertyResult; + } + + return false; +}; + var columnCountValidator = function columnCountValidator(valueParserAST) { var node = valueParserAST.nodes[0]; @@ -2432,6 +2612,7 @@ var validators = { "-webkit-box-orient": mozBoxOrientValidator, "-webkit-box-pack": mozBoxPackValidator, "-webkit-box-sizing": boxSizingValidator, + "-webkit-clip-path": clipPathValidator, "-webkit-column-break-inside": webkitColumnBreakInsideValidator, "-webkit-column-count": columnCountValidator, "-webkit-column-fill": columnFillValidator, @@ -2540,6 +2721,7 @@ var validators = { "break-inside": webkitColumnBreakInsideValidator, "caption-side": captionSideValidator, "clear": clearValidator, + "clip-path": clipPathValidator, "color": webkitBorderBeforeColorValidator, "column-count": columnCountValidator, "column-fill": columnFillValidator, diff --git a/packages/css-values/test.js b/packages/css-values/test.js index 4dc7e91..da0e835 100644 --- a/packages/css-values/test.js +++ b/packages/css-values/test.js @@ -1695,6 +1695,55 @@ test(validCI, clearValidator, "inline-start"); test(invalid, clearValidator, "inline-start inline-start"); test(validCI, clearValidator, "inline-end"); test(invalid, clearValidator, "inline-end inline-end"); +var clipPathValidator = ["-webkit-clip-path", "clip-path"]; +test(globals, clipPathValidator); +test(invalid, clipPathValidator, "0118-999-881-999-119-725-3"); +test(valid, clipPathValidator, "url(https://ru.wikipedia.org/wiki/URI)"); +test(valid, clipPathValidator, "url(data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw)"); +test(valid, clipPathValidator, "margin-box"); +test(valid, clipPathValidator, "fill-box"); +test(valid, clipPathValidator, "stroke-box"); +test(valid, clipPathValidator, "view-box"); +test(valid, clipPathValidator, "content"); +test(valid, clipPathValidator, "padding"); +test(valid, clipPathValidator, "border"); +test(valid, clipPathValidator, "border-box"); +test(valid, clipPathValidator, "padding-box"); +test(valid, clipPathValidator, "content-box"); +test(valid, clipPathValidator, "BORDER-BOX"); +test(valid, clipPathValidator, "PADDING-BOX"); +test(valid, clipPathValidator, "CONTENT-BOX"); +test(valid, clipPathValidator, "view-box"); +test(valid, clipPathValidator, "fill-box"); +test(valid, clipPathValidator, "border-box"); +test(valid, clipPathValidator, "inset(20% 50px)"); +test(valid, clipPathValidator, "circle(closest-side at 20px 20px)"); +test(valid, clipPathValidator, "circle(12%)"); +test(valid, clipPathValidator, "ellipse(closest-side 20px at 20px 20px)"); +test(valid, clipPathValidator, "polygon(30px 20px, 15px 10px, 12% 5px)"); +test(valid, clipPathValidator, "polygon(evenodd, 30px 20px, 15px 10px, 12% 5px)"); +test(invalid, clipPathValidator, "ur(https://ru.wikipedia.org/wiki/URI)"); +test(invalid, clipPathValidator, "rock-box"); +test(invalid, clipPathValidator, "view-box, fill-box, 1px"); +test(invalid, clipPathValidator, "isnet(35px)"); +test(invalid, clipPathValidator, "inset(98 12kHz)"); +test(invalid, clipPathValidator, "inset(20%, 50px)"); +test(invalid, clipPathValidator, "clrcke(15px at 20px 20px)"); +test(invalid, clipPathValidator, "circle(12kHz at 20px 20px)"); +test(invalid, clipPathValidator, "circle(farthest-side, at 20px)"); +test(invalid, clipPathValidator, "circle(farthest-side at 20)"); +test(invalid, clipPathValidator, "ellipse(closest-side, 20px at 20px 12em)"); +test(invalid, clipPathValidator, "ellipse(closest-side 90deg at 20px 20px)"); +test(invalid, clipPathValidator, "ellipse(90deg 20px at 65deg)"); +test(invalid, clipPathValidator, "elcipse(closest-side 20px at 201)"); +test(invalid, clipPathValidator, "monogon(30px 20px, 15px 10px, 12% 5px)"); +test(invalid, clipPathValidator, "polygon(25kHz, 30px 20px, 15px 10px, 12% 5px)"); +test(invalid, clipPathValidator, "polygon(15px 10px, yoyo 20px, 12% 5px)"); +test(invalid, clipPathValidator, "polygon(closest-side, 10% 20px 6px, 15px 10px, 12% 5px)"); +test(invalid, clipPathValidator, "polygon(30px 20px, 15px 10px, 12% 5px 78px)"); +test(invalid, clipPathValidator, "url(https://ru.wikipedia.org/wiki/URI) url(https://ru.wikipedia.org/wiki/URI)"); +test(validCI, clipPathValidator, "none"); +test(invalid, clipPathValidator, "none none"); var columnCountValidator = ["-webkit-column-count", "-moz-column-count", "column-count"]; test(globals, columnCountValidator); test(invalid, columnCountValidator, "0118-999-881-999-119-725-3"); @@ -2812,7 +2861,9 @@ test(valid, maskOriginValidator, "content-box"); test(valid, maskOriginValidator, "BORDER-BOX"); test(valid, maskOriginValidator, "PADDING-BOX"); test(valid, maskOriginValidator, "CONTENT-BOX"); -test(valid, maskOriginValidator, "view-box, fill-box, border-box"); +test(valid, maskOriginValidator, "view-box"); +test(valid, maskOriginValidator, "fill-box"); +test(valid, maskOriginValidator, "border-box"); test(invalid, maskOriginValidator, "rock-box"); test(invalid, maskOriginValidator, "view-box, fill-box, 1px"); test(valid, maskOriginValidator, "margin-box, margin-box"); diff --git a/src/completed.js b/src/completed.js index 0c4d089..6f0cec2 100644 --- a/src/completed.js +++ b/src/completed.js @@ -18,6 +18,7 @@ export default [ '-webkit-border-before-color', '-webkit-border-before-style', '-webkit-border-before-width', + '-webkit-clip-path', '-webkit-mask-attachment', '-webkit-mask-composite', '-webkit-mask-position', @@ -103,6 +104,7 @@ export default [ 'break-inside', 'caption-side', 'clear', + 'clip-path', 'color', 'column-count', 'column-fill', diff --git a/src/data.js b/src/data.js index 8dacf5f..9f1014e 100644 --- a/src/data.js +++ b/src/data.js @@ -32,6 +32,7 @@ const overrides = { 'text-indent': '[ | ] && hanging? && each-line?', top: ' | auto', 'vertical-align': 'baseline | sub | super | text-top | text-bottom | middle | top | bottom | ', + 'clip-path': ' | none ', // syntaxes 'feature-value-name': '', 'single-transition-property': 'all | ', diff --git a/src/fixtures/basicShape.js b/src/fixtures/basicShape.js new file mode 100644 index 0000000..2c76bee --- /dev/null +++ b/src/fixtures/basicShape.js @@ -0,0 +1,28 @@ +export default { + valid: [ + 'inset(20% 50px)', + 'circle(closest-side at 20px 20px)', + 'circle(12%)', + 'ellipse(closest-side 20px at 20px 20px)', + 'polygon(30px 20px, 15px 10px, 12% 5px)', + 'polygon(evenodd, 30px 20px, 15px 10px, 12% 5px)', + ], + invalid: [ + 'isnet(35px)', + 'inset(98 12kHz)', + 'inset(20%, 50px)', + 'clrcke(15px at 20px 20px)', + 'circle(12kHz at 20px 20px)', + 'circle(farthest-side, at 20px)', + 'circle(farthest-side at 20)', + 'ellipse(closest-side, 20px at 20px 12em)', + 'ellipse(closest-side 90deg at 20px 20px)', + 'ellipse(90deg 20px at 65deg)', + 'elcipse(closest-side 20px at 201)', + 'monogon(30px 20px, 15px 10px, 12% 5px)', + 'polygon(25kHz, 30px 20px, 15px 10px, 12% 5px)', + 'polygon(15px 10px, yoyo 20px, 12% 5px)', + 'polygon(closest-side, 10% 20px 6px, 15px 10px, 12% 5px)', + 'polygon(30px 20px, 15px 10px, 12% 5px 78px)', + ], +}; diff --git a/src/fixtures/clipPathProperty.js b/src/fixtures/clipPathProperty.js new file mode 100644 index 0000000..44ad72b --- /dev/null +++ b/src/fixtures/clipPathProperty.js @@ -0,0 +1,16 @@ +import url from './url'; +import geometryBox from './geometryBox'; +import basicShape from './basicShape'; + +export default { + valid: [ + ...url.valid, + ...geometryBox.valid, + ...basicShape.valid, + ], + invalid: [ + ...url.invalid, + ...geometryBox.invalid, + ...basicShape.invalid, + ], +}; diff --git a/src/fixtures/geometryBox.js b/src/fixtures/geometryBox.js index 77cc6c5..21f13fb 100644 --- a/src/fixtures/geometryBox.js +++ b/src/fixtures/geometryBox.js @@ -6,7 +6,9 @@ export default { ...geometryBoxes, ...nonStandardKeywords, ...boxes.valid, - 'view-box, fill-box, border-box', + 'view-box', + 'fill-box', + 'border-box', ], invalid: [ ...boxes.invalid, diff --git a/src/fixtures/index.js b/src/fixtures/index.js index 42e1ad4..f0b540a 100644 --- a/src/fixtures/index.js +++ b/src/fixtures/index.js @@ -47,3 +47,4 @@ export uri from './uri'; export geometryBox from './geometryBox'; export trackSize from './trackSize'; export maskReference from './maskReference'; +export clipPathProperty from './clipPathProperty'; diff --git a/src/generators/property.js b/src/generators/property.js index 2f7b183..debc442 100644 --- a/src/generators/property.js +++ b/src/generators/property.js @@ -196,7 +196,8 @@ function createValidator (opts) { switch (value) { case 'bg-size': case 'repeat-style': - return genericValidatorStub(dataValidator(value), opts); + case 'clip-path-property': + return genericValidatorStub(dataValidator(value), opts); case 'position': return generatePositionValidator(opts); } diff --git a/src/validators/index.js b/src/validators/index.js index d13f80c..f4d0420 100644 --- a/src/validators/index.js +++ b/src/validators/index.js @@ -48,3 +48,4 @@ export * as isCalc from './isCalc'; export * as isGeometryBox from './isGeometryBox'; export * as isTrackSize from './isTrackSize'; export * as isMaskReference from './isMaskReference'; +export * as isClipPathProperty from './isClipPathProperty'; diff --git a/src/validators/isBasicShape.js b/src/validators/isBasicShape.js new file mode 100644 index 0000000..5b3b80d --- /dev/null +++ b/src/validators/isBasicShape.js @@ -0,0 +1,169 @@ +import {walk} from 'postcss-value-parser'; +import isLengthPercentage from './isLengthPercentage'; +import isFunction from './isFunction'; +import isKeyword from './isKeyword'; +import {isPositionNoRepeat} from './isPosition'; +import isSpace from './isSpace'; +import isAt from './isAt'; +import isEven from './isEven'; +import isComma from './isComma'; + +function isFillRule (node) { + return isKeyword(node, ['nonzero', 'evenodd']); +} + +function isShapeRadius (node) { + return isLengthPercentage(node) + || isKeyword(node, ['closest-side', 'farthest-side']); +} + + +export function isInset (node) { + if (!isFunction(node, 'inset')) { + return false; + } + let valid = true; + walk(node.nodes, (child, index) => { + const even = isEven(index); + if (!even && !isSpace(child)) { + valid = false; + return false; + } + if (even && !isLengthPercentage(child)) { + valid = false; + return false; + } + }); + return valid; +} + +export function isCircle (node) { + if (!isFunction(node, 'circle')) { + return false; + } + let valid = true; + let atIdx = 0; + let skip = false; + walk(node.nodes, (child, index) => { + if (skip) { + return false; + } + const even = isEven(index); + if (!even && !isSpace(child)) { + valid = false; + return false; + } + if (even) { + if (isAt(child)) { + skip = true; + atIdx = index; + return false; + } + + if (!isShapeRadius(child)) { + valid = false; + return false; + } + } + }); + if (skip && !isPositionNoRepeat({nodes:node.nodes.slice(atIdx + 2)})) { + return false; + }; + return valid; +} + +export function isEllipse (node) { + if (!isFunction(node, 'ellipse')) { + return false; + } + let valid = true; + let atIdx = 0; + let skip = false; + let expectShapeRadius = false; + walk(node.nodes, (child, index) => { + if (skip) { + return false; + } + if (index === 0) { + if (isShapeRadius(child)) { + expectShapeRadius = true; + } + }; + if (index === 2 && expectShapeRadius) { + if (!isShapeRadius(child)) { + valid = false; + return false; + } + }; + const even = isEven(index); + if (!even && !isSpace(child)) { + valid = false; + return false; + } + if (even) { + if (isAt(child)) { + skip = true; + atIdx = index; + return false; + } + }; + }); + if (skip && !isPositionNoRepeat({nodes:node.nodes.slice(atIdx + 2)})) { + return false; + }; + return valid; +} + +export function isPolygon (node) { + if (!isFunction(node, 'polygon')) { + return false; + } + let valid = true; + let commaIdx; + walk(node.nodes, (child, index) => { + if (index === node.nodes.length - 1) { + if (!isComma(node.nodes[index - 3])) { + valid = false; + return false; + }; + }; + if (index === 0) { + if (isFillRule(child)) { + commaIdx = 1; + return false; + } + if (isLengthPercentage(child)) { + commaIdx = 3; + return false; + } + valid = false; + return false; + }; + if (index === commaIdx) { + commaIdx += 4; + if (!isComma(child)) { + valid = false; + return false; + }; + } else { + const even = isEven(index); + if (even && !isLengthPercentage(child)) { + valid = false; + return false; + } + if (!even && !isSpace(child)) { + valid = false; + return false; + } + }; + + }); + return valid; +} + +export default (node) => { + return isInset(node) + || isCircle(node) + || isEllipse(node) + || isPolygon(node); +}; diff --git a/src/validators/isClipPathProperty.js b/src/validators/isClipPathProperty.js new file mode 100644 index 0000000..d75ae26 --- /dev/null +++ b/src/validators/isClipPathProperty.js @@ -0,0 +1,9 @@ +import isUrl from './isUrl'; +import isGeometryBox from './isGeometryBox'; +import isBasicShape from './isBasicShape'; + +export default node => { + return isUrl(node) + || isBasicShape(node) + || isGeometryBox(node); +};