Прежде, чем мы двинемся вперёд, давайте закончим тему использования миксинов как функций, поднятую во второй части. Без этого будут непонятны некоторые синтаксические моменты.

❯ Функциональный подход

❯ Возвращаем найденное, цена договорная

Как я рассказывал ранее, миксины могут выступать не только в роли базовых CSS-классов, но и в роли функций, которые что-то рассчитывают и возвращают. При этом, никакого ключевого слова для возврата значений (типа return) в LESS нет — вместо этого автоматически возвращается карта (map) всех локальных переменных.

// Миксин в роли базового класса.
.size(@size)
{
	width: @size;
	height: @size;
}

// Миксин в роли функции, которая высчитывает, какой процент от
// ширины заданной картинки составляет переданный размер в пикселях
.rel-x(@x, @src)
{
	@width: unit(image-width(@src));
	@return: unit((@x / @width * 100), %);
}

Пара слов о миксине .rel-x(). Иногда при создании пользовательского интерфейса нужен общий фон, заданый картинкой, на котором располагаются отдельные элементы управления.

Если у фона нет повторяющихся элементов, его не оптимизировать. (Пример фона взят из стоковой библиотеки).
Если у фона нет повторяющихся элементов, его не оптимизировать. (Пример фона взят из стоковой библиотеки).

Как их позиционировать? Указывать отступы в пикселях? Это удобно, но интерфейс перестаёт быть масштабируемым, а значит и адаптивным. В процентах? Это позволит сохранить масштабируемость, но смещения придётся самому вычислять на калькуляторе, а потом таскать неудобоваримого вида десятичные дроби. Тут и пригодится этот миксин, который сгенерирует проценты относительно заданной картинки задника из координат в пикселях.

ℹ️ Дизайнеру на заметку
При публикации скриншотов и видео из приложения, «голые» картинки всегда выглядят немного сиротливо. Хуманами они воспринимаются лучше, когда поставлены в контекст. Так что пририсуйте к ним рамку в виде экрана и чашку кофе / пончик / наушники где-нибудь сбоку. Видео, понятное дело, потребует отдельного элемента в разметке, а задник может превратиться в… э-э… передник, как на примере ниже (чтобы сделать модную «чёлочку» у экрана, он должен находиться сверху). Для позиционирования скриншота/видео относительно этой рамки, миксины .rel-x()/.rel-y() тоже будут весьма полезны.

Скриншоты и видео лучше воспринимаются в контексте.
Скриншоты и видео лучше воспринимаются в контексте.

При вызове миксина .rel-x() он вернёт карту из двух локальных переменных: @width и @return. Их затем можно использовать, чтобы задать свойства CSS. Пусть на нашем заднем фоне для какого-то контрола предусмотрено «окно» с координатами 80, 50. Предполагается, что задник культурно-нейтральный (он не отражается справа налево для ближневосточных юзеров и не начинает идти сверху вниз для дальневосточных, подробнее о мультикультурализме см. тут), а значит мы можем задавать для позиционирования старые добрые left и top.

@bg-src: '../images/bg.png';

.subcontrol-1
{
	@x: 80;		// Жаль, что нет способа хранить пары
	@y: 50;		// координат в метаданных картинки :(

	@x-percent: .rel-x(@x, @bg-src);
	left: @x-percent[@return];
	// Если понадобится, можно использовать в расчётах и
	// попутно найденную ширину задника: @x-percent[@width]

	@y-percent: .rel-y(@y, @bg-src);
	top: @y-percent[@return];
}

Если нам нужна только одна локальная переменная, её можно извлечь сразу при вызове миксина, без промежуточного объекта @x-percent:

left: .rel-x(@x, @bg-src)[@return];

А ещё для удобства в LESS принято следующее правило — если мы не указываем в квадратных скобках имя переменной, возвращается последняя:

left: .rel-x(@x, @bg-src)[]; // Вернёт @return

Поэтому-то я и назвал её @return. Рекомендую этот приём: так удобнее следить, что она последняя по порядку, если вы пишете код в той же логике, что и с оператором return в обычных языках (таких, как Javascript).

❯ Чистота — залог здоровья (и источник багов)

Авторы LESS, видимо, вдохновлялись функциональными языками программирования. Иначе чем объяснить, что если миксин выступает в роли функции, эта функция обязана быть чистой (pure), то есть не иметь побочных эффектов?

«Но позволь!», скажете вы. «А как же твой .assert() из предыдущей части, который одновременно проверяет картинку и возвращает её размеры?». Дело в том, друзья, что побочный эффект у .assert() очень своеобразный: упасть в случае необходимости и свалить компилятор. Можно ли такое вообще считать побочным эффектом — вопрос философский, но позитивный побочный эффект (то есть, генерацию CSS) так получить точно нельзя. Нет, если вызвать миксин с побочными эффектами как функцию, это не будет считаться ошибкой. Но и генерировать он тоже ничего не будет. Если мы перепишем .rel-x() следующим образом:

.left-rel-x(@x, @src)
{
	@width: unit(image-width(@src));
	@return: unit((@x / @width * 100), %);
	left: @return;
}

…и попробуем вызвать вот так:

.subcontrol-1
{
	@x: 80;
	@y: 50;

	@left: .rel-x(@x, @bg-src)[];
	// Запоминаем координату для последующего использования в браузере
	--left: @left;
}

…то окажется, что свойство left он нам не сгенерировал. По квадратным скобкам [] компилятор видит, что мы вызываем миксин как функцию, и обрубает ему все побочные эффекты. Вот так: или дудочка, или кувшинчик. А если хотите и то, и другое, то сначала вызовите миксин просто (как базовый класс), а потом — с квадратными скобками (как функцию). Что нарушает DRY, и о чём ваш покорный слуга открыл тикет («Дайте возможность маркировать миксины как грязные функции с побочными эффектами!»), но воз, увы, и ныне там.

