Я уже писал о распознавании имен БЭМ-сущностей на HTML-странице. В той статье речь шла о проверке на ошибки с точки зрения методологии БЭМ имен БЭМ-сущностей (блоков, элементов, модификаторов), привязанных к узлам HTML-дерева в виде слов (названий классов CSS) в составе значения атрибута «class» HTML-элементов. После проверки на ошибки проводилось распознавание типов БЭМ-сущностей и выводились результаты. Эта работа выполняется в небольшом скрипте на языке PowerShell. Скрипт получает через входной параметр имя файла с кодом на языке HTML. С помощью библиотеки «HTML Agility Pack» трансформируем код из файла в объект, представляющий HTML-дерево, и производим обход узлов этого HTML-дерева. HTML-дерево визуализируем в консоли, там же выводим сообщения об ошибках и результаты распознавания типов БЭМ-сущностей (при проверке и распознавании используем регулярные выражения). При выводе текста в консоль используем разные цвета, чтобы фокусировать внимание пользователя на главном. Предполагается, что скрипт будет работать в программах-оболочках «Windows PowerShell» версии 5.1 и «PowerShell» версии 7. Я работаю в операционной системе Windows 10. В той предыдущей статье проверялось и распознавалось каждое имя каждой БЭМ-сущности по отдельности. В данной статье будет рассматриваться проверка правильности совмещения (сочетания) имен БЭМ-сущностей на каждом отдельном узле HTML-дерева. (Пока что не будем рассматривать (отложим на будущее) реализацию проверки связей БЭМ-сущностей на разных узлах HTML-дерева.) Схематично работа скрипта на языке PowerShell выглядит примерно так: Этот код был подробно рассмотрен в предыдущей статье. Напомню, что в переменной $class содержится очередное слово (название класса CSS) из значения атрибута «class» HTML-элемента. Реализация функции testBEMNameClassic, проверяющей название класса CSS на ошибки с точки зрения методологии БЭМ, была рассмотрена в еще одной отдельной статье. Эта функция возвращает число 0, если ошибок не найдено. В противном случае она возвращает число, отличное от нуля, номер ошибки. Проверка производится с точки зрения классической схемы именования БЭМ-сущностей. Но в скрипт при желании можно добавить проверки имен БЭМ-сущностей по альтернативным схемам именования. К схеме действий из предыдущей статьи я добавил больше контекста. Это сделано потому, что в предыдущей статье я рассматривал анализ на ошибки и распознавание одного отдельного слова (названия класса CSS) из значения атрибута «class» HTML-элемента, то есть рассматривал внутреннюю часть цикла перебора слов, а теперь нам нужно будет рассматривать совмещение всех слов (названий классов CSS) на одном узле, то есть нужно будет выйти за рамки цикла перебора слов. Новый код будет добавлен до цикла перебора слов, после цикла перебора слов и внутри него. Для анализа сразу всех имен БЭМ-сущностей, размещенных на одном узле, нам понадобится совокупность информации об этих именах, которую и будем анализировать. Для реализации такой совокупности удобно использовать массив. Сначала я решил собрать массив типов БЭМ-сущностей, расположенных на одном узле. У нас после распознавания уже есть названия типов БЭМ-сущностей, это можно увидеть в коде на схеме выше. Но для анализа текстовые названия неудобны, поэтому я решил представить типы БЭМ-сущностей номерами от 1 до 6. Таким образом, нам нужен массив целых чисел. Меняем (дополняем) код на языке PowerShell в схеме из начала статьи: В этом коде показаны только две первые ветки распознавания типа БЭМ-сущности, но принцип, думаю, должен быть понятен. Создаем наш массив $arrBEMTypes снаружи цикла перебора слов, так как использовать мы его в итоге будем тоже снаружи этого цикла, после него. Выражение @() при инициализации переменной означает пустой массив. Оператор += в данном случае означает добавление нового элемента массива в его конец. (Впрочем, напомню, что на самом деле этот оператор создает новый массив длиннее старого на один элемент, а затем переписывает значения элементов из старого в новый массив, прибавляя значение нового элемента. Один из недостатков этой структуры данных в том, что из нее нельзя удалить элемент или нельзя в нее добавить элемент; эти операции выполняются с созданием нового массива нужной длины. Об этом сказано в документации.) После цикла перебора слов, если в массиве $arrBEMTypes есть элементы, выведем содержимое массива в консоль. По умолчанию элементы массива выводятся в консоль вертикально (на каждый элемент массива отводится отдельная строка в консоли). С помощью выражения "" + $arrBEMTypes производится вывод элементов массива горизонтально, то есть элементы массива выводятся в одной строке, разделяемые символом пробела. В принципе, вывод массива $arrBEMTypes не обязателен, но я решил его выводить, потому что так нагляднее можно видеть, что собою представляет совокупность БЭМ-сущностей на текущем узле HTML-дерева. Вот как при этом может выглядеть фрагмент вывода в консоль: На иллюстрации выше на HTML-элементе div расположена совокупность двух БЭМ-сущностей: блока и булевого модификатора этого блока. Такая совокупность БЭМ-сущностей представлена массивом чисел 1 2. Этот массив виден на иллюстрации в самом низу. Всё вышеизложенное — это пока что были не сами проверки правильности совмещения имен БЭМ-сущностей на одном узле, а лишь подготовка к ним. Приступим к проверкам. Что можно проверить по типам БЭМ-сущностей, расположенных на одном узле HTML-дерева? Например, если следовать логике методологии БЭМ, то можно заметить, что в каждой совокупности таких БЭМ-сущностей должна быть хотя бы одна БЭМ-сущность типа «блок» или «элемент», в числах у нас это число 1 или число 4. Пишем код проверки, который можно добавить в код выше: С помощью оператора -notin (фраза «not in» в данном случае по-русски означает «не входит в») проверяем вхождение чисел 1 и 4 в наш массив типов БЭМ-сущностей. Если этих чисел в анализируемом массиве нет, выдаем в консоль сообщение об ошибке. Обычно люди пишут название блока или элемента первым в совокупности названий БЭМ-сущностей, расположенных на одном узле. Это логично. Однако, по идее, никто не запрещает размещать имена БЭМ-сущностей в значении атрибута «class» HTML-элемента в любом порядке (про это у меня была отдельная статья). При этом название блока или элемента могут оказаться не первыми в совокупности БЭМ-сущностей, расположенных на одном узле HTML-дерева. Именно поэтому я использую оператор -notin, а не проверяю сразу первый элемент массива. После того, как пройдена первая проверка (в совокупности БЭМ-сущностей есть имя блока или имя элемента), я проверяю первый элемент массива типов БЭМ-сущностей. Если имя блока или имя элемента стоит не первым в совокупности БЭМ-сущностей, выдаем в консоль сообщение с рекомендацией поставить имя блока или элемента первым. Это не ошибка, а предупреждение. В коде эта разница выражается в том, что при ошибке дальнейшие проверки не проводятся (для ошибки выделена отдельная ветка), а при предупреждении (рекомендации) проводятся дальнейшие проверки. Таким образом, сообщение об ошибке может быть только одно для одного узла HTML-дерева, а предупреждений может быть выведено несколько. Таков принцип, реализованный в этом коде. Для дальнейших, более сложных, проверок кроме типов БЭМ-сущностей нужна дополнительная информация о БЭМ-сущностях. Конкретно для проверок понадобятся части названий БЭМ-сущностей, отделенные друг от друга разделителями. Можно было при распознавании помещать эти части в отдельные массивы, создав несколько нужных массивов так же, как было сделано выше с массивом типов БЭМ-сущностей. Однако, в данном случае для представления каждой БЭМ-сущности напрашивается использование объекта, как это принято в объектно-ориентированном программировании. Таким образом, для всех нужных целей мы создадим массив объектов $arrBEMEntities, который заменит массив типов $arrBEMTypes. Меняем (дополняем) код на языке PowerShell в схеме из начала статьи: В этом коде опять показаны только две первые ветки распознавания типа БЭМ-сущности, но принцип, думаю, тоже должен быть понятен. Инициализация массива объектов $arrBEMEntities производится точно так же, как и инициализация массива чисел $arrBEMTypes ранее в статье. В языке PowerShell объект пользовательского класса можно создать примерно по такой схеме (это один из нескольких возможных способов): На самом деле тут две операции: создание хеш-таблицы (ассоциативного массива, то есть массива, позволяющего хранить пары ключ-значение) с помощью выражения @{} и приведение полученной хеш-таблицы к типу PSCustomObject (точнее, это псевдоним настоящего типа; тут подробнее). Для выделения частей названия БЭМ-сущности я добавил в первоначальные регулярные выражения круглые скобки вокруг выражений [a-z-]+, значение которых разбиралось в предыдущей статье. Такие круглые скобки в регулярных выражениях называют «группирующими конструкциями» или «захватывающими группами» (или «группами захвата», как в спецназе). Захваченные круглыми скобками подвыражения можно получить с помощью встроенной в язык PowerShell переменной (автоматической переменной) $Matches (см. как это сделано в коде выше). Первый захваченный кусочек строки можно получить с помощью выражения $Matches.1, второй — с помощью выражения $Matches.2 и так далее. С помощью выражения $Matches.0 можно получить исходную строку всю целиком. Состав полей (свойств) наших объектов, представляющих БЭМ-сущности, может быть в шести комбинациях: Поле Raw содержит название БЭМ-сущности целиком, не порубленное на куски. Поэтому я назвал его словом «raw» (по-русски «сырец»). Чтобы воспользоваться уже написанными выше для массива чисел $arrBEMTypes простыми проверками, я вытаскиваю этот массив из массива объектов $arrBEMEntities. Это можно сделать потому, что свойство Type объекта, представляющего БЭМ-сущность, — это и есть массив $arrBEMTypes, который мы использовали ранее. Для его создания нужно получить значение свойства Type каждого объекта из массива объектов $arrBEMEntities. В языке PowerShell это легко сделать с помощью следующего выражения (оно же используется в коде выше): Однако, здесь есть подводный камень: такая операция возвращает массив, если количество объектов больше одного. Если в массиве $arrBEMEntities содержится только один объект, выражение $arrBEMEntities.Type возвратит не массив с одним элементом, а сам этот элемент, в данном случае — число. В нашем случае это не ломает код простых проверок, показанных выше, но в других случаях это может повлиять на работу программы. (Без строгого назначения типа переменным в программах часто могут возникать такие вещи. Поэтому при написании больших программ обычно предпочитают строгую типизацию.) Сначала реализуем еще одно предупреждение, которое будет выдаваться в случае нахождения в значении атрибута «class» HTML-элемента слов-дубликатов. Точно так же, как ранее, это предупреждение (поскольку это не ошибка) не будет прерывать последующие проверки. Пишем код на языке PowerShell (я даю некоторый контекст, чтобы было понятно, куда вставляется этот код в код, приведенный выше; место вставки имеет значение): Поле (свойство) Raw объекта, представляющего БЭМ-сущность, я создал специально для поиска дубликатов в значении атрибута «class» HTML-элемента. Для такого поиска удобны имена БЭМ-сущностей целиком, не порубленные на куски. Получаем массив $arrBEMRaws имен БЭМ-сущностей из массива объектов $arrBEMEntities тем же способом, что и массив типов БЭМ-сущностей $arrBEMTypes. Тут следует учесть тонкость, которая была объяснена ранее: если в массиве объектов $arrBEMEntities будет только один объект (на узле размещена только одна БЭМ-сущность), то в переменную $arrBEMRaws будет записан не массив, а значение свойства Raw объекта, то есть в нашем случае — строка. В принципе, дубликаты возможны, только если на узле размещено больше одной БЭМ-сущности. Поэтому мы отфильтровываем случаи, когда на узле больше одной БЭМ-сущности, выражением if ($arrBEMRaws.GetType().Name -eq "Object[]"). После этого можно быть уверенным, что в переменной $arrBEMRaws точно содержится массив и в этом массиве больше одного элемента. Для сравнения всех элементов массива друг с другом используется известный алгоритм с использованием двух циклов. При нахождении дубликатов проверка прекращается и выдается предупреждение о наличии дубликатов. Для прерывания работы циклов используется инструкция break. Чтобы выйти из обоих циклов сразу и продолжить работу с инструкции после внешнего цикла, используем метку loop. Этой меткой отмечен внешний цикл, из которого в итоге планируется выйти. (По умолчанию, без использования метки, инструкция break прерывает только текущий цикл, а не оба сразу.) Пример совокупности БЭМ-сущностей на узле, который вызовет это предупреждение (фрагмент из возможных результатов работы скрипта): На иллюстрации выше на узле HTML-элемента p (параграф) размещено два булевых модификатора блока с одинаковыми названиями, что вызывает вывод предупреждения. Согласно методологии БЭМ модификатор не может использоваться в отрыве от модифицируемого блока или элемента. Пишем код проверки на языке PowerShell (этот код вставляется в код выше после простых проверок; при вставке можно ориентироваться на текст комментариев и отступы): С помощью метода Where массивов мы фильтруем исходный массив, получая на выходе коллекцию элементов исходного массива, удовлетворяющих указанным в параметрах метода Where условиям. Первым параметром этого метода является блок кода. Обычно параметры метода указываются в круглых скобках, но если нужно указать только первый параметр, то круглые скобки можно опустить, как я сделал в нашем случае. Следует иметь в виду, что метод Where возвращает не массив, как можно было бы подумать (я так думал), а именно коллекцию элементов. У массива и коллекции элементов в данном случае разные типы: Object[] и Collection`1. Насколько я понимаю, под Collection`1 подразумевается класс Collection<T> платформы «.NET». Это так называемая «обобщенная коллекция» («обобщенная» по-английски «generic»), то есть коллекция, которая может включать элементы разных типов. Поэтому нужно понимать, что у массива и коллекции в данном случае разные свойства и методы. Например, у коллекции нет метода Length, но есть метод Count для определения количества элементов (у массива есть оба этих метода). В коллекцию можно легко добавлять элементы, также как и удалять, с помощью соответствующих методов Add и Remove, в отличие от массива (о массиве в этом плане было рассказано выше; там добавление и удаление элементов реализуется с помощью создания нового массива). Чтобы не путаться, массивам я добавил в название приставку arr, а коллекциям — приставку coll. В условиях фильтрации метода Where используется встроенная (автоматическая) переменная $_. Она представляет очередной элемент массива при фильтрации. Для реализации проверки мы создаем из массива объектов БЭМ-сущностей на узле две коллекции: коллекцию блоков $collBEMBlocks и коллекцию модификаторов блока $collBEMBlockMods. Фильтрация происходит по свойству Type (тип БЭМ-сущности, число от 1 до 6; это уже разбиралось выше) каждого объекта исходного массива БЭМ-сущностей. После этого в двойном цикле мы проверяем, есть ли на узле для каждого модификатора соответствующий блок. Эта проверка выполняется по части BlockName названия блока и названия модификатора блока. Если для какого-нибудь модификатора блока на узле не найден соответствующий блок, выдаем сообщение об ошибке в консоль. Проверка прерывается. Проверка для модификаторов элементов выполняется примерно так же, как и для модификаторов блока. Только для модификаторов элемента в двойном цикле выполняется проверка двух частей названия — BlockName и ElemName. Примеры совокупностей БЭМ-сущностей на узле, которые вызовут эту ошибку (фрагмент из возможных результатов работы скрипта): Согласно методологии БЭМ невозможно одновременно использовать несколько одинаковых модификаторов типа ключ-значение с разными значениями на одном узле. Пишем код проверки на языке PowerShell (в скрипте эту проверку можно разместить сразу после проверки из предыдущего раздела данной статьи): Смысл кода выше должен уже быть примерно понятен, исходя из объяснений в предыдущих разделах статьи. Выбираем из исходного массива БЭМ-сущностей, расположенных на одном узле HTML-дерева коллекцию БЭМ-сущностей с типом 3 (модификатор блока ключ-значение) или с типом 6 (модификатор элемента ключ-значение). В двойном цикле сравниваем все модификаторы, оказавшиеся в отфильтрованной коллекции, друг с другом по соответствующим частям названий БЭМ-сущностей. Для модификатора блока ключ-значение: если совпадают части названия модификатора BlockName и ModName, но не совпадают части ModValue, то выдаем сообщение об ошибке в консоль и прерываем двойной цикл (выход по метке loop позволяет выйти из циклов и продолжить работу скрипта с инструкции, идущей после внешнего цикла). Для модификатора элемента ключ-значение делаем проверку так же, как и для модификатора блока ключ-значение, только при проверке частей проверяем еще часть ElemName. Примеры совокупностей БЭМ-сущностей на узле, которые вызовут эту ошибку (фрагмент из возможных результатов работы скрипта): Вероятно, есть еще какие-то проверки, которые можно реализовать для названий БЭМ-сущностей на одном узле HTML-дерева. Всё вышеизложенное можно дополнять и улучшать, меняя код. В этой статье я лишь хотел поделиться принципами проверок названий БЭМ-сущностей по методологии БЭМ так, как я их понимаю. Возможно, где-то понимаю не совсем правильно, тогда прошу поделиться замечанием в комментариях к данной статье. За скобками пока остались проверки названий БЭМ-сущностей, расположенных на разных узлах HTML-дерева. Например, проверка принципа принадлежности при работе с элементами: элемент — всегда часть блока и не должен использоваться отдельно от него (вне его контекста).