UNPKG

imba

Version:

Intuitive and powerful language for building webapps that fly

1,364 lines (1,174 loc) 32.3 kB
export function defineStyleNodes(deps) { const { AST, Node, ListNode, ValueNode, LIT, C, M, SourceMapper, StyleRule, StyleTheme, Color, colord, constants, helpers, iter$, idx$, STACK, TagDeclaration, TagLike, ClassBody, TagBody, TagFlag, TagStyleAttr, } = deps; class StyleNode extends Node {} class StyleSelector extends StyleNode {} const stylePropertyCache = new Map(); function parseStyleUnitName(value) { let len = value.length; let i = 0; while (i < len) { let code = value.charCodeAt(i); if (code < 48 || code > 57) break; i++; } if (i == 0 || i == len) return null; let j = i; while (j < len) { let code = value.charCodeAt(j); if (!((code >= 65 && code <= 90) || (code >= 97 && code <= 122))) { return null; } j++; } return { number: parseInt(value.slice(0, i)), unit: value.slice(i), }; } function hasStyleNameStart(value) { let code = value.charCodeAt(0); return ( code == 35 || code == 45 || code == 95 || (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) ); } function parseStylePropertyMetadata(raw, constants) { let cached = stylePropertyCache.get(raw); if (cached) return cached; let parts = raw.replace(/(^|\b)\$/g, "--").split(/\b(?=[\^\.\@\!])/g); for (let i = 0, len = parts.length; i < len; i++) { let part = parts[i]; if (part[0] == "." && part[1] && part[1] != ".") { parts[i] = "@." + part.slice(1); } } let name = String(parts[0]); let kind = null; let colorError = false; if (raw[0] == "#") { kind = "color"; colorError = constants.HEX_REGEX.test(name); } let unit = parseStyleUnitName(name); let number = unit && unit.number; let unitName = unit && unit.unit; if (!hasStyleNameStart(name)) { parts = [null].concat(parts); name = null; } cached = { parts, name, kind, number, unit: unitName, colorError, }; stylePropertyCache.set(raw, cached); return cached; } // all weird parts of a selector? Or do we just compile it? class StyleRuleSet extends StyleNode { constructor(selectors, body) { super(...arguments); this._placeholders = []; this._selectors = selectors; this._body = body; } isStatic() { return true; } isGlobal() { return !!this.option("global"); } addPlaceholder(item) { this._placeholders.push(item); return this; } cssid() { return ( this._cssid || (this._cssid = "" + STACK.root().sourceId() + "-" + this.tid()) ); } visit(stack, o) { let cmp = (this._tagDeclaration = stack.up(TagDeclaration)); let tags = stack.parents(TagLike); if (tags[0] && cmp && tags[0].isSelf() && tags[1]) { tags[0] = cmp; } if (tags.length == 0 && cmp) { tags = [cmp]; } this._css = {}; this._flag = stack.up(TagFlag); this._tag = this._flag && this._flag._tag; let keywordName = String(this.option("name") || ""); if (keywordName[0] == "%") { // need to be safely converted to a reference? Can do that later this._mixin = this.scope__().mixin(keywordName.slice(1)); ((this._mixin._rule = this), this._mixin); this._mixin._options.id = this.cssid(); } if (this.option("export")) { STACK.root().mixinExports().add(this._mixin.name(), this._mixin._options); } let sel = String(this._selectors).trim(); if (stack.parent() instanceof ClassBody) { // Declaration in a tag declaration let owner = stack.up(2); if (owner instanceof TagDeclaration) { this._css.type = "component"; if (!this._variable) { this._sel = sel || "&"; this._css.scope = cmp; } } else { throw "css not allowed in class declaration"; } } else if (stack.parent() instanceof TagBody) { this._tag = tags[tags.length - 1]; this._sel = sel || "&"; this._css.type = "scoped"; this._css.scope = this._tag; // FIX the selector based on the tag } else if (this.option("toplevel")) { let inbody = stack.up(TagBody); if (inbody) { // Inside some logical nesting this._tag = stack.up(TagLike); this._sel = sel || "&"; this._css.scope = this._tag; this._css.ns = this.cssid(); this._css.id = this.cssid(); this._css.type = "scoped"; this._name = this.cssid(); this.set({ inTagTree: true }); } else { this._css.scope = this.isGlobal() ? null : this.scope__().closure(); this._sel || (this._sel = sel); } } else if (o.rule) { this._sel || (this._sel = this._selectors && this._selectors.toString && this._selectors.toString().trim()); // console.log "inside other rule? {@sel} | {o:rule.@sel} |" if (this._sel.indexOf("&") == -1) { this._sel = "& " + this._sel; } } else if ( !this._name && this._tag && this._flag && !this._flag._condition ) { this._css.scope = this._tag; this._name = this._tag.cssid(); this._sel = "&"; } else if (!this._name) { this._name = this.cssid(); // (cmp ? (cmp.cssns + oid) : (sourceId + oid)) this._sel = "." + this._name; } this._selectors && this._selectors.traverse && this._selectors.traverse(); this._styles = {}; this._body && this._body.traverse && this._body.traverse({ rule: this, styles: this._styles, rootRule: o.rule || this, }); // add the placeholderes if (this._placeholders.length) { if (this.option("inTagTree")) { for ( let i = 0, items = iter$(this._placeholders), len = items.length, ph; i < len; i++ ) { ph = items[i]; let setter = new TagStyleAttr(ph.name()); setter._tag = this._tag; ((setter._value = ph.runtimeValue()), setter); setter.set({ propname: ph._propname, unit: ph.option("unit"), styleterm: ph, }); ph._setter = setter; setter.traverse(); } } else if (!this._flag) { for ( let i = 0, items = iter$(this._placeholders), len = items.length; i < len; i++ ) { items[i].warn("Only allowed inside tag tree"); } } } if (o.rule && o.styles) { if (o.styles[this._sel]) { let base = o.styles[this._sel]; helpers.deepAssign(base, this._styles); } else { o.styles[this._sel] = this._styles; } } else { let component = this._tagDeclaration; let opts = { selectors: [], ns: this._css.ns, id: this._css.id, type: this._css.type, scope: this._css.scope, tags: tags, component: cmp, inline: !!this._flag, global: !!this.isGlobal(), mixins: {}, apply: {}, depth: this._tag ? this._tag._level : 0, }; this._css = new StyleRule(null, this._sel, this._styles, opts).toString(); STACK._css.add(this._css, opts); } return this; } toRaw() { return "" + this._name; } c() { if (this.option("toplevel") && this.option("export")) { // console.log "EXPORT??!",@identifier,@mixin,@name return ""; } if (this._tvar) { let out = ["" + this._tvar + " = '" + this._name + "'"]; let add = function (_0) { return out.push(_0); }; let cvar = this._tag.cvar(); let bvar = this._tag.bvar(); for ( let i = 0, items = iter$(this._placeholders), len = items.length; i < len; i++ ) { let item = items[i]._setter; // TODO - this logic should definitely move into TagAttr.c let iref = "" + cvar + "[" + item.osym() + "]"; let val = item.value(); // TODO optimize the css variable setters if (true) { add("" + M(item.js(this._o), item)); } } // console.log out.join('\n'),STACK.isExpression let expr = STACK.isExpression(); return expr ? "(" + out.join(",") + ")" : out.join(";\n"); // return "{@tvar} = '{@flagIf}'" } if (STACK.tsc() && this._placeholders.length) { let out = []; for ( let i = 0, items = iter$(this._placeholders), len = items.length; i < len; i++ ) { out.push(items[i].runtimeValue().c()); } let expr = STACK.isExpression(); return expr ? "(" + out.join(",") + ")" : out.join(";\n"); } if ( this.option("inClassBody") || this.option("inTagTree") || this.option("toplevel") ) { return ""; } let out = "'" + this._name + "'"; return out; } } // nodes # bunch of style properties and potentially nested rules class StyleBody extends ListNode { visit() { let items = this._nodes; let i = 0; let prevname; for (let j = 0, ary = iter$(items), len = ary.length, item; j < len; j++) { item = ary[j]; if (!(item instanceof StyleDeclaration)) { continue; } if (!item._property._name) { item._property.setName(prevname); } prevname = item._property._name; } while (i < items.length) { let item = items[i]; let res = item.traverse(); if (res != item) { if (res instanceof Array) { items.splice.apply(items, [].concat([i, 1], Array.from(res))); continue; } } // has changed? if (item == items[i]) { i++; } } return this; } toJSON() { return this.values(); } } class StyleDeclaration extends StyleNode { constructor(property, expr) { super(...arguments); this._property = property; this._expr = expr instanceof StyleExpressions ? expr : new StyleExpressions(expr); this; } clone(name, params) { if (params == null) { params = this._expr.clone(); } if (typeof params == "string" || typeof params == "number") { params = [params]; } if ( !(params instanceof Array) && (!(params instanceof ListNode) || params instanceof StyleOperation) ) { params = [params]; } return new StyleDeclaration(this._property.clone(name), params); } visit(stack, o) { // see if property can be expanded var self = this, v_; let theme = stack.theme(); let list = stack.parent(); let name = String(self._property.name()); let alias = theme.expandProperty(name); if (self._expr) { self._expr.traverse({ rule: o.rule, rootRule: o.rootRule, decl: self, property: self._property, }); } if (alias instanceof Array) { list.replace( self, alias.map(function (_0) { return self.clone(_0); }), ); return; } else if (alias && alias != name) { self._property = self._property.clone(alias); } let method = String(alias || name).replace(/-/g, "_"); if (self._expr) { self._expr.traverse({ decl: self, property: self._property }); } let res; let expanded = []; if (self._property.isColor && self._property.isColor()) { res = theme.colormix(name.slice(1), self._expr.toArray()); // console.log "prop is color!!!",res } else if (theme[method] && !self.option("plain")) { res = theme[method].apply(theme, self._expr.toArray()); } if (res instanceof Array) { self._expr = new StyleExpressions(res); } else if (res instanceof Object) { for ( let v, i = 0, keys = Object.keys(res), l = keys.length, k; i < l; i++ ) { k = keys[i]; v = res[k]; if (k.indexOf("&") >= 0) { let body = new StyleBody([]); let rule = new StyleRuleSet(LIT(k), body); expanded.push(rule); for ( let v2, j = 0, keys1 = Object.keys(v), l = keys1.length, k2; j < l; j++ ) { // need recursive thing here k2 = keys1[j]; v2 = v[k2]; body.add(self.clone(k2, v2)); } } else { expanded.push( self.clone(k, v).set({ plain: k == name || k == alias }), ); } } list.replace(self, expanded); return; } if (self._expr) { self._expr.traverse({ decl: self, property: self._property }); self._expr.set({ parens: false }); } if (o.styles) { let key = self._property.toKey(); let val = self._expr; if (o.selector) { key = JSON.stringify([o.selector, key]); } if (self._property.isUnit()) { if (self._property.number() != 1) { val = LIT( "calc(" + val.c() + " / " + self._property.number() + ")", ); } } // if this key has already been set we need to delete it // because we rely on the key order of the object. // Should move over to using an array for this probably if (o.styles[key]) { ((v_ = o.styles[key]), delete o.styles[key], v_); } o.styles[key] = val.c({ property: self._property }); } return self; } toCSS() { return "" + this._property.c() + ": " + AST.cary(this._expr).join(" "); } toJSON() { return this.toCSS(); } } class StyleProperty extends StyleNode { constructor(token) { super(...arguments); this._token = token; let raw = String(this._token); let meta = parseStylePropertyMetadata(raw, constants); this._parts = meta.parts.slice(); this._name = meta.name; this._kind = meta.kind; this._number = meta.number; this._unit = meta.unit; if (meta.colorError) { this.error( "Color name " + this._name + " cannot be identical to valid hex color", { loc: token[0] || token }, ); } this; } setName(value) { let unit = parseStyleUnitName(value); if (unit) { this._number = unit.number; this._unit = unit.unit; } else { this._number = this._unit = null; } this._name = value; return this; } name() { return this._name || (this._name = String(this._parts[0])); } number() { return this._number; } setNumber(value) { this._number = value; return this; } unit() { return this._unit; } setUnit(value) { this._unit = value; return this; } kind() { return this._kind; } setKind(value) { this._kind = value; return this; } clone(newname) { return new StyleProperty( [newname || this._name].concat(this.modifiers()).join(""), ); } addModifier(modifier) { this._parts.push(modifier); return this; } isUnit() { return this._unit; } isColor() { return this._kind == "color" || this._name[0] == "#"; } modifiers() { return this._parts.slice(1); } toJSON() { return this._name + this.modifiers().join("§"); } toString() { return this._name + this.modifiers().join("§"); } toKey() { let name = this.isUnit() ? "--u_" + this._unit : this.isColor() ? "--c_" + this._name.slice(1) : this._name; return [name].concat(this.modifiers()).join("§"); } c() { return this.toString(); } } // modifiers // values // lookup shorthand. If shorthand represents multiple // props then we compile it to multiple props class StylePropertyIdentifier extends StyleNode { constructor(name) { super(...arguments); this._name = name; if (String(name)[0] == "$") { this._name = "--" + String(name).slice(1); } // val[0] == '$' ? "var(--{val.slice(1)})" : val } toJSON() { return String(this._name); } toString() { return String(this._name); } } class StylePropertyModifier extends StyleNode { constructor(name) { super(...arguments); this._name = name; } toJSON() { return String(this._name); } toString() { return String(this._name); } } class StyleExpressions extends ListNode { load(list) { if (list instanceof Array) { list = list.map(function (_0) { return _0 instanceof StyleExpression ? _0 : new StyleExpression(_0); }); } return [].concat(list); } c(o) { let out = AST.cary(this._nodes, o).join(", "); if (this.option("parens")) { out = "( " + out + " )"; } return out; } clone() { return new StyleExpressions(this._nodes.slice(0)); } toArray() { return this._nodes .filter(function (_0) { return _0 instanceof StyleExpression; }) .map(function (_0) { return _0.toArray(); }); } } class StyleExpression extends ListNode { load(list) { return [].concat(list); } toString() { return AST.cary(this._nodes).join(" "); } clone() { return new StyleExpression(this._nodes.slice(0)); } c(o) { if (o && o.as == "js") { return AST.cary(this._nodes, o).join(" "); } return this.toString(); } toJSON() { return this.toString(); } toArray() { return this._nodes; } toIterable() { return this._nodes; } addParam(param, op) { param._op = op; this.last().addParam(param); return this; } reclaimParams() { let items = this.filter(function (_0) { return _0.param; }); for (let i = 0, ary = iter$(items), len = ary.length, item; i < len; i++) { item = ary[i]; let param = item.param; let op = param._op; this.add([op, param], { after: item }); item._params = []; } return; } visit(stack, o) { if (o && o.property) { let name = o.property._name; if (name == "gt" || name == "grid-template") { this.reclaimParams(); } } return super.visit(...arguments); } } class StyleParens extends ValueNode { visit(stack, o) { super.visit(...arguments); return this.set({ calc: !stack.up(StyleParens) && !stack.up(StyleFunction), }); } c(o) { let plain = this._value.c(); // TODO warn when option(:unit) is set if (o && o.as == "js") { return plain; } else if (this.option("calc")) { let unit = this._options && String(this._options.unit || ""); if (unit) { return "calc(calc(" + plain + ") * 1" + unit + ")"; } else { return "calc(" + plain + ")"; } } else { return "(" + plain + ")"; } } } class StyleOperation extends ListNode { c(o) { return AST.cary(this._nodes, o).join(" "); } } class StyleTerm extends ValueNode { valueOf() { return String(this._value); } toString() { return String(this._value); } toRaw() { return this.valueOf(); } toAlpha() { return this.valueOf(); } visit(stack, o) { this._token = this._value; this._property = o.property; this._propname = o.property && o.property._name; this.alone = stack.up() instanceof StyleExpression && stack.up().values().length == 1; let resolved = stack.theme().$value(this, 0, this._propname); if (!(stack.up(StyleParens) || stack.up(StyleFunction))) { this._resolvedValue = resolved; } return this; } get param() { return this._params && this._params[0]; } kind() { return this._kind; } runtimeValue() { return this.value(); } addParam(param) { this._params || (this._params = []); this._params.push(param); return this; } c(o) { let out = this._resolvedValue && !(this._resolvedValue instanceof Node) ? C(this._resolvedValue) : this.valueOf(); return out; } } class StyleInterpolationExpression extends StyleTerm { name(v) { return this._name; } setName(v) { this._name = v; return this; } loc() { return [this._startLoc, this._endLoc]; } visit(stack, o) { super.visit(...arguments); if (o.rootRule) { o.rootRule.addPlaceholder(this); } this._id = "" + this.sourceId() + "_" + this.tid(); // could use a different counter? this._name = "--" + this._id; return (this._runtimeValue = this.value()); // @propname = stack.theme.expandProperty } runtimeValue() { return this._runtimeValue; } get unit() { return (this._options && String(this._options.unit)) || ""; } c() { return "var(--" + this._id + ")"; } } class StyleFunction extends Node { constructor(value, params) { super(...arguments); this._name = value; this._params = params; } kind() { return "function"; } visit(stack, o) { this._property = o.property; this._propname = o.property && o.property._name; if (this._params && this._params.traverse) { this._params.traverse(); } let name = String(this._name); let parts = this._params.toArray().flat(); if (this._property.isColor() && name.match(/^(lch|rgba?|hsla?)$/)) { this._lcha = []; for ( let i = 0, items = iter$(parts), len = items.length, part; i < len; i++ ) { part = items[i]; if (part._value == "/") { continue; } this._lcha.push(part); } if (name != "lch") { let alpha = this._lcha[3]; let kind = name.slice(0, 3); for ( let i = 0, items = iter$(this._lcha), len = items.length, part; i < len; i++ ) { part = items[i]; if (!(part instanceof StyleDimension) && i < 3) { return this.error( "Dynamic part not allowed in non-lch #color definitions", { loc: part }, ); } } try { let inside = this._params.c(); if (alpha && !(alpha instanceof StyleDimension)) { inside = inside.replace(alpha.c(), "1"); } let full = "" + name + "(" + inside + ")"; let col = colord(full).toLch(); this._lcha = [col.l, col.c, col.h, col.a]; if (alpha) { this._lcha[3] = alpha; } } catch (e) { this.error("Failed to parse color", { loc: this }); } } } return this; } lcha() { return this._lcha || [0, 0, 0, 1]; } toString() { return this.c(); } c(o) { var res; let name = String(this._name); let pars = this._params.c(); let out = "" + name + "(" + pars + ")"; if (this._property && this._property.isColor()) { if (name == "hsl") { let parts = this._params.toArray().flat(); if (parts.length == 3) { return AST.cary(parts).join(","); } } if ((res = Color.from(out))) { return res.toVar(); } } if (o && o.as == "js") { out = helpers.singlequote(out); } return out; } } class StyleURL extends ValueNode { c() { let out = String(this._value); return SourceMapper.strip(out); } } class StyleIdentifier extends StyleTerm { color() { return this._color; } setColor(value) { this._color = value; return this; } visit(stack) { let raw = this.toString(); if (raw.match(/^[lcha]$/)) { super.visit(...arguments); let mix = stack.up(StyleColorMix); this._colormix = mix; return (this._resolvedValue = "var(--u_" + this._colormix._name + raw.toUpperCase() + ")"); } else { if (raw.match(/^([a-zA-Z]+\d+|black|white)$/)) { this.setColor("" + raw); if (this.param) { this.setColor(this.color() + "/" + this.param.toAlpha()); } } return super.visit(...arguments); } } c(o) { if (this._colormix) { return ( "var(--u_" + this._colormix._name + this.toString().toUpperCase() + ")" ); } if (this.color()) { let val = this.color().toString(); let asvar = this.option("parameterize") || (this._property && this._property.isColor()); let pre = asvar ? "/*##*/" : "/*#*/"; return pre + val; } let val = this.toString(); if (val[0] == "$") { val = "var(--" + val.slice(1) + ")"; if (o && o.as == "js") { val = helpers.singlequote(val); } return val; } else { return super.c(...arguments); } } } class StyleString extends StyleTerm {} class StyleColor extends StyleTerm { visit() { var m; super.visit(...arguments); let raw = this.toRaw(); let name = (this._name = raw.slice(1)); // only needed for the property color? if ((m = raw.match(constants.HEX_REGEX))) { this._hex = true; let col = colord(raw).toLch(); this._lcha = [col.l, col.c, col.h, col.a]; } else { this._lcha = [ "var(--u_" + name + "L)", "var(--u_" + name + "C)", "var(--u_" + name + "H)", "var(--u_" + name + "A,1)", ]; } let a = this.param && this.param.toAlpha(); if (a != null) { if (a[0] == "$") { a = "var(--" + a.slice(1) + ",100%)"; } return (this._lcha[3] = a); } } lcha() { return this._lcha; } c(o) { var ary; let raw = this.toRaw(); let name = raw.slice(1); let rich = Color.from(raw); if (this._property && this._property.isColor()) { console.log("deprecated"); return rich.toVar(); } var ary = iter$(this._lcha); let l = ary[0], c = ary[1], h = ary[2], a = ary[3]; if (this._hex && a == 1) { return raw; } return "lch(" + l + " " + c + " " + h + " / " + a + ")"; } } class StyleColorMix extends StyleTerm { params() { return this.option("params"); } lcha() { return this._lcha; } visit() { let pars = this.params().toArray().flat(); let name = (this._name = this.toRaw().slice(1)); this.params().traverse(); super.visit(...arguments); this._lcha = []; for ( let i = 0, items = iter$(pars), len = items.length, part; i < len; i++ ) { part = items[i]; if (part._value == "/") { continue; } this._lcha.push(part); } if (this._lcha.length == 3) { this._lcha.push(LIT("var(--u_" + name + "A,1)")); } return this; } c(o) { var ary; let raw = this.toRaw(); let name = raw.slice(1); var ary = iter$(AST.cary(this._lcha)); let l = ary[0], c = ary[1], h = ary[2], a = ary[3]; return "lch(" + l + " " + c + " " + h + " / " + a + ")"; } } class StyleVar extends StyleTerm { c(o) { return this.toString(); } } var VALID_CSS_UNITS = "cm mm Q in pc pt px em ex ch rem vw vh vmin vmax % s ms fr deg rad grad turn Hz kHz cqw cqh cqi cqb cqmin cqmax".split( " ", ); class StyleDimension extends StyleTerm { constructor(value) { super(...arguments); this._value = value; let m = String(value).match(/^([\-\+]?[\d\.]*)([a-zA-Z]+|%)?$/); this._number = parseFloat(m[1]); this._unit = m[2] || null; } get unit() { return this._unit || ""; } number() { return this._number; } setNumber(value) { this._number = value; return this; } setUnit(value) { this._unit = value; return this; } clone(num, unit) { if (num === undefined) num = this._number; if (unit === undefined) unit = this._unit; let cloned = new StyleDimension(this.value()); cloned._unit = unit; cloned._number = num; return cloned; } visit(stack) { if (this._unit && this._unit.match(/^[lcha]$/)) { if ((this._colormix = stack.up(StyleColorMix))) { this._unit = "" + this._colormix._name + this._unit.toUpperCase(); } // if par isa StyleDimension // let u = par:_unit // if u and regex.test(u) // par:_unit = "{@name}{u.toUpperCase()}" } return super.visit(...arguments); } toString() { return "" + this._number + (this._unit || ""); } toFloat(pct) { if (pct === undefined) pct = 0.01; let num = this._number; if (this._unit == "%") { num = num * pct; } return num; } toRaw() { return this._unit ? this.toString() : this._number; } c(o) { let out = this._resolvedValue && !(this._resolvedValue instanceof Node) ? C(this._resolvedValue) : this.valueOf(); if (o && o.as == "js" && this._unit) { out = helpers.singlequote(out); } return out; } valueOf() { if (this._unit == "u") { return this._number * 4 + "px"; } else if (this._unit == null) { return this._number; } else if (idx$(this._unit, VALID_CSS_UNITS) >= 0) { return String(this._value); } else { let fallback = this._colormix ? "" : ",1" + this._unit; if (this._number == 1) { return "var(--u_" + this._unit + fallback + ")"; } else { return ( "calc(var(--u_" + this._unit + fallback + ") * " + this._number + ")" ); // return String(@value) } } } toAlpha() { if (!this._unit) { return this._number + "%"; } else { return this.valueOf(); } } } class StyleNumber extends StyleDimension {} return { StyleNode, StyleSelector, StyleRuleSet, StyleBody, StyleDeclaration, StyleProperty, StylePropertyIdentifier, StylePropertyModifier, StyleExpressions, StyleExpression, StyleParens, StyleOperation, StyleTerm, StyleInterpolationExpression, StyleFunction, StyleURL, StyleIdentifier, StyleString, StyleColor, StyleColorMix, StyleVar, StyleDimension, StyleNumber, }; }