❯ Больше скобок богу скобок, больше переменных богу переменных

Помню, читал я чей-то комментарий к статье уважаемого коллеги @melnik909. Мол, здорово там у вас, в HTML: жизнь кипит, бурлит, появляются новые фичи, старые ошибки исправляются, благодать! Ну, что сказать… чужое болото всегда зеленее. И в HTML вообще, и в CSS в частности, полно подводных камней, граблей и непродуманностей, которые там живут с рождения, и которые, кажется, никто не собирается исправлять. И я сейчас говорю даже не про фичи (какое-нибудь спорное свойство), а буквально про основы синтаксиса.

Замечательный пример — сепараторы. Запятыми принято разделять значения, образующие список: background-image: url('1.png'), url('2.png'), url('3.png');. Пробелами разделяют подсвойства shorthand'ов (составных свойств): border: solid 1px red;. Кроме того, пробелы используют для т.н. N-value syntax (где N — целое число). Например, для background-size используется two-value syntax: background-size: 200px 300px;, где первое значение — ширина, а второе — высота. Как будто всего этого мало, появился третий вид сепараторов. Например для того, чтобы разделить положение и размер фона:

background: no-repeat center/10% url("/shared-assets/images/examples/star.png");

И, конечно же, других вариантов, кроме слеша /, для третьего сепаратора не нашлось. И это поставило жирный крест на возможной будущей арифметике в выражениях. Взяли бы, скажем, |, это поставило бы жирный крест на дизъюнкции (операции ИЛИ) — а видит бог, без дизъюнкции в CSS прожить гораздо легче, чем без арифметики! Вот и приходится для арифметики использовать специальную функцию calc(), где слеш означает деление. Именно в CSS это страданий особо не причиняет, потому что calc() — штука динамическая, а такие как раз и описываются функциями. Но это создаёт проблемы для написания препроцессоров и интеграции с другими языками. Потому, что теперь неизвестно, что хотел сказать автор: поделить одно число или переменную на другое, или записать оба значения через сепаратор.

Создатели LESS тоже не ударили в грязь лицом перед коллегами, и подкинули дополнительных проблем сверху. Они ввели разные режимы компиляции, в которых слеш означает разные вещи. А чтобы стало ещё веселее, они пошли за ружьём управлять этими режимами из самого исходного кода нельзя. Как тут не вспомнить добрым словом C++, где директива #pragma позволяет из исходника задавать всё, что обычно задаётся ключами командной строки (вплоть до подключения бинарных библиотек). Когда вы компилируете LESS-код из браузера, это не проблема: компилятор представлен Javascript-объектом, и ему можно задавать свойства. Однако я уже объяснял, почему это плохая идея и лучше компилировать вчерновую при сохранении в IDE, а начисто —  на билд-сервере. Но это означает, что результат не будет зависеть эксклюзивно от исходников, он будет зависеть ещё и от параметров в конфигурационных файлах на билд-сервере. При проектировании своих языков не делайте так, пожалуйста: один и тот же исходник всегда должен давать один и тот же результат!

К счастью, дефолтный режим совпадает во всех средах компиляции, что немного уменьшает головную боль. В этом режиме круглые скобки вокруг выражения в сочетании с присвоением значения переменной дадут понять компилятору, что мы хотим вычисления значения. Так мы плавно подъехали к очередной тонкости написания миксинов-функций, на которой я хочу остановиться. Короткий рецепт такой: выносите все части вычисляемого выражения в отдельные переменные, и берите их в скобки, чтобы компилятор знал, что вы хотите произвести деление и другие арифметические операции. (А если хотите использовать слеш не для деления, а как сепаратор — используйте форматирование строк, хе-хе). Вот пример из реального проекта, где левую и правую части надо было поделить в пропорции 3 к 5, а саму пропорцию скормить браузеру:

@left-height-factor: (3/8);
@right-height-factor: (5/8);
--left-height-factor: @left-height-factor;
--right-height-factor: @right-height-factor;

❯ LESS: «Я теку от таких программистов!»

И последняя по порядку (но не по значению) тонкость, связанная с предыдущей. (С объявлением большого числа локальных переменных).

Представьте себе, что у вас есть такой код:

.mixin-1()
{
	@v: 5px;
	b: @v;
}

.mixin-2(@v)
{
	a: @v;
	.mixin-1();
}
.test
{
	// В нашем примере мы не просто передаём аргумент
	// "7px", а явно указываем, что это параметр @v.
	// Делать так не обязательно.
	.mixin-2(@v:7px);
}

«Что он сгенерирует?», спрашивает знатоков телезритель из Костромы.

Чёрный ящик в студию!
.test {
  a: 5px;
  b: 5px;
}

Немного не то, что вы ожидали, правда? Всё дело в том, что при вызове миксина без [], как генерирующего (т.е. как базового класса, а не чистой функции) происходит утечка переменных в контекст вызывающей стороны. И самое замечательное тут то, что утёкшие переменные перезаписывают параметры. А поскольку LESS — препроцессорный язык, то даже перестановка вызова .mixin-1(); в конец, после присвоения значения a: @v;, на результат не повлияет никак (компилятору пофиг, в каком порядке вы что-то делаете).

Я считаю, это крупный косяк со стороны проектировщиков языка.

Бороться с ним можно, например, так:

.mixin-2(@v)
{
	a: @v;
	// Ловим утечку в новый скоуп, затем всё, что
	// сгенерировалось в этом скоупе, подставляем
	// в верхний скоуп за счёт селектора &
	& {	.mixin-1();	}
}

Что даёт, наконец, ожидаемое:

.test {
  a: 7px;
  b: 5px;
}

Но не будем же мы просто на всякий случай заворачивать в & { } вызов каждого миксина! Что же делать, спросите вы? Мой совет: давайте переменным длинные, осмысленные имена с учётом контекста. Не просто @v или @width, а, например, @compass-image-width или @search-bezel-image-width. Что, кстати, только повысит читаемость исходников. Вот так недостатки языка можно использовать как стимул для внедрения хороших практик в свой проект /s.

