Я бы сделал это с помощью регулярных выражений.
Сначала преобразуйте селектор в регулярное выражение, соответствующее простому списку открывающихся тегов сверху вниз, представляющих данное состояние стека синтаксического анализатора. Чтобы объяснить, вот некоторые простые селекторы и соответствующие им регулярные выражения:
A
становится /<A[^>]*>$/
A#someid
становится /<A[^>]*id="someid"[^>]*>$/
A.someclass
становится /<A[^>]*class="[^"]*(?<= |")someclass(?= |")[^"]*"[^>]*>$/
A > B
становится /<A[^>]*><B[^>]*>$/
A B
становится /<A[^>]*>(?:<[^>]*>)*<B[^>]*>$/
И так далее. Обратите внимание, что все регулярные выражения заканчиваются на $, но не начинаются на ^; это соответствует тому, как селекторы CSS не должны совпадать с корнем документа. Также обратите внимание на то, что в коде сопоставления классов есть некоторые вещи типа lookbehind и lookahead, которые необходимы, чтобы вы случайно не сравнивали с «someclass-super-duper», когда вам нужен совершенно отличный класс «someclass».
Если вам нужно больше примеров, пожалуйста, дайте мне знать.
Как только вы построите регулярное выражение селектора, вы готовы начать синтаксический анализ. При разборе сохраняйте стопку тегов, которые в настоящее время применяются; обновляйте этот стек каждый раз, когда вы спускаетесь или поднимаетесь. Чтобы проверить соответствие селектора, преобразуйте этот стек в список тегов, которые могут соответствовать регулярному выражению. Например, рассмотрим этот документ:
<x><a>Stuff goes here</a><y id="boo"><z class="bar">Content here</z></y></x>
Ваша строка состояния стека будет проходить через следующие значения по порядку при вводе каждого элемента:
<x>
<x><a>
<x><y id="boo">
<x><y id="boo"><z class="bar">
Процесс сопоставления прост: всякий раз, когда анализатор опускается в новый элемент, обновляйте строку состояния и проверяйте, соответствует ли она регулярному выражению селектора. Если регулярное выражение совпадает, то селектор соответствует этому элементу!
Проблемы, на которые нужно обратить внимание:
Двойные кавычки внутри атрибутов. Чтобы обойти это, примените кодировку сущности html к значениям атрибутов при создании регулярного выражения и к значениям атрибутов при создании строки состояния стека.
Порядок атрибутов. При построении как регулярного выражения, так и строки состояния, используйте некоторый канонический порядок для атрибутов (алфавитный самый простой). В противном случае вы можете обнаружить, что ваше регулярное выражение для селектора a#someid.someclass
, который ожидает, что <a id="someid" class="someclass">
, к сожалению, завершится неудачно, когда ваш синтаксический анализатор перейдет в <a class="someclass" id="someid">
.
Чувствительность к регистру. В соответствии с HTML spec атрибуты class и id чувствительны к регистру (обратите внимание на маркер 'CS' в соответствующих разделах). Таким образом, вы должны использовать регистрозависимое сопоставление регулярных выражений. Однако в HTML имена элементов не чувствительны к регистру, хотя они представлены в XML. Если требуется сопоставление имен элементов без учета регистра в HTML-формате, канонизируйте имена элементов либо в верхнем, либо в нижнем регистре как в регулярном выражении селектора, так и в строке стека состояний.
Дополнительная магия необходима для работы с шаблонами селектора, которые включают в себя наличие или отсутствие родственных элементов, а именно A:first-child
и A + B
. Вы можете сделать это, добавив специальный атрибут к тегу, содержащий имя тега непосредственно перед, или "", если этот тег является первым дочерним элементом. Также есть общий селектор брата, A ~ B
; Я не совсем уверен, как с этим справиться.
EDIT : Если вам не нравится хакерство регулярных выражений, вы все равно можете использовать этот подход для решения проблемы, используя только свой собственный конечный автомат вместо механизма регулярных выражений. В частности, CSS-селектор может быть реализован как недетерминированный конечный автомат , который является пугающим звучащим термином, но на практике означает следующее:
- Может быть более одного возможного перехода из любого данного состояния
- Машина пробует один из них, и если это не сработает, то возвращается и пробует другой
- Самый простой способ реализовать это - сохранить стек для машины, на который вы нажимаете всякий раз, когда вы следуете по пути, и извлекаете его, когда вам нужно вернуться назад. Все сводится к тому, что вы использовали для поиска в глубину.
Секрет почти всех удивительных регулярных выражений заключается в использовании этого стиля конечного автомата.