From 3f8d972dc74f20c1036dc7a7965e3730eada2103 Mon Sep 17 00:00:00 2001 From: Dima Voytenko Date: Mon, 22 May 2017 09:25:29 -0700 Subject: [PATCH] Animations: CSS var/calc polyfill calculations (#9400) * Animations: CSS var/calc polyfill calculations * review fixes --- extensions/amp-animation/0.1/css-expr-ast.js | 528 ++++++++- .../amp-animation/0.1/css-expr-impl.jison | 21 +- extensions/amp-animation/0.1/css-expr-impl.js | 74 +- .../amp-animation/0.1/test/test-css-expr.js | 1017 +++++++++++++++-- 4 files changed, 1504 insertions(+), 136 deletions(-) diff --git a/extensions/amp-animation/0.1/css-expr-ast.js b/extensions/amp-animation/0.1/css-expr-ast.js index 4985dfdc42ca..a09acdba4648 100644 --- a/extensions/amp-animation/0.1/css-expr-ast.js +++ b/extensions/amp-animation/0.1/css-expr-ast.js @@ -14,10 +14,73 @@ * limitations under the License. */ -import {assertHttpsUrl} from '../../../src/url'; +const FINAL_URL_RE = /^(data|https)\:/i; +const DEG_TO_RAD = 2 * Math.PI / 360; +const GRAD_TO_RAD = Math.PI / 200; -const EXTRACT_URL_RE = /url\(\s*['"]?([^()'"]*)['"]?\s*\)/i; -const DATA_URL_RE = /^data\:/i; + +/** + * An interface that assists in CSS evaluation. + * @interface + */ +export class CssContext { + + /** + * Returns a resolved URL. The result must be an allowed URL for execution, + * with HTTPS restrictions. + * @param {string} unusedUrl + * @return {string} + */ + resolveUrl(unusedUrl) {} + + /** + * Returns the value of a CSS variable or `null` if not available. + * @param {string} unusedVarName + * @return {?CssNode} + */ + getVar(unusedVarName) {} + + /** + * Returns the current font size. + * @return {number} + */ + getCurrentFontSize() {} + + /** + * Returns the root font size. + * @return {number} + */ + getRootFontSize() {} + + /** + * Returns the viewport size. + * @return {!{width: number, height: number}} + */ + getViewportSize() {} + + /** + * Returns the current element's size. + * @return {!{width: number, height: number}} + */ + getCurrentElementSize() {} + + /** + * Returns the dimension: "w" for width or "h" for height. + * @return {?string} + */ + getDimension() {} + + /** + * Pushes the dimension: "w" for width or "h" for height. + * @param {?string} unusedDim + */ + pushDimension(unusedDim) {} + + /** + * Pops the dimension. + */ + popDimension() {} +} /** @@ -27,6 +90,67 @@ const DATA_URL_RE = /^data\:/i; */ export class CssNode { constructor() {} + + /** + * Returns a string CSS representation. + * @return {string} + * @abstract + */ + css() {} + + /** + * Resolves the value of all variable components. Only performs any work if + * variable components exist. As an optimization, this node is returned + * for a non-variable nodes (`isConst() == true`). Otherwise, `calc()` method + * is used to calculate the new value. + * @param {!CssContext} context + * @return {?CssNode} + * @final + */ + resolve(context) { + if (this.isConst()) { + return this; + } + return this.calc(context); + } + + /** + * Whether the CSS node is a constant or includes variable components. + * @return {boolean} + * @protected + */ + isConst() { + return true; + } + + /** + * Calculates the value of all variable components. + * @param {!CssContext} unusedContext + * @return {?CssNode} + * @protected + */ + calc(unusedContext) { + return this; + } +} + + +/** + * A CSS expression that's simply passed through from the original expression. + * Used for `url()`, colors, etc. + */ +export class CssPassthroughNode extends CssNode { + /** @param {string} css */ + constructor(css) { + super(); + /** @const @private {string} */ + this.css_ = css; + } + + /** @override */ + css() { + return this.css_; + } } @@ -62,19 +186,30 @@ export class CssConcatNode extends CssNode { } return set; } -} + /** @override */ + css() { + return this.array_.map(node => node.css()).join(' '); + } -/** - * A CSS expression that's simply passed through from the original expression. - * Used for `url()`, colors, etc. - */ -export class CssPassthroughNode extends CssNode { - /** @param {string} css */ - constructor(css) { - super(); - /** @const @private {string} */ - this.css_ = css; + /** @override */ + isConst() { + return this.array_.reduce((acc, node) => acc && node.isConst(), true); + } + + /** @override */ + calc(context) { + const resolvedArray = []; + for (let i = 0; i < this.array_.length; i++) { + const resolved = this.array_[i].resolve(context); + if (resolved) { + resolvedArray.push(resolved); + } else { + // One element is null - the result is null. + return null; + } + } + return new CssConcatNode(resolvedArray); } } @@ -82,17 +217,32 @@ export class CssPassthroughNode extends CssNode { /** * Verifies that URL is an HTTPS URL. */ -export class CssUrlNode extends CssPassthroughNode { - /** @param {string} css */ - constructor(css) { - super(css); - const matches = css.match(EXTRACT_URL_RE); - if (matches || matches.length > 1) { - const url = (matches[1] || '').trim(); - if (!url.match(DATA_URL_RE)) { - assertHttpsUrl(url); - } +export class CssUrlNode extends CssNode { + /** @param {string} url */ + constructor(url) { + super(); + /** @const @private {string} */ + this.url_ = url; + } + + /** @override */ + css() { + if (!this.url_) { + return ''; } + return `url("${this.url_}")`; + } + + /** @override */ + isConst() { + return !this.url_ || FINAL_URL_RE.test(this.url_); + } + + /** @override */ + calc(context) { + const url = context.resolveUrl(this.url_); + // Return a passthrough CSS to avoid recursive `url()` evaluation. + return new CssPassthroughNode(`url("${url}")`); } } @@ -115,6 +265,35 @@ export class CssNumericNode extends CssNode { /** @const @private {string} */ this.units_ = units.toLowerCase(); } + + /** @override */ + css() { + return `${this.num_}${this.units_}`; + } + + /** + * @param {number} unusedNum + * @return {!CssNumberNode} + * @abstract + */ + createSameUnits(unusedNum) {} + + /** + * @param {!CssContext} unusedContext + * @return {!CssNumberNode} + */ + norm(unusedContext) { + return this; + } + + /** + * @param {number} percent + * @param {!CssContext} unusedContext + * @return {!CssNumberNode} + */ + calcPercent(percent, unusedContext) { + throw new Error('cannot calculate percent for ' + this.type_); + } } @@ -126,6 +305,11 @@ export class CssNumberNode extends CssNumericNode { constructor(num) { super('NUM', num, ''); } + + /** @override */ + createSameUnits(num) { + return new CssNumberNode(num); + } } @@ -137,6 +321,11 @@ export class CssPercentNode extends CssNumericNode { constructor(num) { super('PRC', num, '%'); } + + /** @override */ + createSameUnits(num) { + return new CssPercentNode(num); + } } @@ -151,6 +340,61 @@ export class CssLengthNode extends CssNumericNode { constructor(num, units) { super('LEN', num, units); } + + /** @override */ + createSameUnits(num) { + return new CssLengthNode(num, this.units_); + } + + /** @override */ + norm(context) { + if (this.units_ == 'px') { + return this; + } + + // Font-based: em/rem. + if (this.units_ == 'em' || this.units_ == 'rem') { + const fontSize = this.units_ == 'em' ? + context.getCurrentFontSize() : + context.getRootFontSize(); + return new CssLengthNode(this.num_ * fontSize, 'px'); + } + + // Viewport based: vw, vh, vmin, vmax. + if (this.units_ == 'vw' || + this.units_ == 'vh' || + this.units_ == 'vmin' || + this.units_ == 'vmax') { + const vp = context.getViewportSize(); + const vw = vp.width * this.num_ / 100; + const vh = vp.height * this.num_ / 100; + let num = 0; + if (this.units_ == 'vw') { + num = vw; + } else if (this.units_ == 'vh') { + num = vh; + } else if (this.units_ == 'vmin') { + num = Math.min(vw, vh); + } else if (this.units_ == 'vmax') { + num = Math.max(vw, vh); + } + return new CssLengthNode(num, 'px'); + } + + // Can't convert cm/in/etc to px at this time. + throw unknownUnits(this.units_); + } + + /** @override */ + calcPercent(percent, context) { + const dim = context.getDimension(); + const size = context.getCurrentElementSize(); + const side = + dim == 'w' ? size.width : + dim == 'h' ? size.height : + 0; + return new CssLengthNode(side * percent / 100, 'px'); + } } @@ -165,6 +409,25 @@ export class CssAngleNode extends CssNumericNode { constructor(num, units) { super('ANG', num, units); } + + /** @override */ + createSameUnits(num) { + return new CssAngleNode(num, this.units_); + } + + /** @override */ + norm() { + if (this.units_ == 'rad') { + return this; + } + if (this.units_ == 'deg') { + return new CssAngleNode(this.num_ * DEG_TO_RAD, 'rad'); + } + if (this.units_ == 'grad') { + return new CssAngleNode(this.num_ * GRAD_TO_RAD, 'rad'); + } + throw unknownUnits(this.units_); + } } @@ -179,6 +442,22 @@ export class CssTimeNode extends CssNumericNode { constructor(num, units) { super('TME', num, units); } + + /** @override */ + createSameUnits(num) { + return new CssTimeNode(num, this.units_); + } + + /** @override */ + norm() { + if (this.units_ == 'ms') { + return this; + } + if (this.units_ == 's') { + return new CssTimeNode(this.num_ * 1000, 'ms'); + } + throw unknownUnits(this.units_); + } } @@ -189,13 +468,50 @@ export class CssFuncNode extends CssNode { /** * @param {string} name * @param {!Array} args + * @param {?Array=} opt_dimensions */ - constructor(name, args) { + constructor(name, args, opt_dimensions) { super(); /** @const @private {string} */ this.name_ = name.toLowerCase(); /** @const @private {!Array} */ this.args_ = args; + /** @const @private {?Array} */ + this.dimensions_ = opt_dimensions || null; + } + + /** @override */ + css() { + const args = this.args_.map(node => node.css()).join(','); + return `${this.name_}(${args})`; + } + + /** @override */ + isConst() { + return this.args_.reduce((acc, node) => acc && node.isConst(), true); + } + + /** @override */ + calc(context) { + const resolvedArgs = []; + for (let i = 0; i < this.args_.length; i++) { + const node = this.args_[i]; + let resolved; + if (this.dimensions_ && i < this.dimensions_.length) { + context.pushDimension(this.dimensions_[i]); + resolved = node.resolve(context); + context.popDimension(); + } else { + resolved = node.resolve(context); + } + if (resolved) { + resolvedArgs.push(resolved); + } else { + // One argument is null - the function's result is null. + return null; + } + } + return new CssFuncNode(this.name_, resolvedArgs); } } @@ -214,7 +530,12 @@ export class CssTranslateNode extends CssFuncNode { * @param {!Array} args */ constructor(suffix, args) { - super(`translate${suffix}`, args); + super(`translate${suffix.toUpperCase()}`, args, + suffix == '' ? ['w', 'h'] : + suffix == 'x' ? ['w'] : + suffix == 'y' ? ['h'] : + suffix == 'z' ? ['z'] : + suffix == '3d' ? ['w', 'h', 'z'] : null); /** @const @private {string} */ this.suffix_ = suffix; } @@ -237,6 +558,28 @@ export class CssVarNode extends CssNode { /** @const @private {?CssNode} */ this.def_ = opt_def || null; } + + /** @override */ + css() { + return `var(${this.varName_}${this.def_ ? ',' + this.def_.css() : ''})`; + } + + /** @override */ + isConst() { + return false; + } + + /** @override */ + calc(context) { + const varNode = context.getVar(this.varName_); + if (varNode) { + return varNode.resolve(context); + } + if (this.def_) { + return this.def_.resolve(context); + } + return null; + } } @@ -251,6 +594,21 @@ export class CssCalcNode extends CssNode { /** @const @private {!CssNode} */ this.expr_ = expr; } + + /** @override */ + css() { + return `calc(${this.expr_.css()})`; + } + + /** @override */ + isConst() { + return false; + } + + /** @override */ + calc(context) { + return this.expr_.resolve(context); + } } @@ -272,6 +630,56 @@ export class CssCalcSumNode extends CssNode { /** @const @private {string} */ this.op_ = op; } + + /** @override */ + css() { + return `${this.left_.css()} ${this.op_} ${this.right_.css()}`; + } + + /** @override */ + isConst() { + return false; + } + + /** @override */ + calc(context) { + /* + * From spec: + * At + or -, check that both sides have the same type, or that one side is + * a and the other is an . If both sides are the same + * type, resolve to that type. If one side is a and the other is + * an , resolve to . + */ + let left = this.left_.resolve(context); + let right = this.right_.resolve(context); + if (left == null || right == null) { + return null; + } + if (!(left instanceof CssNumericNode) || + !(right instanceof CssNumericNode)) { + throw new Error('left and right must be both numerical'); + } + if (left.type_ != right.type_) { + // Percent values are special: they need to be resolved in the context + // of the other dimension. + if (left instanceof CssPercentNode) { + left = right.calcPercent(left.num_, context); + } else if (right instanceof CssPercentNode) { + right = left.calcPercent(right.num_, context); + } else { + throw new Error('left and right must be the same type'); + } + } + + // Units are the same, the math is simple: numerals are summed. Otherwise, + // the units neeed to be normalized first. + if (left.units_ != right.units_) { + left = left.norm(context); + right = right.norm(context); + } + const sign = this.op_ == '+' ? 1 : -1; + return left.createSameUnits(left.num_ + sign * right.num_); + } } @@ -293,4 +701,72 @@ export class CssCalcProductNode extends CssNode { /** @const @private {string} */ this.op_ = op; } + + /** @override */ + css() { + return `${this.left_.css()} ${this.op_} ${this.right_.css()}`; + } + + /** @override */ + isConst() { + return false; + } + + /** @override */ + calc(context) { + const left = this.left_.resolve(context); + const right = this.right_.resolve(context); + if (left == null || right == null) { + return null; + } + if (!(left instanceof CssNumericNode) || + !(right instanceof CssNumericNode)) { + throw new Error('left and right must be both numerical'); + } + + /* + * From spec: + * At *, check that at least one side is . If both sides are + * , resolve to . Otherwise, resolve to the type of the + * other side. + * At /, check that the right side is . If the left side is + * , resolve to . Otherwise, resolve to the type of the + * left side. + */ + let base; + let multi; + if (this.op_ == '*') { + if (left instanceof CssNumberNode) { + multi = left.num_; + base = right; + } else { + if (!(right instanceof CssNumberNode)) { + throw new Error('one of sides in multiplication must be a number'); + } + multi = right.num_; + base = left; + } + } else { + if (!(right instanceof CssNumberNode)) { + throw new Error('denominator must be a number'); + } + base = left; + multi = 1 / right.num_; + } + + const num = base.num_ * multi; + if (!isFinite(num)) { + return null; + } + return base.createSameUnits(num); + } +} + + +/** + * @param {string} units + * @return {!Error} + */ +function unknownUnits(units) { + return new Error('unknown units: ' + units); } diff --git a/extensions/amp-animation/0.1/css-expr-impl.jison b/extensions/amp-animation/0.1/css-expr-impl.jison index f925f83baf52..143612e6ac94 100644 --- a/extensions/amp-animation/0.1/css-expr-impl.jison +++ b/extensions/amp-animation/0.1/css-expr-impl.jison @@ -66,9 +66,9 @@ ident \-?[a-zA-Z_][a-zA-Z0-9_]* "#"{hex} return 'HEXCOLOR'; -{U}{R}{L}"("{str}")" return 'URL' +{U}{R}{L}\( return 'URL_START' {C}{A}{L}{C}\( return 'CALC_START' -{V}{A}{R}"(" return 'VAR_START' +{V}{A}{R}\( return 'VAR_START' {T}{R}{A}{N}{S}{L}{A}{T}{E}\( return 'TRANSLATE_START' {T}{R}{A}{N}{S}{L}{A}{T}{E}{X}\( return 'TRANSLATE_X_START' {T}{R}{A}{N}{S}{L}{A}{T}{E}{Y}\( return 'TRANSLATE_Y_START' @@ -170,8 +170,8 @@ literal: {$$ = $1;} | time {$$ = $1;} - | URL - {$$ = new ast.CssUrlNode($1);} + | url + {$$ = $1;} | HEXCOLOR {$$ = new ast.CssPassthroughNode($1);} | IDENT @@ -272,6 +272,19 @@ args: ; +/** + * CSS `url()` function. + * - `url("https://acme.org/img")` + * - `url(`https://acme.org/img`)` + * - `url(`data:...`)` + * - `url("/img")` + */ +url: + URL_START STRING ')' + {$$ = new ast.CssUrlNode($2.slice(1, -1));} + ; + + /** * Translate set (https://developer.mozilla.org/en-US/docs/Web/CSS/transform): * - `translate(x, y)` diff --git a/extensions/amp-animation/0.1/css-expr-impl.js b/extensions/amp-animation/0.1/css-expr-impl.js index 6c993c363312..01904747862e 100644 --- a/extensions/amp-animation/0.1/css-expr-impl.js +++ b/extensions/amp-animation/0.1/css-expr-impl.js @@ -93,12 +93,12 @@ import * as ast from './css-expr-ast'; } */ var parser = (function(){ -var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,7],$V1=[1,8],$V2=[1,9],$V3=[1,13],$V4=[1,14],$V5=[1,15],$V6=[1,20],$V7=[1,21],$V8=[1,22],$V9=[1,23],$Va=[1,24],$Vb=[1,25],$Vc=[1,26],$Vd=[1,27],$Ve=[1,28],$Vf=[1,29],$Vg=[1,30],$Vh=[1,31],$Vi=[1,32],$Vj=[1,33],$Vk=[1,34],$Vl=[1,35],$Vm=[1,36],$Vn=[1,37],$Vo=[1,45],$Vp=[1,40],$Vq=[1,41],$Vr=[1,42],$Vs=[1,43],$Vt=[1,44],$Vu=[1,38],$Vv=[1,39],$Vw=[5,9,10,11,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,40,44,45,46,47,48,49,51],$Vx=[5,9,10,11,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,40,41,43,44,45,46,47,48,49,51,54,55,56,57],$Vy=[1,51],$Vz=[1,63],$VA=[1,64],$VB=[1,65],$VC=[1,66],$VD=[41,54,55,56,57],$VE=[1,69],$VF=[41,43],$VG=[41,56,57]; +var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,7],$V1=[1,8],$V2=[1,9],$V3=[1,14],$V4=[1,15],$V5=[1,20],$V6=[1,21],$V7=[1,22],$V8=[1,23],$V9=[1,24],$Va=[1,25],$Vb=[1,26],$Vc=[1,27],$Vd=[1,28],$Ve=[1,29],$Vf=[1,30],$Vg=[1,31],$Vh=[1,32],$Vi=[1,33],$Vj=[1,34],$Vk=[1,35],$Vl=[1,36],$Vm=[1,37],$Vn=[1,46],$Vo=[1,38],$Vp=[1,41],$Vq=[1,42],$Vr=[1,43],$Vs=[1,44],$Vt=[1,45],$Vu=[1,39],$Vv=[1,40],$Vw=[5,9,10,11,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,40,44,45,46,47,48,49,50,52],$Vx=[5,9,10,11,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,40,41,43,44,45,46,47,48,49,50,52,55,56,57,58],$Vy=[1,53],$Vz=[1,66],$VA=[1,67],$VB=[1,68],$VC=[1,69],$VD=[41,55,56,57,58],$VE=[1,72],$VF=[41,43],$VG=[41,57,58]; var parser = {trace: function trace() { }, yy: {}, -symbols_: {"error":2,"result":3,"value":4,"EOF":5,"literal_or_function":6,"literal":7,"function":8,"STRING":9,"NUMBER":10,"PERCENTAGE":11,"length":12,"angle":13,"time":14,"URL":15,"HEXCOLOR":16,"IDENT":17,"LENGTH_PX":18,"LENGTH_EM":19,"LENGTH_REM":20,"LENGTH_VH":21,"LENGTH_VW":22,"LENGTH_VMIN":23,"LENGTH_VMAX":24,"LENGTH_CM":25,"LENGTH_MM":26,"LENGTH_Q":27,"LENGTH_IN":28,"LENGTH_PC":29,"LENGTH_PT":30,"ANGLE_DEG":31,"ANGLE_RAD":32,"ANGLE_GRAD":33,"TIME_MS":34,"TIME_S":35,"var_function":36,"calc_function":37,"translate_function":38,"any_function":39,"FUNCTION_START":40,")":41,"args":42,",":43,"TRANSLATE_START":44,"TRANSLATE_X_START":45,"TRANSLATE_Y_START":46,"TRANSLATE_Z_START":47,"TRANSLATE_3D_START":48,"VAR_START":49,"VAR_NAME":50,"CALC_START":51,"calc_expr":52,"(":53,"*":54,"/":55,"+":56,"-":57,"$accept":0,"$end":1}, -terminals_: {2:"error",5:"EOF",9:"STRING",10:"NUMBER",11:"PERCENTAGE",15:"URL",16:"HEXCOLOR",17:"IDENT",18:"LENGTH_PX",19:"LENGTH_EM",20:"LENGTH_REM",21:"LENGTH_VH",22:"LENGTH_VW",23:"LENGTH_VMIN",24:"LENGTH_VMAX",25:"LENGTH_CM",26:"LENGTH_MM",27:"LENGTH_Q",28:"LENGTH_IN",29:"LENGTH_PC",30:"LENGTH_PT",31:"ANGLE_DEG",32:"ANGLE_RAD",33:"ANGLE_GRAD",34:"TIME_MS",35:"TIME_S",40:"FUNCTION_START",41:")",43:",",44:"TRANSLATE_START",45:"TRANSLATE_X_START",46:"TRANSLATE_Y_START",47:"TRANSLATE_Z_START",48:"TRANSLATE_3D_START",49:"VAR_START",50:"VAR_NAME",51:"CALC_START",53:"(",54:"*",55:"/",56:"+",57:"-"}, -productions_: [0,[3,2],[3,1],[4,1],[4,2],[6,1],[6,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[13,1],[13,1],[13,1],[14,1],[14,1],[8,1],[8,1],[8,1],[8,1],[39,2],[39,3],[42,1],[42,3],[38,3],[38,3],[38,3],[38,3],[38,3],[36,3],[36,5],[37,3],[52,1],[52,3],[52,3],[52,3],[52,3],[52,3]], +symbols_: {"error":2,"result":3,"value":4,"EOF":5,"literal_or_function":6,"literal":7,"function":8,"STRING":9,"NUMBER":10,"PERCENTAGE":11,"length":12,"angle":13,"time":14,"url":15,"HEXCOLOR":16,"IDENT":17,"LENGTH_PX":18,"LENGTH_EM":19,"LENGTH_REM":20,"LENGTH_VH":21,"LENGTH_VW":22,"LENGTH_VMIN":23,"LENGTH_VMAX":24,"LENGTH_CM":25,"LENGTH_MM":26,"LENGTH_Q":27,"LENGTH_IN":28,"LENGTH_PC":29,"LENGTH_PT":30,"ANGLE_DEG":31,"ANGLE_RAD":32,"ANGLE_GRAD":33,"TIME_MS":34,"TIME_S":35,"var_function":36,"calc_function":37,"translate_function":38,"any_function":39,"FUNCTION_START":40,")":41,"args":42,",":43,"URL_START":44,"TRANSLATE_START":45,"TRANSLATE_X_START":46,"TRANSLATE_Y_START":47,"TRANSLATE_Z_START":48,"TRANSLATE_3D_START":49,"VAR_START":50,"VAR_NAME":51,"CALC_START":52,"calc_expr":53,"(":54,"*":55,"/":56,"+":57,"-":58,"$accept":0,"$end":1}, +terminals_: {2:"error",5:"EOF",9:"STRING",10:"NUMBER",11:"PERCENTAGE",16:"HEXCOLOR",17:"IDENT",18:"LENGTH_PX",19:"LENGTH_EM",20:"LENGTH_REM",21:"LENGTH_VH",22:"LENGTH_VW",23:"LENGTH_VMIN",24:"LENGTH_VMAX",25:"LENGTH_CM",26:"LENGTH_MM",27:"LENGTH_Q",28:"LENGTH_IN",29:"LENGTH_PC",30:"LENGTH_PT",31:"ANGLE_DEG",32:"ANGLE_RAD",33:"ANGLE_GRAD",34:"TIME_MS",35:"TIME_S",40:"FUNCTION_START",41:")",43:",",44:"URL_START",45:"TRANSLATE_START",46:"TRANSLATE_X_START",47:"TRANSLATE_Y_START",48:"TRANSLATE_Z_START",49:"TRANSLATE_3D_START",50:"VAR_START",51:"VAR_NAME",52:"CALC_START",54:"(",55:"*",56:"/",57:"+",58:"-"}, +productions_: [0,[3,2],[3,1],[4,1],[4,2],[6,1],[6,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[12,1],[13,1],[13,1],[13,1],[14,1],[14,1],[8,1],[8,1],[8,1],[8,1],[39,2],[39,3],[42,1],[42,3],[15,3],[38,3],[38,3],[38,3],[38,3],[38,3],[36,3],[36,5],[37,3],[53,1],[53,3],[53,3],[53,3],[53,3],[53,3]], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { /* this == yyval */ @@ -110,7 +110,7 @@ break; case 2: return null; break; -case 3: case 5: case 6: case 10: case 11: case 12: case 34: case 35: case 36: case 37: case 50: +case 3: case 5: case 6: case 10: case 11: case 12: case 13: case 34: case 35: case 36: case 37: case 51: this.$ = $$[$0]; break; case 4: @@ -125,9 +125,6 @@ break; case 9: this.$ = new ast.CssPercentNode(parseFloat($$[$0])); break; -case 13: -this.$ = new ast.CssUrlNode($$[$0]); -break; case 16: this.$ = new ast.CssLengthNode(parseFloat($$[$0]), 'px'); break; @@ -199,48 +196,51 @@ case 41: break; case 42: -this.$ = new ast.CssTranslateNode('', $$[$0-1]); +this.$ = new ast.CssUrlNode($$[$0-1].slice(1, -1)); break; case 43: -this.$ = new ast.CssTranslateNode('x', $$[$0-1]); +this.$ = new ast.CssTranslateNode('', $$[$0-1]); break; case 44: -this.$ = new ast.CssTranslateNode('y', $$[$0-1]); +this.$ = new ast.CssTranslateNode('x', $$[$0-1]); break; case 45: -this.$ = new ast.CssTranslateNode('z', $$[$0-1]); +this.$ = new ast.CssTranslateNode('y', $$[$0-1]); break; case 46: -this.$ = new ast.CssTranslateNode('3d', $$[$0-1]); +this.$ = new ast.CssTranslateNode('z', $$[$0-1]); break; case 47: -this.$ = new ast.CssVarNode($$[$0-1]); +this.$ = new ast.CssTranslateNode('3d', $$[$0-1]); break; case 48: -this.$ = new ast.CssVarNode($$[$0-3], $$[$0-1]); +this.$ = new ast.CssVarNode($$[$0-1]); break; case 49: +this.$ = new ast.CssVarNode($$[$0-3], $$[$0-1]); +break; +case 50: this.$ = new ast.CssCalcNode($$[$0-1]); break; -case 51: +case 52: this.$ = $$[$0-1]; break; -case 52: +case 53: this.$ = new ast.CssCalcProductNode($$[$0-2], $$[$0], '*'); break; -case 53: +case 54: this.$ = new ast.CssCalcProductNode($$[$0-2], $$[$0], '/'); break; -case 54: +case 55: this.$ = new ast.CssCalcSumNode($$[$0-2], $$[$0], '+'); break; -case 55: +case 56: this.$ = new ast.CssCalcSumNode($$[$0-2], $$[$0], '-'); break; } }, -table: [{3:1,4:2,5:[1,3],6:4,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{1:[3]},{5:[1,46],6:47,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{1:[2,2]},o($Vw,[2,3]),o($Vx,[2,5]),o($Vx,[2,6]),o($Vx,[2,7]),o($Vx,[2,8]),o($Vx,[2,9]),o($Vx,[2,10]),o($Vx,[2,11]),o($Vx,[2,12]),o($Vx,[2,13]),o($Vx,[2,14]),o($Vx,[2,15]),o($Vx,[2,34]),o($Vx,[2,35]),o($Vx,[2,36]),o($Vx,[2,37]),o($Vx,[2,16]),o($Vx,[2,17]),o($Vx,[2,18]),o($Vx,[2,19]),o($Vx,[2,20]),o($Vx,[2,21]),o($Vx,[2,22]),o($Vx,[2,23]),o($Vx,[2,24]),o($Vx,[2,25]),o($Vx,[2,26]),o($Vx,[2,27]),o($Vx,[2,28]),o($Vx,[2,29]),o($Vx,[2,30]),o($Vx,[2,31]),o($Vx,[2,32]),o($Vx,[2,33]),{50:[1,48]},{6:50,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv,52:49,53:$Vy},{6:53,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,42:52,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{6:53,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,42:54,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{6:53,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,42:55,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{6:53,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,42:56,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{6:53,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,42:57,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{6:53,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,41:[1,58],42:59,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},{1:[2,1]},o($Vw,[2,4]),{41:[1,60],43:[1,61]},{41:[1,62],54:$Vz,55:$VA,56:$VB,57:$VC},o($VD,[2,50]),{6:50,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv,52:67,53:$Vy},{41:[1,68],43:$VE},o($VF,[2,40]),{41:[1,70],43:$VE},{41:[1,71],43:$VE},{41:[1,72],43:$VE},{41:[1,73],43:$VE},o($Vx,[2,38]),{41:[1,74],43:$VE},o($Vx,[2,47]),{6:75,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},o($Vx,[2,49]),{6:50,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv,52:76,53:$Vy},{6:50,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv,52:77,53:$Vy},{6:50,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv,52:78,53:$Vy},{6:50,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv,52:79,53:$Vy},{41:[1,80],54:$Vz,55:$VA,56:$VB,57:$VC},o($Vx,[2,42]),{6:81,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:$V3,16:$V4,17:$V5,18:$V6,19:$V7,20:$V8,21:$V9,22:$Va,23:$Vb,24:$Vc,25:$Vd,26:$Ve,27:$Vf,28:$Vg,29:$Vh,30:$Vi,31:$Vj,32:$Vk,33:$Vl,34:$Vm,35:$Vn,36:16,37:17,38:18,39:19,40:$Vo,44:$Vp,45:$Vq,46:$Vr,47:$Vs,48:$Vt,49:$Vu,51:$Vv},o($Vx,[2,43]),o($Vx,[2,44]),o($Vx,[2,45]),o($Vx,[2,46]),o($Vx,[2,39]),{41:[1,82]},o($VD,[2,52]),o($VD,[2,53]),o($VG,[2,54],{54:$Vz,55:$VA}),o($VG,[2,55],{54:$Vz,55:$VA}),o($VD,[2,51]),o($VF,[2,41]),o($Vx,[2,48])], -defaultActions: {3:[2,2],46:[2,1]}, +table: [{3:1,4:2,5:[1,3],6:4,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{1:[3]},{5:[1,47],6:48,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{1:[2,2]},o($Vw,[2,3]),o($Vx,[2,5]),o($Vx,[2,6]),o($Vx,[2,7]),o($Vx,[2,8]),o($Vx,[2,9]),o($Vx,[2,10]),o($Vx,[2,11]),o($Vx,[2,12]),o($Vx,[2,13]),o($Vx,[2,14]),o($Vx,[2,15]),o($Vx,[2,34]),o($Vx,[2,35]),o($Vx,[2,36]),o($Vx,[2,37]),o($Vx,[2,16]),o($Vx,[2,17]),o($Vx,[2,18]),o($Vx,[2,19]),o($Vx,[2,20]),o($Vx,[2,21]),o($Vx,[2,22]),o($Vx,[2,23]),o($Vx,[2,24]),o($Vx,[2,25]),o($Vx,[2,26]),o($Vx,[2,27]),o($Vx,[2,28]),o($Vx,[2,29]),o($Vx,[2,30]),o($Vx,[2,31]),o($Vx,[2,32]),o($Vx,[2,33]),{9:[1,49]},{51:[1,50]},{6:52,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv,53:51,54:$Vy},{6:55,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,42:54,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{6:55,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,42:56,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{6:55,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,42:57,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{6:55,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,42:58,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{6:55,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,42:59,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{6:55,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,41:[1,60],42:61,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},{1:[2,1]},o($Vw,[2,4]),{41:[1,62]},{41:[1,63],43:[1,64]},{41:[1,65],55:$Vz,56:$VA,57:$VB,58:$VC},o($VD,[2,51]),{6:52,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv,53:70,54:$Vy},{41:[1,71],43:$VE},o($VF,[2,40]),{41:[1,73],43:$VE},{41:[1,74],43:$VE},{41:[1,75],43:$VE},{41:[1,76],43:$VE},o($Vx,[2,38]),{41:[1,77],43:$VE},o($Vx,[2,42]),o($Vx,[2,48]),{6:78,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},o($Vx,[2,50]),{6:52,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv,53:79,54:$Vy},{6:52,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv,53:80,54:$Vy},{6:52,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv,53:81,54:$Vy},{6:52,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv,53:82,54:$Vy},{41:[1,83],55:$Vz,56:$VA,57:$VB,58:$VC},o($Vx,[2,43]),{6:84,7:5,8:6,9:$V0,10:$V1,11:$V2,12:10,13:11,14:12,15:13,16:$V3,17:$V4,18:$V5,19:$V6,20:$V7,21:$V8,22:$V9,23:$Va,24:$Vb,25:$Vc,26:$Vd,27:$Ve,28:$Vf,29:$Vg,30:$Vh,31:$Vi,32:$Vj,33:$Vk,34:$Vl,35:$Vm,36:16,37:17,38:18,39:19,40:$Vn,44:$Vo,45:$Vp,46:$Vq,47:$Vr,48:$Vs,49:$Vt,50:$Vu,52:$Vv},o($Vx,[2,44]),o($Vx,[2,45]),o($Vx,[2,46]),o($Vx,[2,47]),o($Vx,[2,39]),{41:[1,85]},o($VD,[2,53]),o($VD,[2,54]),o($VG,[2,55],{55:$Vz,56:$VA}),o($VG,[2,56],{55:$Vz,56:$VA}),o($VD,[2,52]),o($VF,[2,41]),o($Vx,[2,49])], +defaultActions: {3:[2,2],47:[2,1]}, parseError: function parseError(str, hash) { if (hash.recoverable) { this.trace(str); @@ -757,39 +757,39 @@ case 20:return 10 break; case 21:return 16; break; -case 22:return 15 +case 22:return 44 break; -case 23:return 51 +case 23:return 52 break; -case 24:return 49 +case 24:return 50 break; -case 25:return 44 +case 25:return 45 break; -case 26:return 45 +case 26:return 46 break; -case 27:return 46 +case 27:return 47 break; -case 28:return 47 +case 28:return 48 break; -case 29:return 48 +case 29:return 49 break; case 30:return 40 break; case 31:return 17 break; -case 32:return 50; +case 32:return 51; break; case 33:return 9 break; -case 34:return 56 +case 34:return 57 break; -case 35:return 57 +case 35:return 58 break; -case 36:return 54 +case 36:return 55 break; -case 37:return 55 +case 37:return 56 break; -case 38:return 53 +case 38:return 54 break; case 39:return 41 break; @@ -801,7 +801,7 @@ case 42:return 5 break; } }, -rules: [/^(?:\s+)/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Pp])([Xx]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Ee])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Rr])([Ee])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Hh]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Ww]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Mm])([Ii])([Nn]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Mm])([Aa])([Xx]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Cc])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Mm])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Qq]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Ii])([Nn]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Pp])([Cc]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Pp])([Tt]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Dd])([Ee])([Gg]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Rr])([Aa])([Dd]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Gg])([Rr])([Aa])([Dd]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Mm])([Ss]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Ss]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)%)/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)\b)/,/^(?:#([a-fA-F0-9]+))/,/^(?:([Uu])([Rr])([Ll])\(('[^\']*'|"[^\"]*")\))/,/^(?:([Cc])([Aa])([Ll])([Cc])\()/,/^(?:([Vv])([Aa])([Rr])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])([Xx])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])([Yy])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])([Zz])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])3([Dd])\()/,/^(?:(-?[a-zA-Z_][a-zA-Z0-9_]*)\()/,/^(?:(-?[a-zA-Z_][a-zA-Z0-9_]*))/,/^(?:--(-?[a-zA-Z_][a-zA-Z0-9_]*))/,/^(?:('[^\']*'|"[^\"]*"))/,/^(?:\+)/,/^(?:-)/,/^(?:\*)/,/^(?:\/)/,/^(?:\()/,/^(?:\))/,/^(?:,)/,/^(?:.)/,/^(?:$)/], +rules: [/^(?:\s+)/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Pp])([Xx]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Ee])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Rr])([Ee])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Hh]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Ww]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Mm])([Ii])([Nn]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Vv])([Mm])([Aa])([Xx]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Cc])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Mm])([Mm]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Qq]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Ii])([Nn]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Pp])([Cc]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Pp])([Tt]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Dd])([Ee])([Gg]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Rr])([Aa])([Dd]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Gg])([Rr])([Aa])([Dd]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Mm])([Ss]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)([Ss]))/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)%)/,/^(?:([+-]?[0-9]+(\.[0-9]+)?([eE][+\-]?[0-9]+)?|[+-]?\.[0-9]+([eE][+\-]?[0-9]+)?)\b)/,/^(?:#([a-fA-F0-9]+))/,/^(?:([Uu])([Rr])([Ll])\()/,/^(?:([Cc])([Aa])([Ll])([Cc])\()/,/^(?:([Vv])([Aa])([Rr])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])([Xx])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])([Yy])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])([Zz])\()/,/^(?:([Tt])([Rr])([Aa])([Nn])([Ss])([Ll])([Aa])([Tt])([Ee])3([Dd])\()/,/^(?:(-?[a-zA-Z_][a-zA-Z0-9_]*)\()/,/^(?:(-?[a-zA-Z_][a-zA-Z0-9_]*))/,/^(?:--(-?[a-zA-Z_][a-zA-Z0-9_]*))/,/^(?:('[^\']*'|"[^\"]*"))/,/^(?:\+)/,/^(?:-)/,/^(?:\*)/,/^(?:\/)/,/^(?:\()/,/^(?:\))/,/^(?:,)/,/^(?:.)/,/^(?:$)/], conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"inclusive":true}} }); return lexer; diff --git a/extensions/amp-animation/0.1/test/test-css-expr.js b/extensions/amp-animation/0.1/test/test-css-expr.js index b3168af182fa..78f484d7d161 100644 --- a/extensions/amp-animation/0.1/test/test-css-expr.js +++ b/extensions/amp-animation/0.1/test/test-css-expr.js @@ -18,72 +18,74 @@ import {parseCss} from '../css-expr'; import * as ast from '../css-expr-ast'; -/** - * @param {string} cssString - * @return {string} - */ -function parsePseudo(cssString) { - const node = parseCss(cssString); - if (node == null) { - return null; +describe('CSS parse', () => { + + /** + * @param {string} cssString + * @return {string} + */ + function parsePseudo(cssString) { + const node = parseCss(cssString); + if (node == null) { + return null; + } + return pseudo(node); } - return pseudo(node); -} -/** - * @param {!CssNode} n - * @return {string} - */ -function pseudo(n) { - if (n instanceof ast.CssPassthroughNode) { - return n.css_; - } - if (n instanceof ast.CssConcatNode) { - return `CON<${pseudoArray(n.array_)}>`; - } - if (n instanceof ast.CssNumericNode) { - return `${n.type_}<${n.num_}` + - `${n.units_ && n.units_ != '%' ? ' ' + n.units_.toUpperCase() : ''}>`; - } - if (n instanceof ast.CssTranslateNode) { - return 'TRANSLATE' + - `${n.suffix_ ? '-' + n.suffix_.toUpperCase() : ''}` + - `<${pseudoArray(n.args_)}>`; + /** + * @param {!CssNode} n + * @return {string} + */ + function pseudo(n) { + if (n instanceof ast.CssPassthroughNode) { + return n.css_; + } + if (n instanceof ast.CssUrlNode) { + return `URL<${n.url_}>`; + } + if (n instanceof ast.CssConcatNode) { + return `CON<${pseudoArray(n.array_)}>`; + } + if (n instanceof ast.CssNumericNode) { + return `${n.type_}<${n.num_}` + + `${n.units_ && n.units_ != '%' ? ' ' + n.units_.toUpperCase() : ''}>`; + } + if (n instanceof ast.CssTranslateNode) { + return 'TRANSLATE' + + `${n.suffix_ ? '-' + n.suffix_.toUpperCase() : ''}` + + `<${pseudoArray(n.args_)}>`; + } + if (n instanceof ast.CssVarNode) { + return `VAR<${n.varName_}${n.def_ ? ', ' + pseudo(n.def_) : ''}>`; + } + if (n instanceof ast.CssCalcNode) { + return `CALC<${pseudo(n.expr_)}>`; + } + if (n instanceof ast.CssCalcSumNode) { + return `${n.op_ == '+' ? 'ADD' : 'SUB'}` + + `<${pseudo(n.left_)}, ${pseudo(n.right_)}>`; + } + if (n instanceof ast.CssCalcProductNode) { + return `${n.op_ == '*' ? 'MUL' : 'DIV'}` + + `<${pseudo(n.left_)}, ${pseudo(n.right_)}>`; + } + if (n instanceof ast.CssFuncNode) { + return `${n.name_.toUpperCase()}<${pseudoArray(n.args_)}>`; + } + throw new Error('unknown node: ' + n); } - if (n instanceof ast.CssVarNode) { - return `VAR<${n.varName_}${n.def_ ? ', ' + pseudo(n.def_) : ''}>`; - } - if (n instanceof ast.CssCalcNode) { - return `CALC<${pseudo(n.expr_)}>`; - } - if (n instanceof ast.CssCalcSumNode) { - return `${n.op_ == '+' ? 'ADD' : 'SUB'}` + - `<${pseudo(n.left_)}, ${pseudo(n.right_)}>`; - } - if (n instanceof ast.CssCalcProductNode) { - return `${n.op_ == '*' ? 'MUL' : 'DIV'}` + - `<${pseudo(n.left_)}, ${pseudo(n.right_)}>`; - } - if (n instanceof ast.CssFuncNode) { - return `${n.name_.toUpperCase()}<${pseudoArray(n.args_)}>`; - } - throw new Error('unknown node: ' + n); -} -/** - * @param {!Array} array - * @return {string} - */ -function pseudoArray(array) { - if (!array || array.length == 0) { - return ''; + /** + * @param {!Array} array + * @return {string} + */ + function pseudoArray(array) { + if (!array || array.length == 0) { + return ''; + } + return array.map(n => pseudo(n)).join(', '); } - return array.map(n => pseudo(n)).join(', '); -} - - -describe('parse', () => { it('should parse empty string as null', () => { expect(parsePseudo('')).to.be.null; @@ -169,17 +171,16 @@ describe('parse', () => { it('should parse url', () => { expect(parsePseudo('url("https://acme.org/abc")')) - .to.equal('url("https://acme.org/abc")'); + .to.equal('URL'); expect(parsePseudo('url(\'https://acme.org/abc\')')) - .to.equal('url(\'https://acme.org/abc\')'); + .to.equal('URL'); expect(parsePseudo('url(\'data:abc\')')) - .to.equal('url(\'data:abc\')'); - }); - - it('should disallow non-https urls', () => { - expect(() => { - parsePseudo('url("http://acme.org/abc")'); - }).to.throw(/https/); + .to.equal('URL'); + // HTTP and relative are allowed at this stage. + expect(parsePseudo('url(\'http://acme.org/abc\')')) + .to.equal('URL'); + expect(parsePseudo('url(\'/relative\')')) + .to.equal('URL'); }); it('should parse hexcolor', () => { @@ -304,3 +305,881 @@ describe('parse', () => { .to.equal('CALC>, LEN<200 PX>>>'); }); }); + + +describes.sandboxed('CSS resolve', {}, () => { + let context; + let contextMock; + + beforeEach(() => { + context = new ast.CssContext(); + contextMock = sandbox.mock(context); + }); + + afterEach(() => { + contextMock.verify(); + }); + + function resolvedCss(node) { + const resolved = node.resolve(context); + return resolved ? resolved.css() : null; + } + + it('should not resolve value for a const', () => { + const node = new ast.CssNode(); + const nodeMock = sandbox.mock(node); + nodeMock.expects('css').returns('CSS').atLeast(1); + nodeMock.expects('isConst').returns(true).atLeast(1); + nodeMock.expects('calc').never(); + expect(node.css()).to.equal('CSS'); + expect(node.resolve(context)).to.equal(node); + expect(resolvedCss(node)).to.equal('CSS'); + nodeMock.verify(); + }); + + it('should resolve value for a non-const', () => { + const node = new ast.CssNode(); + const node2 = new ast.CssNode(); + node2.css = () => 'CSS2'; + const nodeMock = sandbox.mock(node); + nodeMock.expects('isConst').returns(false).atLeast(1); + nodeMock.expects('css').returns('CSS').atLeast(1); + nodeMock.expects('calc').returns(node2).atLeast(1); + expect(node.css()).to.equal('CSS'); + expect(node.resolve(context)).to.equal(node2); + expect(resolvedCss(node)).to.equal('CSS2'); + nodeMock.verify(); + }); + + it('should resolve a const concat node', () => { + const node = new ast.CssConcatNode([ + new ast.CssPassthroughNode('css1'), + new ast.CssPassthroughNode('css2'), + ]); + expect(node.isConst()).to.be.true; + expect(node.resolve(context)).to.equal(node); + expect(node.css()).to.equal('css1 css2'); + expect(resolvedCss(node)).to.equal('css1 css2'); + }); + + it('should resolve a var concat node', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssPassthroughNode('val1')) + .once(); + const node = new ast.CssConcatNode([ + new ast.CssPassthroughNode('css1'), + new ast.CssVarNode('--var1'), + ]); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('css1 var(--var1)'); + expect(resolvedCss(node)).to.equal('css1 val1'); + }); + + it('should resolve a null concat node', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(null) + .once(); + const node = new ast.CssConcatNode([ + new ast.CssPassthroughNode('css1'), + new ast.CssVarNode('--var1'), + ]); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('css1 var(--var1)'); + expect(resolvedCss(node)).to.be.null; + }); + + it('should resolve a number node', () => { + const node = new ast.CssNumberNode(11.5); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5'); + expect(resolvedCss(node)).to.equal('11.5'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5'); + }); + + it('should resolve a percent node', () => { + const node = new ast.CssPercentNode(11.5); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5%'); + expect(resolvedCss(node)).to.equal('11.5%'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5%'); + }); + + describe('url', () => { + it('should resolve an absolute HTTPS URL', () => { + const node = new ast.CssUrlNode('https://acme.org/img'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('url("https://acme.org/img")'); + }); + + it('should resolve an data URL', () => { + const node = new ast.CssUrlNode('data:abc'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('url("data:abc")'); + }); + + it('should resolve an empty URL', () => { + const node = new ast.CssUrlNode(''); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal(''); + }); + + it('should re-resolve an HTTP url', () => { + contextMock + .expects('resolveUrl') + .withExactArgs('http://acme.org/non-secure') + .returns('broken') + .once(); + const node = new ast.CssUrlNode('http://acme.org/non-secure'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('url("http://acme.org/non-secure")'); + expect(node.calc(context).css()).to.equal('url("broken")'); + }); + + it('should re-resolve a relative url', () => { + contextMock + .expects('resolveUrl') + .withExactArgs('/relative') + .returns('https://acme.org/relative') + .once(); + const node = new ast.CssUrlNode('/relative'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('url("/relative")'); + expect(node.calc(context).css()) + .to.equal('url("https://acme.org/relative")'); + }); + + it('should fail when context resolution fails', () => { + contextMock + .expects('resolveUrl') + .withExactArgs('http://acme.org/non-secure') + .throws(new Error('intentional')) + .once(); + const node = new ast.CssUrlNode('http://acme.org/non-secure'); + expect(() => { + node.calc(context); + }).to.throw(/intentional/); + }); + }); + + describe('length', () => { + it('should resolve a px-length node', () => { + const node = new ast.CssLengthNode(11.5, 'px'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5px'); + expect(resolvedCss(node)).to.equal('11.5px'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5px'); + expect(node.norm()).to.equal(node); + expect(node.norm().css()).to.equal('11.5px'); + }); + + it('should resolve a em-length node', () => { + const node = new ast.CssLengthNode(11.5, 'em'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5em'); + expect(resolvedCss(node)).to.equal('11.5em'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5em'); + + contextMock.expects('getCurrentFontSize').returns(10).once(); + const norm = node.norm(context); + expect(norm).to.not.equal(node); + expect(norm.css()).to.equal('115px'); + }); + + it('should resolve a rem-length node', () => { + const node = new ast.CssLengthNode(11.5, 'rem'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5rem'); + expect(resolvedCss(node)).to.equal('11.5rem'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5rem'); + + contextMock.expects('getRootFontSize').returns(2).once(); + const norm = node.norm(context); + expect(norm).to.not.equal(node); + expect(norm.css()).to.equal('23px'); + }); + + describe('vw-length', () => { + beforeEach(() => { + contextMock.expects('getViewportSize') + .returns({width: 200, height: 400}) + .once(); + }); + + it('should resolve a vw-length node', () => { + const node = new ast.CssLengthNode(11.5, 'vw'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5vw'); + expect(resolvedCss(node)).to.equal('11.5vw'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5vw'); + expect(node.norm(context).css()).to.equal('23px'); + }); + + it('should resolve a vh-length node', () => { + const node = new ast.CssLengthNode(11.5, 'vh'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5vh'); + expect(resolvedCss(node)).to.equal('11.5vh'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5vh'); + expect(node.norm(context).css()).to.equal('46px'); + }); + + it('should resolve a vmin-length node', () => { + const node = new ast.CssLengthNode(11.5, 'vmin'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5vmin'); + expect(resolvedCss(node)).to.equal('11.5vmin'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5vmin'); + expect(node.norm(context).css()).to.equal('23px'); + }); + + it('should resolve a vmax-length node', () => { + const node = new ast.CssLengthNode(11.5, 'vmax'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5vmax'); + expect(resolvedCss(node)).to.equal('11.5vmax'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5vmax'); + expect(node.norm(context).css()).to.equal('46px'); + }); + }); + + it('should disallow a real-length node', () => { + expect(() => { + new ast.CssLengthNode(1, 'cm').norm(context); + }).to.throw(/unknown/); + expect(() => { + new ast.CssLengthNode(1, 'mm').norm(context); + }).to.throw(/unknown/); + expect(() => { + new ast.CssLengthNode(1, 'in').norm(context); + }).to.throw(/unknown/); + expect(() => { + new ast.CssLengthNode(1, 'pt').norm(context); + }).to.throw(/unknown/); + expect(() => { + new ast.CssLengthNode(1, 'q').norm(context); + }).to.throw(/unknown/); + }); + + it('should resolve a percent-length in w-direction', () => { + contextMock.expects('getDimension').returns('w'); + contextMock.expects('getCurrentElementSize') + .returns({width: 110, height: 220}) + .once(); + const node = new ast.CssLengthNode(11.5, 'px'); + const percent = node.calcPercent(10, context); + expect(percent.css()).to.equal('11px'); + }); + + it('should resolve a percent-length in h-direction', () => { + contextMock.expects('getDimension').returns('h'); + contextMock.expects('getCurrentElementSize') + .returns({width: 110, height: 220}) + .once(); + const node = new ast.CssLengthNode(11.5, 'px'); + const percent = node.calcPercent(10, context); + expect(percent.css()).to.equal('22px'); + }); + + it('should resolve a percent-length in unknown direction', () => { + contextMock.expects('getCurrentElementSize') + .returns({width: 110, height: 220}) + .once(); + const node = new ast.CssLengthNode(11.5, 'px'); + const percent = node.calcPercent(10, context); + expect(percent.css()).to.equal('0px'); + }); + }); + + describe('angle', () => { + it('should resolve a rad-angle node', () => { + const node = new ast.CssAngleNode(11.5, 'rad'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5rad'); + expect(resolvedCss(node)).to.equal('11.5rad'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5rad'); + expect(node.norm()).to.equal(node); + expect(node.norm().css()).to.equal('11.5rad'); + }); + + it('should resolve a deg-length node', () => { + const node = new ast.CssAngleNode(11.5, 'deg'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5deg'); + expect(resolvedCss(node)).to.equal('11.5deg'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5deg'); + + const norm = node.norm(context); + expect(norm).to.not.equal(node); + expect(norm.css()).to.match(/[\d\.]*rad/); + expect(norm.num_).to.closeTo(0.2007, 1e-4); + }); + + it('should resolve a grad-length node', () => { + const node = new ast.CssAngleNode(11.5, 'grad'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5grad'); + expect(resolvedCss(node)).to.equal('11.5grad'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5grad'); + + const norm = node.norm(context); + expect(norm).to.not.equal(node); + expect(norm.css()).to.match(/[\d\.]*rad/); + expect(norm.num_).to.closeTo(0.1806, 1e-4); + }); + }); + + describe('time', () => { + it('should resolve a milliseconds node', () => { + const node = new ast.CssTimeNode(11.5, 'ms'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5ms'); + expect(resolvedCss(node)).to.equal('11.5ms'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5ms'); + expect(node.norm()).to.equal(node); + expect(node.norm().css()).to.equal('11.5ms'); + }); + + it('should resolve a seconds node', () => { + const node = new ast.CssTimeNode(11.5, 's'); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('11.5s'); + expect(resolvedCss(node)).to.equal('11.5s'); + expect(node.createSameUnits(20.5).css()).to.equal('20.5s'); + + const norm = node.norm(context); + expect(norm).to.not.equal(node); + expect(norm.css()).to.equal('11500ms'); + }); + }); + + describe('function', () => { + it('should resolve a const-arg function', () => { + contextMock.expects('pushDimension').never(); + contextMock.expects('popDimension').never(); + const node = new ast.CssFuncNode('rgb', [ + new ast.CssNumberNode(201), + new ast.CssNumberNode(202), + new ast.CssNumberNode(203), + ]); + expect(node.isConst()).to.be.true; + expect(node.css()).to.equal('rgb(201,202,203)'); + expect(resolvedCss(node)).to.equal('rgb(201,202,203)'); + expect(node.resolve(context)).to.equal(node); + }); + + it('should resolve a var-arg function', () => { + contextMock.expects('pushDimension').never(); + contextMock.expects('popDimension').never(); + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssNumberNode(11)) + .once(); + const node = new ast.CssFuncNode('rgb', [ + new ast.CssNumberNode(201), + new ast.CssNumberNode(202), + new ast.CssVarNode('--var1'), + ]); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('rgb(201,202,var(--var1))'); + expect(resolvedCss(node)).to.equal('rgb(201,202,11)'); + }); + + it('should push a dimension when specified', () => { + let index = 0; + const stack = []; + context.pushDimension = function(dim) { + stack.push(dim); + }; + context.popDimension = function() { + stack.pop(); + }; + + const arg1 = new ast.CssNumberNode(201); + arg1.resolve = function() { + expect(index).to.equal(0); + expect(stack).to.deep.equal(['w']); + index++; + return new ast.CssPassthroughNode('w'); + }; + + const arg2 = new ast.CssNumberNode(202); + arg2.resolve = function() { + expect(index).to.equal(1); + expect(stack).to.deep.equal(['h']); + index++; + return new ast.CssPassthroughNode('h'); + }; + + const arg3 = new ast.CssNumberNode(203); + arg3.resolve = function() { + expect(index).to.equal(2); + expect(stack).to.deep.equal([]); + index++; + return new ast.CssPassthroughNode('-'); + }; + + const node = new ast.CssFuncNode('rgb', + [arg1, arg2, arg3], ['w', 'h']); + expect(node.calc(context).css()).to.equal('rgb(w,h,-)'); + expect(index).to.equal(3); + }); + + it('should resolve a var-arg function with nulls', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(null) + .once(); + const node = new ast.CssFuncNode('rgb', [ + new ast.CssNumberNode(201), + new ast.CssNumberNode(202), + new ast.CssVarNode('--var1'), + ]); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('rgb(201,202,var(--var1))'); + expect(resolvedCss(node)).to.be.null; + }); + }); + + describe('translate', () => { + let dimStack; + + beforeEach(() => { + dimStack = []; + context.pushDimension = function(dim) { + dimStack.push(dim); + }; + context.popDimension = function() { + dimStack.pop(); + }; + sandbox.stub(ast.CssPassthroughNode.prototype, 'resolve', function() { + return new ast.CssPassthroughNode(this.css_ + dimStack.join('')); + }); + }); + + it('should resolve 2d translate', () => { + const node = new ast.CssTranslateNode('', [ + new ast.CssPassthroughNode('X'), + new ast.CssPassthroughNode('Y'), + ]); + expect(node.calc(context).css()).to.equal('translate(Xw,Yh)'); + }); + + it('should resolve abbreviated 2d translate', () => { + const node = new ast.CssTranslateNode('', [ + new ast.CssPassthroughNode('X'), + ]); + expect(node.calc(context).css()).to.equal('translate(Xw)'); + }); + + it('should resolve translateX', () => { + const node = new ast.CssTranslateNode('x', [ + new ast.CssPassthroughNode('X'), + ]); + expect(node.calc(context).css()).to.equal('translatex(Xw)'); + }); + + it('should resolve translateY', () => { + const node = new ast.CssTranslateNode('y', [ + new ast.CssPassthroughNode('Y'), + ]); + expect(node.calc(context).css()).to.equal('translatey(Yh)'); + }); + + it('should resolve translateZ', () => { + const node = new ast.CssTranslateNode('z', [ + new ast.CssPassthroughNode('Z'), + ]); + expect(node.calc(context).css()).to.equal('translatez(Zz)'); + }); + + it('should resolve translate3d', () => { + const node = new ast.CssTranslateNode('3d', [ + new ast.CssPassthroughNode('X'), + new ast.CssPassthroughNode('Y'), + new ast.CssPassthroughNode('Z'), + ]); + expect(node.calc(context).css()).to.equal('translate3d(Xw,Yh,Zz)'); + }); + + it('should resolve translate with null args to null', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(null) + .once(); + const node = new ast.CssTranslateNode('', [ + new ast.CssPassthroughNode('X'), + new ast.CssVarNode('--var1'), + ]); + expect(node.calc(context)).to.be.null; + }); + }); + + describe('var', () => { + it('should resolve a var node', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssPassthroughNode('val1')) + .once(); + const node = new ast.CssVarNode('--var1'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1)'); + expect(resolvedCss(node)).to.equal('val1'); + }); + + it('should resolve a var node with def', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssPassthroughNode('val1')) + .once(); + const node = new ast.CssVarNode('--var1', + new ast.CssPassthroughNode('10px')); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1,10px)'); + expect(resolvedCss(node)).to.equal('val1'); + }); + + it('should resolve a var node by fallback to def', () => { + contextMock.expects('getVar').returns(null).once(); + const node = new ast.CssVarNode('--var1', + new ast.CssPassthroughNode('10px')); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1,10px)'); + expect(resolvedCss(node)).to.equal('10px'); + }); + + it('should resolve a var node with def as var', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(null) + .once(); + contextMock.expects('getVar') + .withExactArgs('--var2') + .returns(new ast.CssPassthroughNode('val2')) + .once(); + const node = new ast.CssVarNode('--var1', + new ast.CssVarNode('--var2')); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1,var(--var2))'); + expect(resolvedCss(node)).to.equal('val2'); + }); + + it('should resolve a var node w/o fallback to def', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(null) + .atLeast(1); + const node = new ast.CssVarNode('--var1'); + expect(node.css()).to.equal('var(--var1)'); + expect(node.isConst()).to.be.false; + expect(node.resolve(context)).to.be.null; + expect(node.calc(context)).to.be.null; + expect(resolvedCss(node)).to.be.null; + }); + }); + + describe('calc', () => { + it('should resolve a single-value calc', () => { + const node = new ast.CssCalcNode(new ast.CssLengthNode(10, 'px')); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('calc(10px)'); + expect(resolvedCss(node)).to.equal('10px'); + }); + + describe('sum', () => { + it('should add two same-unit values', () => { + const node = new ast.CssCalcSumNode( + new ast.CssLengthNode(10, 'px'), + new ast.CssLengthNode(20, 'px'), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10px + 20px'); + expect(resolvedCss(node)).to.equal('30px'); + }); + + it('should add two same-unit values - times', () => { + const node = new ast.CssCalcSumNode( + new ast.CssTimeNode(10, 's'), + new ast.CssTimeNode(20, 's'), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10s + 20s'); + expect(resolvedCss(node)).to.equal('30s'); + }); + + it('should subtract two same-unit values', () => { + const node = new ast.CssCalcSumNode( + new ast.CssLengthNode(30, 'px'), + new ast.CssLengthNode(20, 'px'), + '-'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('30px - 20px'); + expect(resolvedCss(node)).to.equal('10px'); + }); + + it('should resolve both parts', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssLengthNode(5, 'px')) + .once(); + contextMock.expects('getVar') + .withExactArgs('--var2') + .returns(new ast.CssLengthNode(1, 'px')) + .once(); + const node = new ast.CssCalcSumNode( + new ast.CssVarNode('--var1'), + new ast.CssVarNode('--var2'), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1) + var(--var2)'); + expect(resolvedCss(node)).to.equal('6px'); + }); + + it('should resolve to null with null args', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssLengthNode(5, 'px')) + .once(); + contextMock.expects('getVar') + .withExactArgs('--var2') + .returns(null) + .once(); + const node = new ast.CssCalcSumNode( + new ast.CssVarNode('--var1'), + new ast.CssVarNode('--var2'), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1) + var(--var2)'); + expect(resolvedCss(node)).to.be.null; + }); + + it('should only allow numerics', () => { + const node = new ast.CssCalcSumNode( + new ast.CssPassthroughNode('A'), + new ast.CssPassthroughNode('B'), + '+'); + expect(() => { + resolvedCss(node); + }).to.throw(/both numerical/); + }); + + it('should only allow same-type', () => { + const node = new ast.CssCalcSumNode( + new ast.CssLengthNode(30, 'px'), + new ast.CssTimeNode(20, 's'), + '+'); + expect(() => { + resolvedCss(node); + }).to.throw(/same type/); + }); + + it('should normalize units', () => { + contextMock.expects('getCurrentFontSize').returns(1).once(); + contextMock.expects('getRootFontSize').returns(2).once(); + const node = new ast.CssCalcSumNode( + new ast.CssLengthNode(10, 'em'), + new ast.CssLengthNode(10, 'rem'), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10em + 10rem'); + // 1 * 10px + 2 * 10px = 30px + expect(resolvedCss(node)).to.equal('30px'); + }); + + it('should resolve left as percent', () => { + contextMock.expects('getDimension').returns('w'); + contextMock.expects('getCurrentElementSize') + .returns({width: 110, height: 220}) + .once(); + const node = new ast.CssCalcSumNode( + new ast.CssPercentNode(10), + new ast.CssLengthNode(10, 'px'), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10% + 10px'); + // 10% * 110 + 10px = 11px + 10px = 21px + expect(resolvedCss(node)).to.equal('21px'); + }); + + it('should resolve right as percent', () => { + contextMock.expects('getDimension').returns('h'); + contextMock.expects('getCurrentElementSize') + .returns({width: 110, height: 220}) + .once(); + const node = new ast.CssCalcSumNode( + new ast.CssLengthNode(10, 'px'), + new ast.CssPercentNode(10), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10px + 10%'); + // 10px + 10% * 220 = 10px + 22px = 32px + expect(resolvedCss(node)).to.equal('32px'); + }); + + it('should normalize the non-percent part', () => { + contextMock.expects('getCurrentFontSize').returns(2).once(); + contextMock.expects('getDimension').returns('h'); + contextMock.expects('getCurrentElementSize') + .returns({width: 110, height: 220}) + .once(); + const node = new ast.CssCalcSumNode( + new ast.CssLengthNode(10, 'em'), + new ast.CssPercentNode(10), + '+'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10em + 10%'); + // 10em * 2px + 10% * 220 = 20px + 22px = 42px + expect(resolvedCss(node)).to.equal('42px'); + }); + }); + + describe('product', () => { + it('should only allow numerics', () => { + const node = new ast.CssCalcProductNode( + new ast.CssPassthroughNode('A'), + new ast.CssPassthroughNode('B'), + '*'); + expect(() => { + resolvedCss(node); + }).to.throw(/both numerical/); + }); + + it('should multiply with right number', () => { + const node = new ast.CssCalcProductNode( + new ast.CssLengthNode(10, 'px'), + new ast.CssNumberNode(2), + '*'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10px * 2'); + expect(resolvedCss(node)).to.equal('20px'); + }); + + it('should multiply with left number', () => { + const node = new ast.CssCalcProductNode( + new ast.CssNumberNode(2), + new ast.CssLengthNode(10, 'px'), + '*'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('2 * 10px'); + expect(resolvedCss(node)).to.equal('20px'); + }); + + it('should multiply for non-norm', () => { + const node = new ast.CssCalcProductNode( + new ast.CssLengthNode(10, 'em'), + new ast.CssNumberNode(2), + '*'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10em * 2'); + expect(resolvedCss(node)).to.equal('20em'); + }); + + it('should multiply for time', () => { + const node = new ast.CssCalcProductNode( + new ast.CssTimeNode(10, 's'), + new ast.CssNumberNode(2), + '*'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10s * 2'); + expect(resolvedCss(node)).to.equal('20s'); + }); + + it('should require at least one number', () => { + const node = new ast.CssCalcProductNode( + new ast.CssLengthNode(10, 'px'), + new ast.CssLengthNode(20, 'px'), + '*'); + expect(() => { + resolvedCss(node); + }).to.throw(/one of sides in multiplication must be a number/); + }); + + it('should divide with right number', () => { + const node = new ast.CssCalcProductNode( + new ast.CssLengthNode(10, 'px'), + new ast.CssNumberNode(2), + '/'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10px / 2'); + expect(resolvedCss(node)).to.equal('5px'); + }); + + it('should divide for non-norm', () => { + const node = new ast.CssCalcProductNode( + new ast.CssLengthNode(10, 'em'), + new ast.CssNumberNode(2), + '/'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10em / 2'); + expect(resolvedCss(node)).to.equal('5em'); + }); + + it('should divide for time', () => { + const node = new ast.CssCalcProductNode( + new ast.CssTimeNode(10, 's'), + new ast.CssNumberNode(2), + '/'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10s / 2'); + expect(resolvedCss(node)).to.equal('5s'); + }); + + it('should only allow number denominator', () => { + const node = new ast.CssCalcProductNode( + new ast.CssTimeNode(10, 's'), + new ast.CssTimeNode(2, 's'), + '/'); + expect(() => { + resolvedCss(node); + }).to.throw(/denominator must be a number/); + }); + + it('should resolve divide-by-zero as null', () => { + const node = new ast.CssCalcProductNode( + new ast.CssLengthNode(10, 'px'), + new ast.CssNumberNode(0), + '/'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('10px / 0'); + expect(resolvedCss(node)).to.be.null; + }); + + it('should resolve both parts', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssLengthNode(10, 'px')) + .once(); + contextMock.expects('getVar') + .withExactArgs('--var2') + .returns(new ast.CssNumberNode(2)) + .once(); + const node = new ast.CssCalcProductNode( + new ast.CssVarNode('--var1'), + new ast.CssVarNode('--var2'), + '*'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1) * var(--var2)'); + expect(resolvedCss(node)).to.equal('20px'); + }); + + it('should resolve to null for null args', () => { + contextMock.expects('getVar') + .withExactArgs('--var1') + .returns(new ast.CssLengthNode(10, 'px')) + .once(); + contextMock.expects('getVar') + .withExactArgs('--var2') + .returns(null) + .once(); + const node = new ast.CssCalcProductNode( + new ast.CssVarNode('--var1'), + new ast.CssVarNode('--var2'), + '*'); + expect(node.isConst()).to.be.false; + expect(node.css()).to.equal('var(--var1) * var(--var2)'); + expect(resolvedCss(node)).to.be.null; + }); + }); + }); +});