Статически типизированный CSS

Ну вот мы и добрались до статической типизации.

Начнём с вопроса: как добавить статические типы в динамически типизированный язык, оставаясь в рамках исходного синтаксиса?

❯ Пойдёмте с нами, Верочка, цыганская венгерочка

Жил да был Чарльз Симони. Американский космический турист, как рекомендует его Википедия. А ещё — плейбой-миллиардер, поднявшийся на микрософтовских опционах, любовник телезвезды Марты Стюарт, руководитель проекта MS Word и автор многих интересных идей.

Вот он, идейный вдохновитель. (Не в смысле опционов и телезвёзд, увы).
Вот он, идейный вдохновитель. (Не в смысле опционов и телезвёзд, увы).

Одной из этих идей была система именования переменных, которая позволила бы сделать код понятнее и, по возможности, безбажнее. В соответствии с этой системой, например, вам не надо было ни запоминать, ни каждый раз подробно расписывать: «длинный-указатель-на-буфер-хранящий-строку-завершающуюся-нулём-и-содержащую-заголовок» (Long Pointer to String ended with Zero containing Caption). Достаточно было назвать переменную lpszCaption. Вот это lpsz выглядело для американцев как европейский язык с шипящими, записанный латиницей, а поскольку Чарльз — этнический венгр, систему назвали «венгерская нотация».

Вообще-то, венгерских нотаций имеется аж две штуки: системная и прикладная. В системной в имени переменной присутствует префикс, обозначающий её тип. i — int, b — bool, f — float и так далее. Например, iSize это «переменная Size типа int». В прикладной же венгерке префикс обозначает семантику (i — index, n — number, т.е. количество, и т.п.), а iSize превращается в «индекс размера» (что бы это ни значило). На практике оба варианта взаимно мутировали и породили систему, положенную в основу WinAPI/MFC, на которых я в детстве учился программированию.

Идея проста, как и любая чужая гениальная идея (спасибо, Чарльз!).

Мы возьмём префиксы системной венгерской нотации, и будем проверять при компиляции, соответствуют ли значение каждой переменной типу в её имени, а аргументы — типам в именах параметров при вызове миксинов.

❯ А оно тебе надо?

Давайте ещё раз повторим, зачем мы всё это делаем.

Дизайн штука семантическая, а ошибки семантики автомат находить не умеет по определению. В лучшем случае, браузер покажет в консоли предупреждение о невалидном значении CSS-свойства, но никогда не сможет понять, что всё разъехалось. А ведь полно таких ситуаций, когда что-то разъезжается лишь при сочетании нескольких условий, и даже ручное тестирование без продуманного тест-плана такой баг не поймает. Когда у вас десятки и сотни тысяч пользователей, и у каждого — свои настройки, свои сценарии и свои данные, это превращается в проблему.

Компиляция, раз уж у нас она появилась в пайплайне, даёт хорошую возможность конвертировать семантические ошибки в синтаксические (а синтаксические ошибки — находить автоматически и не давать собраться проекту). Для этого надо лишь явно прописывать ограничения. Например, так: «Тема оформления может быть только тёмная или светлая. Если она какая-то иная, поздравляю: у вас девочка волчанка ошибка». Или так: «Этот кусок пользовательского интерфейса — адаптивный, а значит все размеры должны быть долями базового размера шрифта». Или так: «Этот пользовательский интерфейс жёстко привязан к экрану конкретного устройства, и все размеры должны быть в пикселях».

Очень продуманные языки, такие как Rust, творят в этом плане чудеса. Там успешная компиляция означает, что (скорее всего) в программе нет ошибок (по крайней мере, связанных с памятью). Я всегда скептически относился к компиляции как способу поиска ошибок (предпочитая юнит- и другие автоматизированные тесты), но Rust изменил моё мнение (на восторженное). В случае же с LESS… всё зависит от умения находить и формулировать ограничения.

Пример вы могли видеть на КДПВ:

❯ Проектируем типы

Для начала перечислим базовые типы, которые мы собираемся реализовать:

  • b — булевский;

  • i — целочисленный;

  • f — вещественный;

  • c — цветовой.

Поскольку корневой язык у нас — CSS, а там принято использовать дефис -, чтобы разделять слова, пусть префикс, кодирующий тип, отделяется подчёркиванием _. Так будет нагляднее (а ещё — проще парсить).

Вот примеры имён переменных и параметров миксинов:

  • b_set-height — булевский флаг, означающий, устанавливать ли высоту;

  • f_zoom-factor — коэффициент масштаба;

  • i_frame-count — целое число кадров анимации;

  • i_timeout — целое число секунд для синхронизации с другими процессами, где минимальный шаг длительности — секунда;

  • c_primary — акцентный цвет интерфейса;

  • c_warning — цвет для привлечения внимания.

Помните, я обещал показать, как сделать «более-менее полноценный enum»? Добавим объявляемые типы (registered types):

  • E-<имя> — перечисления (enum);

  • L-<имя> — диапазоны (limit).

ℹ️ Почему limit, а не range?
Потому, что в LESS уже есть функция range(), которая генерирует диапазон целых чисел. Её, в сочетании с each(), мы использовали для организации for-подобных циклов.

Соответственно, имя переменной E-theme_current будет означать, что она принадлежит к типу «перечисление theme», и значение должна иметь строго из этого списка (как легко догадаться, эта переменная хранит текущую тему оформления).

А имя L-margin_box будет означать, что переменная принадлежит к типу «диапазон margin», и её значение должно лежать в соответствующих границах. Непосредственно имя переменной (часть после префикса) даёт понять, что это отступ для контейнера .box.

