LESS static type system and CSS validation, based on Hungarian notation #4368
SergeVDev
started this conversation in
Show and tell
Replies: 3 comments
-
Here's the code. If someone is interested, I'll create a repository for it. Important For CSS validation I use csstree-validator Node.js module. You should install it into your compiling-LESS-on-save IDE extension or a project building system. lib.less (fragment): @plugin "less-lib";
//========================================
// Universal mixins
//========================================
// Provides *forgiving selector parsing*. Since :is() is not applicable to pseudo-elements, this mixin should be used instead.
// Wrapping with quotes is mandatory for pseudo-elements and optional for regular selectors.
// Example:
// input[type="range"]
// {
// .any(@range-track,
// {
// .reset-control();
// });
// }
// This resets the track sub-control of a slider.
.any(@selector-list, @ruleset)
{
each(@selector-list,
{
@selector: e(@value);
@{selector}
{
@ruleset();
}
});
}
// Checks, whether values of given variables are type-compatible.
// Don't prefix name with '@'. Separate names with space.
// If a single name is passed, returns the corresponding variable value.
// Example:
// .mixin(@p1, @p2, @p3)
// {
// .check(p1 p2 p3);
// prop1: @p1;
// prop2: @p2;
// prop3: @p3;
// ...or...
// prop1: .check(p1)[];
// prop2: .check(p2)[];
// prop3: .check(p3)[];
// }
.check(@names)
{
each(@names,
{
check(@value, @@value, %('%a', @@value));
});
@return: if(length(@names) = 1, @@names, false);
} lib-less.js: const cssValidator = require('csstree-validator');
// Declares an enum pseudotype.
const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({ [k]: Symbol(k) }))));
// Built-in static types.
const VariableType = makeEnum('Integer', 'Float', 'Boolean', 'Color', 'Enum', 'Limit', 'CssProperty');
// Registered enum types.
const lessEnums = [];
// Registered limit types.
const lessLimits = [];
// 'crypto' interface is not available in some LESS-compiling environments.
function getGuid()
{
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c)
{
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// Breaks compilation with the message.
function die(message)
{
throw new Error(`Compiling LESS error: ${message}`);
}
// Checks, whether a string is valid CSS property (with *csstree-validator*).
function isValidCssProperty(property)
{
const css = `.test { ${property}: unset; }`; // 'unset' is always valid.
return cssValidator.validate(css).length == 0;
}
// Checks, whether a string is valid CSS value for a given CSS property (with *csstree-validator*).
function isValidCssValue(property, value)
{
const css = `.test { ${property}: ${value}; }`;
return cssValidator.validate(css).length == 0;
}
// Safely extracts units from a LESS value.
function getValueUnits(v)
{
const found = typeof v.unit === 'object' && Array.isArray(v.unit.numerator) && v.unit.numerator.length > 0;
return found ? v.unit.numerator[0] : '';
}
// Returns registered type (enum/limit) by name or prefix.
function findRegisteredType(prefix, collection, typeName = undefined)
{
if (!typeName)
typeName = prefix.substring(prefix[1] === '-' ? 2 : 1); // '-' is optional.
const result =
{
registeredType: collection.find(type => type.name == typeName),
typeName: typeName
};
return result;
}
// Compares two LESS value objects.
function compareValues(a, b)
{
// If both are colors, compare RGB.
if (a.rgb && b.rgb)
{
return a.rgb[0] == b.rgb[0] && a.rgb[1] == b.rgb[1] && a.rgb[2] == b.rgb[2] && a.alpha == b.alpha;
}
// If at least one is a string.
if (a.quote || b.quote)
{
if (a.quote != b.quote)
return false;
}
// If at least one has units.
if (a.unit || b.unit)
{
// If one has units and another doesn't.
if ((a.unit && !b.unit) || (!a.unit && b.unit))
return false;
// If one array of units is empty and another isn't.
if (a.unit.numerator.length != b.unit.numerator.length)
return false;
if (a.unit.numerator.length > 0 && (a.unit.numerator[0] != b.unit.numerator[0]))
return false;
// Units are equal.
}
return a.value == b.value;
}
// Prefix Type Can variable have units?
//
// Built-in types:
// i_ Integer Yes
// f_ Float Yes
// b_ Boolean No
// c_ Color No
//
// Registered types:
// E-my-enum_ (or Emy-enum_) Enum 'my-enum' No
// L-my-limit_ (or Lmy-limit_) Limit 'my-limit Yes
//
// Property types:
// <any valid CSS property>_ CSS property Yes
//
// -------------- Special units: --------------
//
// Suffix Units
//
// _ (ends with) Zero units (no units allowed)
// _percent %
function checkVariableType(varName, varValue, varCss)
{
const errorMsgInvalidVariableName = 'Invalid variable name: ';
const errorMsgInvalidVariableType = 'Invalid variable type. ';
const errorMsgTypeMismatch = 'Type mismatch. ';
const errorMsgUnknownVariableType = 'Unknown variable type: ';
const errorMsgEnumNameExpected = 'Enum name expected';
const errorMsgLimitNameExpected = 'Limit name expected';
function getTypeInfoFromPrefix(prefix)
{
function isValidPrefixForRegisteredType(prefix)
{
return prefix.length > 1 && // 'E' or 'L' are invalid.
!(prefix.length === 2 && prefix[1] === '-'); // 'E-' or 'L-' are invalid.
}
if (prefix === 'i')
return VariableType.Integer;
else if (prefix === 'f')
return VariableType.Float;
else if (prefix === 'b')
return VariableType.Boolean;
else if (prefix === 'c')
return VariableType.Color;
else if (prefix[0] === 'E')
{
if (isValidPrefixForRegisteredType(prefix))
return VariableType.Enum;
die(errorMsgInvalidVariableType + errorMsgEnumNameExpected);
}
else if (prefix[0] === 'L')
{
if (isValidPrefixForRegisteredType(prefix))
return VariableType.Limit;
die(errorMsgInvalidVariableType + errorMsgLimitNameExpected);
}
else if (isValidCssProperty(prefix))
{
return VariableType.CssProperty;
}
die(errorMsgUnknownVariableType + prefix);
}
function checkSinglenessForType(varName, varValue, varCss)
{
if (Array.isArray(varValue.value))
die(errorMsgTypeMismatch + `${varName}:${varCss}: single value expected for variable of this type`);
}
function checkUnits(varName, varNameWithoutPrefix, varValue, varCss)
{
const secondUnderscoreIndex = varNameWithoutPrefix.indexOf('_');
if (secondUnderscoreIndex < 0) // Any units, no check.
return;
if (secondUnderscoreIndex === 0) // prefix__suffix, no name between.
die(errorMsgInvalidVariableName + `${varName}. Two underscores in a row`);
if (Array.isArray(varValue.value))
die(errorMsgTypeMismatch + `${varName}:${varCss}: single value expected for variable with specified units`);
let varUnits = getValueUnits(varValue);
if (secondUnderscoreIndex === varNameWithoutPrefix.length - 1) // Zero units.
{
if (varUnits == '')
return;
die(errorMsgTypeMismatch + `${varName}:${varCss}: no units expected, ${varUnits} found`);
}
let suffix = varNameWithoutPrefix.substring(secondUnderscoreIndex + 1);
if (suffix == 'percent')
suffix = '%';
if (suffix != varUnits)
{
if (!varUnits)
varUnits = 'no units'; // For readability.
die(errorMsgTypeMismatch + `${varName}:${varCss}: invalid units. ${suffix} expected, ${varUnits} found`);
}
}
function checkNoUnitsForThisType(varName, varNameWithoutPrefix)
{
if (varNameWithoutPrefix.indexOf('_') >= 0)
die(errorMsgInvalidVariableName + `${varName}. Unexpected units for this type`);
}
function checkInteger(varName, varNameWithoutPrefix, varValue, varCss)
{
checkSinglenessForType(varName, varValue, varCss);
if (typeof varValue.value === 'string' || varValue.quote)
die(errorMsgTypeMismatch + `${varName}:${varCss}: integer expected, string found`);
if (!Number.isInteger(varValue.value))
die(errorMsgTypeMismatch + `${varName}:${varCss}: integer expected`);
checkUnits(varName, varNameWithoutPrefix, varValue, varCss);
}
function checkFloat(varName, varNameWithoutPrefix, varValue, varCss)
{
checkSinglenessForType(varName, varValue, varCss);
if (typeof varValue.value === 'string' || varValue.quote)
die(errorMsgTypeMismatch + `${varName}:${varCss}: float expected, string found`);
if (!Number.isFinite(Number(varValue.value)))
die(errorMsgTypeMismatch + `${varName}:${varCss}: float expected`);
checkUnits(varName, varNameWithoutPrefix, varValue, varCss);
}
function checkBoolean(varName, varNameWithoutPrefix, varValue, varCss)
{
checkSinglenessForType(varName, varValue, varCss);
checkNoUnitsForThisType(varName, varNameWithoutPrefix);
if (varValue.quote) // LESS boolean is presented as string in JS, so don't check whether typeof varValue.value === 'string'.
die(errorMsgTypeMismatch + `${varName}:${varCss}: boolean expected, string found`);
if (varValue.value !== 'true' && varValue.value !== 'false')
die(errorMsgTypeMismatch + `${varName}:${varCss}: boolean expected`);
}
function checkColor(varName, varNameWithoutPrefix, varValue, varCss)
{
checkSinglenessForType(varName, varValue, varCss);
checkNoUnitsForThisType(varName, varNameWithoutPrefix);
if (varValue.quote)
die(errorMsgTypeMismatch + `${varName}:${varCss}: color expected, string found`);
if (!varValue.rgb)
die(errorMsgTypeMismatch + `${varName}:${varCss}: color expected`);
}
function checkEnum(varName, varNameWithoutPrefix, prefix, varValue, varCss)
{
checkSinglenessForType(varName, varValue, varCss);
checkNoUnitsForThisType(varName, varNameWithoutPrefix);
const lessEnum = findRegisteredType(prefix, lessEnums);
if (lessEnum.registeredType === undefined)
die(`${varName}:${varCss}: unknown enum ${lessEnum.typeName}`);
for (const val of lessEnum.registeredType.values)
{
if (compareValues(varValue, val))
return;
}
die(errorMsgTypeMismatch + `${varName}:${varCss}: ${lessEnum.typeName} enum member expected`);
}
function checkLimit(varName, varNameWithoutPrefix, prefix, varValue, varCss)
{
checkSinglenessForType(varName, varValue, varCss);
const lessLimit = findRegisteredType(prefix, lessLimits);
if (lessLimit.registeredType === undefined)
die(`${varName}:${varCss}: unknown limit ${lessLimit.typeName}`);
if (typeof varValue.value === 'string' || varValue.quote)
die(errorMsgTypeMismatch + `${varName}:${varCss}: float expected, string found`);
const number = Number(varValue.value);
if (!Number.isFinite(number))
die(errorMsgTypeMismatch + `${varName}:${varCss}: float expected`);
const min = lessLimit.registeredType.min;
const max = lessLimit.registeredType.max;
if (number < min || number > max)
die(errorMsgTypeMismatch + `${varName}:${varCss}: number ${number} is out of range [${min} - ${max}]`);
checkUnits(varName, varNameWithoutPrefix, varValue, varCss);
}
function checkCssValue(varName, varNameWithoutPrefix, prefix, varValue, varCss)
{
if (getValueUnits(varValue))
checkUnits(varName, varNameWithoutPrefix, varValue, varCss);
if (!isValidCssValue(prefix, varCss))
die(errorMsgTypeMismatch + `${varName}:${varCss}: invalid value for ${prefix} CSS property`);
}
const underscoreCount = (varName.match(/_/g) || []).length;
if (underscoreCount > 2)
die(errorMsgInvalidVariableName + `${varName}. ${underscoreCount} underscores (_), expected 2 or less`);
const firstUnderscoreIndex = varName.indexOf('_');
if (firstUnderscoreIndex < 0) // Typeless ('any').
return;
if (firstUnderscoreIndex === 0) // Starts with '_', no prefix.
die(errorMsgInvalidVariableName + `${varName}. Type prefix expected`);
if (firstUnderscoreIndex === varName.length - 1) // Ends with the first '_', no name after prefix.
die(errorMsgInvalidVariableName + `${varName}. No name after prefix`);
const varNameWithoutPrefix = varName.substring(firstUnderscoreIndex + 1);
const typePrefix = varName.substring(0, firstUnderscoreIndex);
const typeInfo = getTypeInfoFromPrefix(typePrefix);
switch (typeInfo)
{
case VariableType.Integer:
checkInteger(varName, varNameWithoutPrefix, varValue, varCss);
break;
case VariableType.Float:
checkFloat(varName, varNameWithoutPrefix, varValue, varCss);
break;
case VariableType.Boolean:
checkBoolean(varName, varNameWithoutPrefix, varValue, varCss);
break;
case VariableType.Color:
checkColor(varName, varNameWithoutPrefix, varValue, varCss);
break;
case VariableType.Enum:
checkEnum(varName, varNameWithoutPrefix, typePrefix, varValue, varCss);
break;
case VariableType.Limit:
checkLimit(varName, varNameWithoutPrefix, typePrefix, varValue, varCss);
break;
case VariableType.CssProperty:
checkCssValue(varName, varNameWithoutPrefix, typePrefix, varValue, varCss);
break;
default:
die('Internal type checking system error');
break;
}
}
// [Helper debug function] Prints an object content with functions, prettified.
function printObject(obj)
{
// GTFO to Egypt.
const expandBraces = (code) =>
{
let expandedCode = code;
const maxNestingLevel = 30;
for (let i = maxNestingLevel; i > 0; i--)
{
expandedCode = expandedCode.replaceAll(' {\n' + '\t'.repeat(i), '\n' + '\t'.repeat(i - 1) + '{\n' + '\t'.repeat(i));
expandedCode = expandedCode.replaceAll(' [\n' + '\t'.repeat(i), '\n' + '\t'.repeat(i - 1) + '[\n' + '\t'.repeat(i));
}
return expandedCode;
};
// Removes circular references.
const getCircularReplacer = () =>
{
const seen = new WeakSet();
return (key, value) =>
{
if (typeof value === 'object' && value !== null)
{
if (seen.has(value))
return;
seen.add(value);
}
if (typeof value === 'function')
return value.toString();
return value;
};
};
return expandBraces(JSON.stringify(obj, getCircularReplacer(), '\t'));
};
// Entry point.
registerPlugin
(
{
install: function (less, pluginManager, functions)
{
// Used as:
//
// enum(safe-color, red, green, blue);
// ...
// @E-safe-color_button-color: red;
// .check(E-safe-color_button-color); // OK
// @E-safe-color_link-color: black;
// .check(E-safe-color_link-color); // Compiling error
//
// Registers a new enum type for checking variable type/value.
functions.add('enum', function (name, ...args)
{
if (findRegisteredType(null, lessEnums, name.value).registeredType !== undefined)
die(`Enum ${name.value} already exists`);
lessEnums.push(
{
name: name.value,
values: args
});
return false;
});
// Used as:
//
// limit(safe-margin, 2, 24);
// limit(safe-factor, 1.0, 3.0);
// ...
// @R-safe-margin_body-margin_px: 20px;
// .check(R-safe-margin_body-margin); // OK
// @R-safe-factor_zoom-factor_: 4;
// .check(R-safe-factor_zoom-factor_); // Compiling error
//
// Registers a new limit type for checking whether a variable is in the range: min <= var <= max.
// For min < var < max comparison, create limits as this: limit(safe-margin, 2.000000001, 24.000000001);
functions.add('limit', function (name, min, max)
{
if (findRegisteredType(null, lessLimits, name.value).registeredType !== undefined)
die(`Limit ${name.value} already exists`);
const errorMsgInvalidLimit = `Invalid limit ${name.value}: `;
function checkLimitValue(value)
{
const units = getValueUnits(value);
if (units)
die(errorMsgInvalidLimit + `unexpected units (${value.value}${units})`);
if (value.rgb)
die(errorMsgInvalidLimit + `unexpected color (${value.value}[${value.rgb}])`);
if (value.quote)
die(errorMsgInvalidLimit + `unexpected string (${value.quote}${value.value}${value.quote})`);
}
checkLimitValue(min);
checkLimitValue(max);
const numberMin = Number(min.value);
const numberMax = Number(max.value);
function checkNumber(number)
{
if (!Number.isFinite(number))
die(errorMsgInvalidLimit + `float expected, ${number} found`);
}
checkNumber(numberMin);
checkNumber(numberMax);
if (numberMax < numberMin)
die(errorMsgInvalidLimit + `max (${numberMax}) < min (${numberMin})`);
lessLimits.push(
{
name: name.value,
min: numberMin,
max: numberMax
});
return false;
});
// Used as:
//
// @i_size_px: .get-size()[];
// check(i_size_px, @i_size_px);
// ...or...
// .check(i_size_px);
//
// Checks whether value of @i_size_px is compatible with type of @i_size_px (integer, px units).
// check() is not supposed to be called directly, use .check() mixin instead.
functions.add('check', function (name, value, css)
{
checkVariableType(name.value, value, css.value);
return false;
});
// Used as:
//
// width: rp(20);
//
// It means '20 relative pixels' or '20px if 1rem == 16px or proportionally scaled otherwise'.
functions.add('rp', function (rpx)
{
return new tree.Dimension(rpx.value / 16, 'rem');
});
// Used as:
//
// assertEq(@value, 5);
//
// It @value is not equal to 5, LESS compiling breaks.
functions.add('assertEq', function (a, b)
{
if (a.value != b.value)
throw new Error(`Assertion failed: ${a.value} != ${b.value}`);
return false;
});
// Used as:
//
// die('Not implemented');
//
// LESS compiling breaks with the given message.
functions.add('die', function (message)
{
die(message.value);
});
// Used as:
//
// @up-to-down: uniqueName('up-to-down');
//
// @up-to-down will be up-to-down-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (with a GUID as the suffix).
functions.add('uniqueName', function (name)
{
const guid = getGuid();
return `${name.value}-${guid}`;
});
// Used as:
//
// @images:
// '../images/image1.png',
// '../images/image2.png',
// '../images/image3.png';
// background-image: multipleBackgrounds(@images);
//
// background-image will be url(../images/image1.png), url(../images/image2.png), url(../images/image3.png).
// Every image is wrapped in url().
functions.add('multipleBackgrounds', function (list)
{
if (Array.isArray(list.value))
return list.value.reduce((a, v) => (a ? a + ', ' : a) + `url(${v.value})`, '');
return `url(${list.value})`; // For single element lists.
});
// Concatenates lists and single values into a new list.
functions.add('concatLists', function (...args)
{
const values = [];
for (const arg of args)
{
if (arg.value instanceof Array)
{
values.push(...arg.value);
}
else
{
values.push(arg);
}
}
return new tree.Value(values);
});
}
}
) |
Beta Was this translation helpful? Give feedback.
0 replies
-
Here are my unit tests: @import "lib";
.test-1-checking-integer-float-boolean
{
@var1: 5;
.check(var1); // OK No type is specified
@var2: 5px;
.check(var2); // OK No type is specified
@var3_: 5;
//.check(var3_); // Fail Units (zero units) specified with no type
@var4_px: 5px;
//.check(var4_px); // Fail Units (px) specified with no type
@_: 5;
//.check(_); // Fail No type
@a_var: 'aaaaa';
//.check(a_var); // Fail Invalid type 'a'
@_var: none;
//.check(_var); // Failed No type
@i__var: 5;
//.check(i__var); // Failed 2 __ in a row
@i_var1: 5;
.check(i_var1); // OK
@i_var2: 5.0;
.check(i_var2); // OK
@i_var3: 5.5;
//.check(i_var3); // Fail Float number instead of integer
@i_var4: 5px;
.check(i_var4); // OK
@i_var5: 5.0px;
.check(i_var5); // OK
@i_var6: -5;
.check(i_var6); // OK
@i_var7: (1.5 + 2.5);
.check(i_var7); // OK
@i_var8: '';
//.check(i_var8); // Fail Empty string (falsy value) instead of 0
@i_var9: '42';
//.check(i_var9); // Fail String casting to integer not allowed
@i_var_px: 10px; // OK
//@i_var_px: 10.5px; // Fail Float number instead of integer
//@i_var_px: '10px'; // Fail String casting to integer not allowed
//@i_var_px: 10; // Fail px units required
//@i_var_px: 10em; // Fail em units instead of px
//@i_var_px: 10%; // Fail % units instead of px
//@i_var_px: blue; // Fail Color value instead of integer
//@i_var_px: ''; // Fail Empty string (falsy value) instead of 0
//@i_var_px: null; // Fail null (falsy value) instead of 0
//@i_var_px: none; // Fail Custom value instead of integer
.check(i_var_px);
width: .check(i_var_px)[];
@i_var_px_px: 10px;
//.check(i_var_px_px); // Fail 3 _ (double units)
@i_var1_: 5;
.check(i_var1_); // OK
@i_var2_: 5px;
//.check(i_var2_); // Fail Zero units required
@f_var1_: 5.5;
.check(f_var1_); // OK
@f_var2_: 5.5px;
//.check(f_var2_); // Fail Zero units required
@f_var1_px: 10px; // OK
@f_var2_px: 10.5px; // OK
@f_var3_px: -10.5px; // OK
@f_var4_: -10.5; // OK
//@f_var1_px: (1 / 0); // Fail Infinity instead of float
//@f_var2_px: 10.5; // Fail px units required
//@f_var3_px: red; // Fail Custom value instead of float
//@f_var4_: 2px; // Fail Zero units required
//@f_var4_: '-10.5'; // Fail String casting to float not allowed
.check(f_var1_px f_var2_px f_var3_px f_var4_);
@i_var_rem: 10rem;
.check(i_var_rem); // OK
@f_var_rem: 1.5rem;
.check(f_var_rem); // OK
@i_var_percent: 10%;
.check(i_var_percent); // OK
@f_var_percent: 10.99%;
.check(f_var_percent); // OK
@b_var1: true;
.check(b_var1); // OK
@b_var2: false;
.check(b_var2); // OK
@b_var3: 'true';
//.check(b_var3); // Fail String casting to boolean not allowed
@b_var4: 'false';
//.check(b_var4); // Fail String casting to boolean not allowed
@b_var5_px: true;
//.check(b_var5_px); // Fail Boolean with px units
@b_var6_: true;
//.check(b_var6_); // Fail Boolean with zero units
}
.test-mixin(@i_param_px)
{
.check(i_param_px);
}
.test-2-checking-mixin-params
{
.test-mixin(14px); // OK
//.test-mixin(14.5px); // Fail Float number instead of integer
//.test-mixin('14px'); // Fail String casting to integer not allowed
//.test-mixin(14); // Fail px units required
//.test-mixin(14em); // Fail em units instead of px
//.test-mixin(14%); // Fail % units instead of px
//.test-mixin(red); // Fail Color instead of integer
//.test-mixin(''); // Fail Empty string (falsy value) instead of 0
//.test-mixin(null); // Fail null (falsy value) instead of 0
//.test-mixin(none); // Fail Custom value instead of integer
}
.test-3-calling-mixin-from-a-loop
{
// 10 calls from a loop.
each(range(10),
{
prop-@{index}: @index;
.test-mixin(unit(@index, px));
});
}
.test-4-checking-color
{
@c_var1: red;
.check(c_var1); // OK
@c_var2: lightgoldenrodyellow;
.check(c_var2); // OK
@c_var3: rgb(0, 100, 0);
.check(c_var3); // OK
@c_var4: rgba(0, 100, 0, 0.3);
.check(c_var4); // OK
@c_var5: #fff;
.check(c_var5); // OK
@c_var6: #f0f0f0;
.check(c_var6); // OK
@c_var7: 0;
//.check(c_var7); // Fail Integer instead of color
@c_var8: ffffff;
//.check(c_var8); // Fail # missed, not a color
@c_var9: 'red';
//.check(c_var9); // Fail String 'red' instead of color red
@c_var10: lighten(hsl(90, 80%, 50%), 20%);
.check(c_var10); // OK
}
enum(test, true, 'false', aliceblue, red, 'green', #0000ff, 16px, 18, 10.3, rgba(255, 255, 255, 0.5));
//enum(test, a, b, c); // Fail Registering 'test' enum twice
.test-5-checking-enum
{
@E_var: 100;
//.check(E_var); // Fail No enum name
@E-test_var_: 200;
//.check(E-test_var_); // Fail Enum with zero units
@E-test_var_px: 300;
//.check(E-test_var_px); // Fail Enum with px units
@E-dummy_var: 100;
//.check(E-dummy_var); // Fail Non-existing enum
@E-test_var1: 100;
//.check(E-test_var1); // Fail Value out of enum 'test'
@E-test_var2: true;
.check(E-test_var2); // OK
@E-test_var3: boolean(@E-test_var1 > 10);
.check(E-test_var3); // OK
@E-test_var4: 'true';
//.check(E-test_var4); // Fail String 'true' instead of boolean true
@E-test_var5: false;
//.check(E-test_var5); // Fail Boolean false instead of string 'false'
@E-test_var6: 'false';
.check(E-test_var6); // OK
@E-test_var7: aliceblue;
.check(E-test_var7); // OK
@E-test_var8: #F0F8FF;
.check(E-test_var8); // OK aliceblue
@E-test_var9: rgb(240, 248, 255);
.check(E-test_var9); // OK aliceblue
@E-test_var10: 'green';
.check(E-test_var10); // OK
@E-test_var11: green;
//.check(E-test_var11); // Fail Color green instead of string 'green'
@E-test_var12: blue;
.check(E-test_var12); // OK #0000ff
@E-test_var13: #0000ff;
.check(E-test_var13); // OK
@E-test_var14: 16px;
.check(E-test_var14); // OK
@E-test_var15: unit(4 * 4, px);
.check(E-test_var15); // OK
@E-test_var16: '16px';
//.check(E-test_var16); // Fail String '16px' instead of value 16px
@E-test_var17: 16;
//.check(E-test_var17); // Fail 16 instead of 16px
@E-test_var18: 16%;
//.check(E-test_var18); // Fail 16% instead of 16px
@E-test_var19: 16.00000px;
.check(E-test_var19); // OK
@E-test_var20: 18;
.check(E-test_var20); // OK
@E-test_var21: 2 * 9;
.check(E-test_var21); // OK
@E-test_var22: '18';
//.check(E-test_var22); // Fail String '18' instead of value 18
@E-test_var23: 18px;
//.check(E-test_var23); // Fail 18px instead of 18
@E-test_var24: 18.0;
.check(E-test_var24); // OK
@E-test_var25: 10.3;
.check(E-test_var25); // OK
@E-test_var26: rgba(255, 255, 255, 0.5);
.check(E-test_var26); // OK
@E-test_var27: rgb(255, 255, 255);
//.check(E-test_var27); // Fail No alpha
}
limit(margin, 2, 24);
limit(factor, 1.0, 3.5);
//limit(margin, 10, 20); // Fail Registering 'margin' limit twice
//limit(test-1, 1px, 3); // Fail Limit type cannot have units, limit variable can
//limit(test-2, 1, 3px); // Fail Limit type cannot have units, limit variable can
//limit(test-3, 1, #000); // Fail Color instead of float
//limit(test-4, '1', 10); // Fail String instead of float
//limit(test-5, 1, '10'); // Fail String instead of float
//limit(test-6, 10, 1); // Fail min > max
//limit(test-7, 1, 1 / 0); // Fail NaN instead of float
.test-6-checking-limits
{
@L_var: 100;
//.check(L_var); // Fail No limit name
@L-dummy_var: 100;
//.check(L-dummy_var); // Fail Non-existing limit
@L-margin_var1_px: 5;
//.check(L-margin_var1_px); // Fail 5 instead of 5px
@L-margin_var2_: 5px;
//.check(L-margin_var2_); // Fail 5px instead of 5
@L-margin_var3_px: 5px;
.check(L-margin_var3_px); // OK px units
@L-margin_var4_: 5;
.check(L-margin_var4_); // OK Zero units
@L-margin_var5: 5;
.check(L-margin_var5); // OK Any units
@L-margin_var6: 5px;
.check(L-margin_var5); // OK Any units
@L-margin_var7: 1;
//.check(L-margin_var7); // Fail 1 < min (2)
@L-margin_var8: 25;
//.check(L-margin_var8); // Fail 25 > max (24)
@L-margin_var9: 2.17;
.check(L-margin_var9); // OK Float
@L-margin_var10: '42';
//.check(L-margin_var10); // Fail String casting to float not allowed
@L-margin_var11: '5px';
//.check(L-margin_var11); // Fail String '5px' instead of value 5px
@L-margin_var12: (10 / 0);
//.check(L-margin_var12); // Fail Infinity instead of float
}
.test-7-checking-css-properties
{
@background-size_var1: contain;
.check(background-size_var1); // OK
@background-size_var2: cover;
.check(background-size_var2); // OK
@background-size_var3: 30%;
.check(background-size_var3); // OK
@background-size_var4: 200px 100px;
.check(background-size_var4); // OK
@background-size_var5: 6px, auto, contain;
.check(background-size_var5); // OK
@background-size_var6: xxl;
//.check(background-size_var6); // Fail Invalid keyword value for background-size
@background-size_var7: 200px 100px 200px;
//.check(background-size_var7); // Fail 3D :)
@background-size_var8: -5 -5;
//.check(background-size_var8); // Fail Negative numbers
@width_var1_px: 100px;
.check(width_var1_px); // OK
@width_var2_px: 100%;
//.check(width_var2_px); // Fail % instead of px
@width_var3_px: auto;
.check(width_var3_px); // OK Unlike integer/float, units are checked for CSS property types only when they are specified
@background_var:
center / contain no-repeat
url("/shared-assets/images/examples/firefox-logo.svg"),
#eeeeee 35% url("/shared-assets/images/examples/lizard.png");
.check(background_var); // OK Multiple values are allowed for 'CSS property' type
}
.test-8-checking-multiple-values-with-builtin-types
{
@i_var1: 5 5;
//.check(i_var1); // Fail Multiple values are not allowed for integer variable
@i_var2: 5px 5px;
//.check(i_var2); // Fail Multiple values are not allowed for integer variable
@i_var3: 5, 5;
//.check(i_var3); // Fail Multiple values are not allowed for integer variable
@i_var4_px: 5px 5px;
//.check(i_var4_px); // Fail Multiple values are not allowed for integer variable
@f_var1: 5 5;
//.check(f_var1); // Fail Multiple values are not allowed for float variable
@f_var2: 5px 5px;
//.check(f_var2); // Fail Multiple values are not allowed for float variable
@f_var3: 5, 5;
//.check(f_var3); // Fail Multiple values are not allowed for float variable
@f_var4_px: 5px 5px;
//.check(f_var4_px); // Fail Multiple values are not allowed for float variable
@c_var1: red, blue;
//.check(c_var1); // Fail Multiple values are not allowed for color variable
@c_var2: red blue;
//.check(c_var2); // Fail Multiple values are not allowed for color variable
@b_var1: true true;
//.check(b_var1); // Fail Multiple values are not allowed for boolean variable
@b_var2: true, true;
//.check(b_var2); // Fail Multiple values are not allowed for boolean variable
@E-test_var1: aliceblue, red;
//.check(E-test_var1); // Fail Multiple values are not allowed for enum variable
@E-test_var2: aliceblue red;
//.check(E-test_var2); // Fail Multiple values are not allowed for enum variable
@L-margin_var1: 5 5;
//.check(L-margin_var1); // Fail Multiple values are not allowed for limit variable
@L-margin_var2: 5, 5;
//.check(L-margin_var2); // Fail Multiple values are not allowed for limit variable
} |
Beta Was this translation helpful? Give feedback.
0 replies
-
Interesting project, thank you for sharing! |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Hi guys,
I'd like to share a LESS helper for creating more safe CSS code.
Prefixes and suffixes
You add a prefix to variables/parameters, that specifies their type and a suffix, that specifies their units. (No prefix means any type, so the variable is just not being checked).
Prefixes and suffixes are separated from a name with underscore symbol (
_
). It means names can contain only 1 or 2 underscores.Here are the types:
Here are special units:
Built-in types are trivial. Some examples:
b_set-height
can containtrue
orfalse
only.i_width_px
can contain an integer number of pixels only (5px
,10px
).f_width_percent
can contain a float number with%
only (100%
,33.3333%
).f_zoom-factor_
can contain a unitless float number only (0
,1
,2.5
).c_button-bg
can contain a color only (aliceblue
,#fff
,rgb(255, 0, 0)
).You can declare enumerations and limits (a limit is a range, set by min and max, it's called “limit” because the
range
keyword is reserved for the generating function).Enum variables can contain a member of the enum only. Limit variables can contain a float number within the range only (including min and max values). You can combine limit variables with unit checking when declaring a variable (not the type itself). Some examples (based on the registered types above):
E-color-scheme_document-area
can contain onlysystem
,light
anddark
(with no quotes).L-margin_box_px
can contain a float number of pixels between2
and24
(2px
,16px
,24px
).L-margin_figure_rem
can contain a float number ofrem
between2
and24
(2rem
,16rem
,24rem
).L-zoom-factor_value_
can contain a float number between1.0
and3.5
without units (1.0
,2.0
,3.5
)..check()
mixinYou must call the
.check()
mixin manually within a mixin (or after a variable declaration):I know, it's kind of ugly, and I really tried hard to get rid of it, but it seems to be impossible to handle mixin call and assign value events. (See details there).
Anyway, my helper is designed for those who writes a system library of LESS mixins, so they only shouldn't forget calling
.check()
, not their users, so it's not such a big flaw.Writing safe mixins
Let we have the following mixin:
Your user calls it this way:
It generates:
And until you take a look at the UI, you won't know something is broken. For special cases you won't know even after.
Let's reimplement the mixin as this:
Now the user cannot compile it:
SyntaxError: Error evaluating function
check
: Compiling LESS error: Type mismatch. b_set-height:'true': boolean expected, string found in \css\test.lessSyntaxError: Error evaluating function
check
: Compiling LESS error: Type mismatch. width_size:1ft: invalid value for width CSS property in \css\test.lessBeta Was this translation helpful? Give feedback.
All reactions