Если мы опишем диапазонный тип margin как число от 1 до 5 (про единицы поговорим чуть позже, но в данном случае это будут проценты viewport'а), и все отступы будем определять как переменные этого типа, то сможем гарантировать, что если у нас что-то и разъедется по причине отступов, то хотя бы не чудовищно. Или мы можем создать корпоративный дизайн-бук с общими требованиями (в том числе, к отступам), а затем гарантировать, что конкретная страница ему соответствует. Причём, прямо в процессе работы (при попытке сохранить файл) извещая «версталу» о найденных нарушениях.

❯ Объявись, покажись, колеском покрутись

Объявляемые типы нужно… э-э… объявлять, Карл!

Вот как это будет выглядеть синтаксически:

// Перечисление по имени theme, описывающее
// допустимые темы оформления.
enum(theme, system, light, dark);

// Диапазон по имени margin
// от 1 до 5 включительно.
limit(margin, 1, 5);

Вы, наверное, уже догадались, что технически enum() и limit() это плагинные функции, которые мы имплементируем на Javascript.

❯ …и проверяем

Если не дунуть, то, как известно, никакого чуда не произойдёт. В роли дутья для чуда статической типизации у нас выступает специальный миксин .check(), который и проверяет соответствие значения переменной и её типа:

@c_primary: royalblue;
// Проверяем переменную после объявления:
.check(c_primary);
.set-bg-color(@c_bg)
{
	// Проверяем параметр прямо в процессе использования:
	background-color: .check(c_bg)[];
}
.set-size(@i_w_px, @i_h_px)
{
	// Все параметры можно проверить за один
	// раз, перечисляя их через пробел:
	.check(i_w_px i_h_px);

	width: @i_w_px;
	height: @i_h_px;
}

При попытке передать в миксин значение, недопустимое для данного типа, компилятор (а значит, и IDE при сохранении файла) выдаст ошибку:

Вот примеры таких ошибок:

.box
{
	// Type mismatch. i_w_px:100: invalid units. px expected, no units found
	.set-size(100, 200);
}
.box
{
	// Type mismatch. i_w_px:33.3333px: integer expected
	.set-size(33.3333px, 200px);
}
.box
{
	// Type mismatch. i_w_px:'100px': integer expected, string found
	.set-size('100px', 200px);
}

Хотя т.н. keyword value auto — допустимое значение ширины, оно не соответствует типу Integer:

.box
{
	// Type mismatch. i_w_px:auto: integer expected
	// 
	.set-size(auto, 200px);
}

Можно ли было обойтись без ручных вызовов миксина .check()? В конце концов, у нас ведь есть компилятор, которые поддерживает плагины. Представляете, как было бы красиво, если бы мы просто называли переменную @i_w_px, а дальше всё происходило само собой? Я — представляю. Очевидно, чтобы добиться такого эффекта, нам нужно было бы подписаться на два события при компиляции:

  • Присвоение значения переменной (для проверки соответствия значения и типа, указанного в её имени);

  • Вызов миксина (для проверки соответствия аргументов и типов, указанных в именах параметров).

С этим вопросом я и пошёл к разработчикам LESS. Вопрос про присвоение значений они тихо проигнорировали, а для отслеживания вызовов посоветовали мне воспользоваться паттерном Visitor, что я и сделал. И даже показали пример.

Вскоре выяснилось, что в обработчик события вызова миксина в качестве аргументов приходит исходное CSS-выражение (а не его значение). О чём я и пожаловался. Разработчики LESS подумали, и ответили простынёй кода, которая в самом плагине парсила таблицу переменных и вычисляла значение выражений.

Затем выяснилось, что если вызвать один миксин и отдать результат вызова (при помощи []) в другой миксин как аргумент, то значение вычислено тоже не будет. Разработчики LESS подумали, и ответили простынёй кода, которая в самом плагине рекурсивно вызывала миксины для вычисления возвращаемого значения.

Затем выяснилось, что понятие «вызов миксина» можно трактовать по-разному. Событие вызова срабатывало, когда я просто описывал миксин (не вызывая его нигде в прикладном коде), зато не срабатывало, когда я вызывал миксин из цикла (10 вызовов с разными аргументами, среди которых могли быть валидные и не валидные, приводили только к одному срабатыванию обработчика). К этому времени уже половина кода компилятора переехала в мой плагин, но стабильного результата как не было, так и не предвиделось.

Постепенно наша переписка стала всё больше походить на песню “Stan” Эминема. В смысле, на мои всё более отчаянные и злобные репорты о новых найденных кейсах они стали глухо отмолчиваться. (Но хотя бы в багажнике никто не утонул, что уже неплохо).

«…твои запросы становятся всё извращённее изощрённее, в дальнейшем читай, пожалуйста, код компилятора!»
— Daniel, мейнтейнер LESS.

Я отправляю очередной репорт разработчикам.
Я отправляю очередной репорт разработчикам.

Так что, на данный момент вызывать миксин .check() всё ещё надо вручную. «Но мы работаем над этим!» (ц). Нет, серьёзно, я при помощи мейнтейнеров потихоньку вникаю в компилятор, и, возможно (хотя стопроцентных гарантий не дам), в одной из следующих версий плагина вызов .check() удастся автоматизировать, о чём я обязательно напишу хотя бы постик. Так что, подписывайтесь, чтобы его не пропустить.

Тем временем, я с кулфейсом сказал себе: проверка типов на практике нужна преимущественно при написании системных миксинов, а они пишутся только один раз. (Вызываются — многократно). Соответственно, при их написании нет большой проблемы не забыть вызвать .check(), ну а от пользователей миксинов такого не потребуется. В общем, всё just as planned.

Just as planned.
Just as planned.

❯ Как всё это устроено?

Теперь давайте поглядим на имплементацию.

Прежде всего, нам потребуется код на Javascript, который позволял бы объявлять enum'ы на Javascript'е, чтобы перечислить все возможные типы, в т.ч. enum'ы, в LESS.

Вот он:

// Declares an enum pseudotype.
const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({ [k]: Symbol(k) }))));

// Static types.
const VariableType = makeEnum('Integer', 'Float', 'Boolean', 'Color', 'Enum', 'Limit', 'CssProperty');

// Registered enum types.
const lessEnums = [];

// Registered limit types.
const lessLimits = [];

Этот код, включая пустые массивы под хранение всех перечислений и диапазонов, мы вставляем в начало плагинного файла less-lib.js. (Подробнее о структуре файлов см. предыдущую часть).

Для статической типизации наш плагин добавляет три функции: enum(), limit() и check() (они не генерируют никакой CSS-код, и поэтому возвращают false — иначе они должны были бы возвращать строку):

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;
});

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;
});

functions.add('check', function (name, value, css)
{
	checkVariableType(name.value, value, css.value);

	return false;
});

Первые две (enum и limit) служат для объявления перечислений и диапазонов (добавляя при вызове соответствующие типы в массивы объявленных типов). Попутно мы выполняем все нужные проверки («тип не может быть переобъявлен», «диапазон задаётся вещественными числами без единиц измерения», «минимум не может быть больше максимума»).

Третья же функция, check(), выполняет проверку соответствия значения переменной её типу, и, в случае необходимости, останавливает компиляцию с подробной ошибкой. Она принимает на вход имя переменной, значение переменной и исходный текст CSS-выражения, из которого значение было получено. Последнее пригодится нам позже, для одной очень крутой штуки.

Как я уже жаловался, ручной вызов проверки переменных / параметров сам по себе выглядит достаточно коряво. Но было бы втройне позорно, если бы мы вызывали функцию check() напрямую, указывая имя переменной трижды: как строку, как значение и как выражение. Чтобы этого не делать, и нужен миксин .check() (его я добавил в библиотеку миксинов lib.less):

.check(@names)
{
	each(@names,
	{
		check(@value, @@value, %('%a', @@value));
	});

	@return: if(length(@names) = 1, @@names, false);
}

Надеюсь, ранее вы обратили внимание, что в миксин .check() мы передавали не сами переменные, а их имена (т.е. без символа @ в начале)?

.set-bg-color(@c_bg)
{
	// "c_bg", а не "@c_bg"
	background-color: .check(c_bg)[];
}

.set-size(@i_w_px, @i_h_px)
{
	// "i_w_px i_h_px", а не "@i_w_px @i_h_px"
	.check(i_w_px i_h_px);
}

И вот почему: имея строку с именем переменной (например, c_bg), её значение и исходный код выражения можно получить автоматически:

… check(@value, @@value, %('%a', @@value)); …

Разберём все три аргумента подробнее.

  • @value. Это строка с именем переменной (c_bg). Из неё будет извлечён префикс (c) и определён тип (VariableType.Color).

  • @@value Это значение переменной (@c_bg). Оно находится путём приписывания второй @ к строковой переменной, хранящей имя другой переменной (@value). (Если вы знаете C/C++, можете считать, что двойная собака @@name это аналог разыменования указателя).

  • %('%a', @@value). Это извлечение из переменной (@c_bg) строки с исходным кодом CSS-выражения.

Что последнее вообще значит? Давайте откроем MDN и почитаем про свойство background-size:

/* Multiple backgrounds */
background-size: auto, auto; /* Not to be confused with `auto auto` */

MDN предупреждает нас, чтобы мы не путали auto, auto (автоматический размер для двух слоёв) и auto auto (автоматический размер для двух измерений одного слоя).

К сожалению, если оба этих значения передать в плагинную функцию, изнутри плагина мы увидим абсолютно одинаковый массив из двух элементов.

Иными словами, если у нас есть две переменных, @bg-size-1: auto, auto и @bg-size-2: auto auto, наша функция check(name, value, css) в обоих случаях получит от LESS-парсера вторым параметром следующий объект:

{
	value:
	[
		{
			value: "auto"
		},
		{
			value: "auto"
		}
	]
}

Информация о разделителе (пробел или запятая) исчезнет, как при падении в чёрную дыру, и отличить оба случая не получится. Поэтому значение переменной @@value мы пропускаем через встроенную функцию LESS %(). Это функция форматирования строк, аналог String.Format() / printf() в других языках. Флаг форматирования %a означает «подставь переданный объект как строку». Форматирующая функция вернёт для @bg-size-1 строку auto, auto, а для @bg-size-2 — auto auto. А нам как раз это и нужно. И плагинная функция check() получает третьим параметром (css) исходное, нераспарсенное значение.

Что ещё сказать о миксине .check()?

.check(@names)
{
	each(@names,
	{
		check(@value, @@value, %('%a', @@value));
	});

	@return: if(length(@names) = 1, @@names, false);
}

Вы видите, что мы считаем, что на вход нам подан не один аргумент, а целый список (@names). Именно поэтому мы и можем проверять сразу все параметры миксинов, разделяя их пробелами:

.check(i_w_px i_h_px);

Миксин .check() переберёт их как список, при помощи each() извлекая каждую строку с именем переменной как @value. (Если мы передадим только одно имя, это равнозначно передаче списка из одного элемента).

Наконец, если мы передаём на проверку только один параметр или переменную (if(length(@names) = 1), то миксин возвращает её значение ( @@names), благодаря чему проверку можно объединить с использованием:

background-color: .check(c_bg)[];

Встроенная функция LESS if() проверяет первый аргумент и возвращает в зависимости от истинности второй или третий. Для реального списка (а не одиночного значения) мы возвращаем false. К сожалению, изнутри миксина .check() нельзя проверить, что мы собираемся сделать с результатом (@return) и собираемся ли вообще, поэтому и нельзя вместо false вызвать die().

❯ Суффиксы (единицы измерения)

Если венгерская нотация не подразумевала единиц измерения (кроме безразмерного индекса и количества для прикладной венгерки), то при работе с LESS/CSS никто не мешает нам сделать их частью типа и проверять при компиляции.

Для этого добавим к именам переменных суффиксы, означающие единицы измерения:

  • @i_var (нет суффикса) означает, что переменная должна иметь целочисленное значение (в любых единицах).

  • @i_width_px (суффикс px) означает, что переменная должна иметь целочисленное значение в пикселях.

  • @i_width_rem (суффикс rem) означает, что переменная должна иметь целочисленное значение в rem'ах.

Поскольку символ процента % использовать как часть имени переменной нельзя, вместо него надо писать суффикс _percent.

Кроме того, я ввёл понятие zero units. Если добавить к имени переменной в конце символ подчёркивания _, но не указывать никаких единиц, переменная должна быть безразмерным числом:

  • f_zoom-factor_ (вещественный коэффициент увеличения).

  • i_frame-count_ (число кадров в анимации).

По очевидным причинам переменные типов Boolean, Color и Enum иметь суффиксы не могут.

Что касается диапазонов, то при объявлении диапазона указываются только вещественные значения минимума и максимума. Единицы измерения при необходимости задаются уже для конкретных переменных этого типа.

Например, если мы объявим диапазоны margin и zoom-factor следующим образом:

limit(margin, 1, 5);
limit(zoom-factor, 1.0, 3.5);

…то переменная @L-margin_box-hz-margin_px (смысловая часть имени box-hz-margin значит «горизонтальный отступ для .box») должна будет содержать значение от 1 до 5 пикселей, а переменная @L-margin_box-hz-margin_vmax должна будет содержать значение от 1 до 5 vmax'ов.

ℹ️ Что такое vmax'ы?
vmax это единица измерения, равная 1% от размера viewport'а. Какого именно размера зависит от того, что больше: ширина или высота. Если нам нужен 1% от наименьшего размера, для этого существует единица измерения vmin.

Ну а переменная @L-zoom-factor_value_ должна быть безразмерной (из-за пустого суффикса, т.е. _ на конце) и лежать в диапазоне от 1.0 до 3.5.

Проверка диапазонов происходит включительно, то есть минимум и максимум — валидные значения. Чтобы сделать проверку «строго меньше» и «строго больше», просто допишите на конце .000001 и .999999. Это всё-таки CSS, а не биткойн-кошелёк, сверхточность в UI не нужна.

❯ Проверки значений на соответствие типам

Я не буду разбирать каждую строку имплементации function checkVariableType(varName, varValue, varCss), которую вызывает плагинная функция check(), и других вспомогательных функций. Желающие могут изучить код плагина самостоятельно, благо он прокомментирован (и достаточно тривиален).

Вместо этого я остановлюсь на отдельных интересных моментах.

Хелпер function compareValues(a, b) сравнивает два уже распарсенных LESS'ом значения (что используется для проверки, входит ли значение в объявленный ранее enum). Благодаря тому, что значения сравниваются после парсера, варианты red, rgb(255, 0, 0) и #ff0000считаются одинаковыми. А какой-нибудь lighten(hsl(90, 80%, 50%), 20%) будет считаться равным #b3f075.

Хелпер function getTypeInfoFromPrefix() парсит префикс и возвращает тип (как значение созданного нами JS-перечисления VariableType).

Хелпер function checkSinglenessForType() проверяет, что для встроенных типов (i, b, f, c), перечислений (enum) и диапазонов (limit) переменная содержит единственное значение, а не список. Так сделано для упрощения. Хотите передать в миксин строго типизированное значение для свойства background-size (например, «целое число пикселей вдоль обеих осей»)? Передавайте ширину и высоту по отдельности, в двух разных типизированных параметрах @i_w_px и @i_h_px. Затем объединяйте внутри миксина: background-size: @i_w_px @i_h_px; (конечно, после проверки: .check(i_w_px i_h_px);).

Хелпер function checkUnits() проверяет допустимость единиц измерения для данного типа и совпадение ожидаемых и фактических единиц у переменной. Проверка валидности самих единиц по стандарту CSS не производится. (Про CSS-валидацию мы поговорим ниже, потому, что, забегая вперёд, да, я её тоже реализовал).

При проверке целой, вещественной, булевской или диапазонной (то есть, тоже вещественной) переменной проверяется отсутствие кавычек (поле quote распарсенного объекта). Потому что у нас всё по-взрослому: не просто статически типизированный CSS, а строго статически типизированный CSS. Неявные кастинги из строк в нём не допускаются. А не допускаются они, например, потому, что LESS'овский оператор when (аналог оператора if в других языках) сам строго типизирован, и если передать в него строку, он обработает её не так, как мог бы ожидать программист. Да и браузеры, если помните, терпимостью в этом вопросе тоже не отличаются. ("red" браузер не считает валидным значением для свойства color и игнорирует).

Цветовые переменные проверяются на валидность очень просто: распарсил ли парсер LESS значение переменной как цветовое или нет. (И это снимает изрядную головную боль поддержки всех разрешённых форматов).

Ну а хелпер function printObject(obj) я написал, пока отлаживал всю эту радость. Когда у вас нет браузера и консоли, для отладочного вывода объектов становится нужно учитывать ранее неочевидные нюансы. Например, что json'ификация объектов с циклическими ссылками (которые очень любит генерировать компилятор LESS) падает по переполнению стека. Или что в результирующей строке мы имеем «египеццкую силу» фигурных скобок, которую лично я на дух не выношу (форматирование, «…будто я египтянин, и со мною и Солнце, и зной»).

Встроенный CSS-валидатор

После того, как я реализовав всё вышеописанное, мне захотелось выйти за рамки. За рамки типизаций в императивных языках. В конце концов, это же CSS, а не C/C++, Java или C#.

Почему бы нам не превратить каждое CSS-свойство в тип переменной и не проверять валидность её значения? Тогда, например, миксин, устанавливающий фон элемента, мог бы требовать в качестве параметра @background_button-bg строго валидное значение для свойства background. А миксин, устанавливающий свойства длин (width), углов (rotate) и промежутков времени (animation-duration), мог бы проверять валидность переданных единиц измерения.

С этой целью я решил проверять каждый префикс на предмет, не является ли он допустимым по стандарту CSS свойством. А значение — валидным значением этого свойства.

Однако, мне крайне не хотелось писать с этой целью свой лексический парсер и, главное, обновлять его потом при каждом мажорном обновлении браузеров. В конце концов, если бы мне нравились подобные вещи, я бы стал разработчиком компиляторов. Но я предпочитаю разрабатывать пользовательские интерфейсы, а разработка плагинов для компилирующего препроцессора — всего лишь суровая необходимость, связанная с созданием (по возможности) массовых продуктов. Что налагает требования на надёжность и безопасность.

Так что, я решил воспользоваться готовым решением.

И вот что забавно: в прошлых частях я, если помните, пинал зумеров за склонность всюду вместо файлов тащить веб-сервера. Однако, когда дошло до дела, то первое, о чём я подумал, это о validator-as-a-service. А именно, о https://jigsaw.w3.org/css-validator/. Потому что… ну, а почему бы и нет? Кто лучше W3C разбирается в валидности CSS с учётом текущего стандарта? А поскольку это форма, а не API, то я просто взял готовый модуль Node.js, специально написанный для работы с этим URL'ом, который потащил чёртову прорву зависимостей… то есть, по моим собственным меркам, опустился ниже плинтуса.

И когда я уже собрался тестировать, насколько мой DDOS серверов W3C замедлит компиляцию (а значит, и ежесекундное сохранение файла в IDE), как до меня дошло.

Разработчики компилятора LESS почему-то совсем не предусмотрели в своей архитектуре асинхронное взаимодействие с ним из колбеков внутри плагинов. (Как непредусмотрительно!)

«О каком взаимодействии идёт речь?», возможно, спросите вы.

Да будет вам известно, что мною опытным путём было установлено: расширение Web Compiler 2022+ к моей IDE (Visual Studio 2022) за каким-то… гм… интересом трактует исключения, выброшенные асинхронно, как warning'и (а не ошибки). Что обесценивает всю идею «безопасного CSS», когда проект перестаёт собираться при малейшем подозрении, что что-то не так. Я обломился даже разбираться, как, почему, и можно ли исправить, а вместо этого решил… правильно, взять синхронный модуль Node.js.

Требование у меня к этому моменту осталось только одно: чтобы последний коммит в библиотеку был не позднее года назад. (С частотой обновления раз в год на практике жить можно. Браузерные движки поддерживают новые фичи с ещё большим лагом). И такой нашёлся (последний раз его обновили аккурат 11 месяцев назад).

Так родились две следующие вспомогательные функции:

const cssValidator = require('csstree-validator');

// 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;
}

Валидатор получает на вход кусок CSS и возвращает массив ошибок, длину которого мы и проверяем.

Вот для чего нам был нужен исходный код CSS-выражения! Потому, что захоти мы провалидировать значение следующей переменной:

@background_var:
		center / contain no-repeat
		url("/shared-assets/images/examples/firefox-logo.svg"),
		#eeeeee 35% url("/shared-assets/images/examples/lizard.png");
background: .check(background_var)[];

…и я бы застрелился восстанавливать этот исходник из пакета с пакетами массива объектов с массивами объектов. Тем более, что это, как я уже объяснил, невозможно из-за потери информации парсером.

Проверка валидности свойств (перед тем, как использовать префикс в качестве типа) была реализована нехитрым трюком с unset, который годится для вообще любого свойства.

Что ещё сказать о валидаторе?

❯ Установка

Ваш билд-сервер наверняка сделан на Node.js, поэтому просто установите в него 'csstree-validator. Расширение для моей IDE (VS 2022) тоже построено на отдельном экземпляре node.exe, и лежит по следующему пути:

C:\Users\%Profile%\AppData\Local\Temp\WebCompiler1.14.15\node_modules

Не сомневаюсь, что и расширения для VS Code, компилирующие LESS-файлы при сохранении, устроены так же.

❯ Единицы измерения

Для переменных типа <CSS Property> проверка единиц измерения слегка отличается от проверки единиц измерения числовых типов. Чтобы сделать её более логичной и полезной, допускается отсутствие единиц. Иными словами, для переменной @width_box_px допустимо значение auto (хоть оно и не в пикселях), но не допустимо значение 100% (хоть оно и валидно для свойства width). Иначе в комбинации <CSS Property> с единицами измерения не было бы особого смысла: можно было использовать просто вещественный тип f_.


Ну и главное: не стоит ждать от валидатора совсем уж волшебных вещей. Например, CSS-свойство transition работает с типом <custom-ident>, а это значит, что оно считает валидным что угодно вообще. И никакие проверки, кроме вдумчивого чтения, вас не спасут. И динамические, рантаймовые вещи (например, браузерные переменные) валидировать тоже не получится:

// Нет ли тут ошибок? Всей правды мы не узнаем.
color: var(--guess-what-i-am);

Хотя если вместо:

@width_css-var: var(--css-var);

…написать:

@width_css-var: --css-var;

…не завернув имя переменной в var(), такую ошибку плагин отловит легко.

Конфликты анимаций

И напоследок расскажу о последней функции своего плагина, которая до сих пор оставалась неупомянутой. (На протяжении этого сериала мы, в общем-то, потихоньку разобрали весь плагин).

В предыдущей части я говорил, что стараюсь не использовать id в прикладном коде. Мне кажется, это очень плохой паттерн проектирования. Судите сами: допустим, мы верстаем форму с полем ввода (<input>) и подписью к нему (<label>). Связь между ними устанавливается через атрибут for элемента подписи, который требует id элемента поля ввода. Такой способ связи лишает нас возможности создать шаблонный строительный кубик для разметки, состоящий из этих двух элементов. Если мы вставим его в документ несколько раз, неважно, на стороне браузера или на стороне бизнес-логики (сервера или десктопного приложения), то в каждой паре поле ввода будет иметь один и тот же id. Обидно, что мы вполне можем адресовать нужный экземпляр по индексу или по значению атрибута и из CSS, и из скриптов. Но сам браузер попарно связать элементы больше не сможет (при щелчке по подписи соответствующее поле ввода не активируется).

Ладно, в случае с полями и подписями всё не так плохо. Помимо атрибута for, который устанавливает связь явно, есть и неявный способ связать их воедино: можно просто вложить поле внутрь подписи, и это решит проблему связи.

Казалось бы, урок проектировщиками должен быть усвоен, но… В августе 2008-ого (уже после релиза HTML5!), была опубликована спецификация первой версии ARIA (стандарта и технологии для accessibility), где был предложен атрибут aria-labelledby и другие атрибуты с пометкой IDREFS #IMPLIED. То есть, требующие id. И вот их… может, конечно, и можно как-то объехать, но я такого способа не знаю.

Всё это, конечно, решается, но способами, далёкими от изящества. Например, движок шаблонизатора можно научить при инстанцировании генерировать GUID, и использовать его в качестве связующего идентификатора.

На уровне CSS есть свои идентификаторы, например, идентификаторы at-rule. Одним из которых является @keyframes (block at-rule), описывающий анимацию. В LESS вы можете вкладывать их внутрь классов, но при компиляции они станут просто глобальными объектами, потенциально порождающими конфликты имён. (Представьте себе анимацию left-to-right, которая может встречаться несколько раз в одном документе, и иметь в каждом случае свой смысл).

Естественно, что имея препроцессор, первым делом хочется автоматически генерировать GUID, как и в случае с ненужными id, используемыми лишь для связки. Так на свет появилась функция uniqueName(name). Оригинальное имя она вставляла в начало идентификатора, перед GUID'ом, чтобы анимацию было удобно отлаживать в DevTools.

При реализации я с интересом обнаружил, что привычный crypto.randomUUID() в среде, используемой компилятором LESS, мне недоступен. Конечно же, из соображений безопасности. Очень часто, натыкаясь на ограничение из соображений безопасности, я пытаюсь понять логику и даже спрашиваю других, но безуспешно. Почему нельзя было включить генерацию GUID'ов в стандарт EcmaScript? Чтобы фальшивая реализация со скрытыми закономерностями не испортила криптографию? А если я напишу свой фальшивый браузер? А главное — если уж так сильно переживать о безопасности, может быть просто не стоило делать генератор GUID'ов частью криптоинтерфейса?

Как бы то ни было, пришлось писать хелпер getGuid() на основе тривиального Math.random():

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);
	});
}
…
functions.add('uniqueName', function (name)
{
	const guid = getGuid();
	return `${name.value}-${guid}`;
});

И проблему конфликтов имён в LESS стало можно решать так:

@bottom-to-top: uniqueName('bottom-to-top');

@keyframes @bottom-to-top
{
	0%
	{
		…
	}

	100%
	{
		…
	}
}

.animated-titles
{
	//animation: animated-titles_bottom-to-top 8s linear infinite 0s;
	animation: @bottom-to-top 8s linear infinite 0s;
}

Самое смешное, что написав эту функцию, и переписав под неё код немаленького проекта, я посмотрел-посмотрел, да и откатил изменения. Во-первых, IDE ведь она только от нормальных людей бронированная совершенно не в состоянии понять, что речь тут идёт о самой обыкновенной анимации. Во-вторых, мне и самому не понравилось, как выглядит смесь кусков типа @bottom-to-top и 8s linear infinite 0s. И я решил перейти на схему именования типа BEM'овской: контекст_имя-анимации.

.animated-titles
{
	animation: animated-titles_bottom-to-top 8s linear infinite 0s;
}

Зачем же я тогда всё это рассказываю?

Во-первых, мне не понравилось — может, понравится вам, да и в VSCode, как говорят, парсер поумнее будет. Во-вторых, как писал велиий Кармак, «когда люди спрашивают меня, почему бы не сделать то-то или то-то, я обычно отвечаю: уже пробовал». В смысле, отрицательный опыт — тоже опыт. И, наконец, если бы я с самого начала сказал, что во избежание конфликтов полагаюсь на схему именования, это показалось бы несерьёзным. Сравнив оба варианта на практике, я понял, что это решение не так уж и плохо.

Исходный код

Архив с файлами

Фрагмент lib.less с необходимыми миксинами:

lib.less
@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:

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) }))));

// 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 (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 (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)
			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 (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);
			//
			// If @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);
			});
		}
	}
)

Файл с юнит-тестами test.less, который демонстрирует совместимости и несовместимости разных значений и типов:

test.less
@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

	background: .check(background_var)[];

	@animation-duration_period: 6s;
	.check(animation-duration_period);	// OK

	@rotate_angle-of-the-dangle: 120deg;
	.check(rotate_angle-of-the-dangle);	// OK

	@width_css-var-1: var(--css-var);
	.check(width_css-var-1);		// OK

	@width_css-var-2: --css-var;
	//.check(width_css-var-2);		// Fail		No var()
}

.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
}

На этом тему LESS предлагаю считать исчерпанной. Надеюсь, вам было полезно и/или интересно. А в следующий раз поговорим о чём-нибудь ещё из области веба и UI. Например, как можно плавно переключать CSS-анимации из любого промежуточного состояния. Или где и как можно применить 3D в UI обычных десктопных приложений. Или как я добавлял в Doom 2 компьютерные терминалы в стиле Doom 3 (конечно же, сделанные из браузера). Подписывайтесь, чтобы не пропустить! ?


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.

Комментарии (1)


  1. adminNiochen
    15.10.2025 07:20

    И в js и в css принят стиль K&R для фигурных скобок, а у вас везде bsd. У нас тут не c#