класс HTML::Selector
Выбирает элементы HTML с помощью селекторов CSS 2.
Класс Selector использует выражения селекторов CSS для сопоставления и выбора элементов HTML.
Например:
selector = HTML::Selector.new "form.login[action=/login]"
создает новый селектор, который соответствует любому элементу form с классом login и атрибутом action со значением /login.
Сопоставление элементов
Используйте метод match, чтобы определить, соответствует ли элемент селектору.
Для простых селекторов метод возвращает массив с этим элементом или nil если элемент не соответствует. Для сложных селекторов (см. ниже) метод возвращает массив со всеми соответствующими элементами или nil если совпадений не найдено.
Например:
if selector.match(element) puts "Element is a login form" end
Выбор элементов
Используйте метод select для выбора всех соответствующих элементов, начиная с одного элемента и проходя через всех потомков в порядке обхода в глубину.
Этот метод возвращает массив всех соответствующих элементов, пустой массив, если совпадений не найдено.
Например:
selector = HTML::Selector.new "input[type=text]"
matches = selector.select(element)
matches.each do |match|
puts "Found text field with name #{match.attributes['name']}"
end
Выражения
Селекторы могут сопоставлять элементы по следующим критериям:
-
name– Сопоставление элемента по его имени (имени тега). Например,pдля сопоставления абзаца. Вы можете использовать*для сопоставления любого элемента. -
#id– Сопоставление элемента по его идентификатору (атрибутid). Например,#page. -
.class– Сопоставление элемента по имени класса, всех имён классов, если указано более одного. -
[attr]– Сопоставление элемента, имеющего указанный атрибут. -
[attr=value]– Сопоставление элемента, имеющего указанный атрибут и значение. (Поддерживаются дополнительные операторы, см. ниже). -
:pseudo-class– Сопоставление элемента на основе псевдокласса, такого как:nth-childи:empty. -
:not(expr)– Сопоставление элемента, не соответствующего выражению отрицания.
При использовании комбинации вышеперечисленного, имя элемента ставится первым, за ним идентификатор, имена классов, атрибуты, псевдоклассы и отрицания в любом порядке. Не разделяйте эти части пробелами! Пробелы используются для селекторов потомков.
Например:
selector = HTML::Selector.new "form.login[action=/login]"
Сопоставленный элемент должен быть типа form и иметь класс login. Он может иметь другие классы, но класс login необходим для соответствия. Он также должен иметь атрибут под названием action со значением /login.
Этот селектор будет соответствовать следующему элементу:
<form class="login form" method="post" action="/login">
но не будет соответствовать элементу:
<form method="post" action="/logout">
Значения атрибутов
Поддерживаются несколько операторов для сопоставления атрибутов:
-
name– Элемент должен иметь атрибут с указанным именем. -
name=value– Элемент должен иметь атрибут с указанным именем и значением. -
name^=value– Значение атрибута должно начинаться с указанного значения. -
name$=value– Значение атрибута должно заканчиваться указанным значением. -
name*=value– Значение атрибута должно содержать указанное значение. -
name~=word– Значение атрибута должно содержать указанное слово (разделенные пробелом). -
name|=word– Значение атрибута должно начинаться с указанного слова.
Например, следующие два селектора соответствуют одному и тому же элементу:
#my_id [id=my_id]
и следующие два селектора:
.my_class [class~=my_class]
Альтернативы, соседи, потомки
Сложные селекторы используют комбинацию выражений для сопоставления элементов:
-
expr1 expr2– Сопоставление любого элемента со вторым выражением, если он имеет родительский элемент, который соответствует первому выражению. -
expr1 > expr2– Сопоставление любого элемента со вторым выражением, если он является потомком элемента, соответствующего первому выражению. -
expr1 + expr2– Сопоставление любого элемента со вторым выражением, если он непосредственно следует за элементом, соответствующим первому выражению. -
expr1 ~ expr2– Сопоставление любого элемента со вторым выражением, если он следует за элементом, соответствующим первому выражению. -
expr1, expr2– Сопоставление любого элемента с первым выражением или со вторым выражением.
Поскольку селекторы потомков и соседей могут соответствовать более чем одному элементу, исходя из первого элемента, метод match может вернуть более одного совпадения.
Псевдоклассы
Псевдоклассы были введены в CSS 3. Они чаще всего используются для выбора элементов в заданном положении:
-
:root– Сопоставление элемента только в том случае, если он является корневым элементом (нет родительского элемента). -
:empty– Сопоставление элемента только в том случае, если он не имеет дочерних элементов и не содержит текстового контента. -
:content(string)– Сопоставление элемента только в том случае, если он содержитstringв качестве своего текстового содержимого (не учитывая начальные и конечные пробелы). -
:only-child– Сопоставление элемента, если он является единственным потомком (элементом) родительского элемента. -
:only-of-type– Сопоставление элемента, если он является единственным потомком (элементом) родительского элемента своего типа. -
:first-child– Сопоставление элемента, если он является первым потомком (элементом) родительского элемента. -
:first-of-type– Сопоставление элемента, если он является первым потомком (элементом) родительского элемента своего типа. -
:last-child– Сопоставление элемента, если он является последним потомком (элементом) родительского элемента. -
:last-of-type– Сопоставление элемента, если он является последним потомком (элементом) родительского элемента своего типа. -
:nth-child(b)– Сопоставление элемента, если он является b-м потомком (элементом) родительского элемента. Значениеbуказывает его индекс, начиная с 1. -
:nth-child(an+b)– Сопоставление элемента, если он является b-м потомком (элементом) в каждой группе изaпотомков элемента родителя. -
:nth-child(-an+b)– Сопоставление элемента, если он является первым потомком (элементом) в каждой группе изaпотомков родительского элемента, до первогоbпотомков родительского элемента. -
:nth-child(odd)– Сопоставление элемента с нечётной позицией (т.е. первой, третьей). То же, что и:nth-child(2n+1). -
:nth-child(even)– Сопоставление элемента с чётной позицией (т.е. второй, четвёртой). То же, что и:nth-child(2n+2). -
:nth-of-type(..)– Как выше, но учитывает только элементы его типа. -
:nth-last-child(..)– Как выше, но считает с последнего потомка. -
:nth-last-of-type(..)– Как выше, но считает с последнего потомка и только элементы его типа. -
:not(selector)– Сопоставление элемента только в том случае, если элемент не соответствует простому селектору.
Как видите, :nth-child псевдокласс и его варианты могут быть довольно сложными, и спецификация CSS не делает лучшую работу по его объяснению. Но после прочтения примеров и нескольких попыток комбинаций, это легко понять.
Например:
table tr:nth-child(odd)
Выбирает каждую вторую строку в таблице, начиная с первой.
div p:nth-child(4)
Выбирает четвёртый абзац в div, но не если div содержит другие элементы, так как они также считаются.
div p:nth-of-type(4)
Выбирает четвёртый абзац в div, считая только абзацы и игнорируя все остальные элементы.
div p:nth-of-type(-n+4)
Выбирает первые четыре абзаца, игнорируя все остальные.
И вы всегда можете выбрать элемент, соответствующий одному набору правил, но не другому, используя :not. Например:
p:not(.post)
Сопоставляет все абзацы, которые не имеют класса .post.
Замещающие значения
Вы можете использовать замену с идентификаторами, именами классов и значениями элементов. Замена имеет вид знака вопроса (?) и использует следующее значение в списке аргументов после выражения CSS.
Значение замены может быть строкой или регулярным выражением. Все остальные значения преобразуются в строки.
Например:
selector = HTML::Selector.new "#?", /^\d+$/
соответствует любому элементу, чьей идентификатор состоит из одной или нескольких цифр.
Методы публичного класса
Создаёт новый селектор для заданного имени класса.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 216 def for_class(cls) self.new([".?", cls]) end
Создаёт новый селектор для заданного идентификатора.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 225 def for_id(id) self.new(["#?", id]) end
Создаёт новый селектор из выражения CSS 2 селектора.
Первый аргумент — выражение селектора. Все последующие аргументы используются для подстановки значений.
Выбрасывает InvalidSelectorError, если выражение селектора некорректно.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 241
def initialize(selector, *values)
raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
@source = ""
values = values[0] if values.size == 1 && values[0].is_a?(Array)
# We need a copy to determine if we failed to parse, and also
# preserve the original pass by-ref statement.
statement = selector.strip.dup
# Create a simple selector, along with negation.
simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
@alternates = []
@depends = nil
# Alternative selector.
if statement.sub!(/^\s*,\s*/, "")
second = Selector.new(statement, values)
@alternates << second
# If there are alternate selectors, we group them in the top selector.
if alternates = second.instance_variable_get(:@alternates)
second.instance_variable_set(:@alternates, [])
@alternates.concat alternates
end
@source << " , " << second.to_s
# Sibling selector: create a dependency into second selector that will
# match element immediately following this one.
elsif statement.sub!(/^\s*\+\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
if element = next_element(element)
second.match(element, first)
end
end
@source << " + " << second.to_s
# Adjacent selector: create a dependency into second selector that will
# match all elements following this one.
elsif statement.sub!(/^\s*~\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
while element = next_element(element)
if subset = second.match(element, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
end
end
matches.empty? ? nil : matches
end
@source << " ~ " << second.to_s
# Child selector: create a dependency into second selector that will
# match a child element of this one.
elsif statement.sub!(/^\s*>\s*/, "")
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
element.children.each do |child|
if child.tag? && subset = second.match(child, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
end
end
matches.empty? ? nil : matches
end
@source << " > " << second.to_s
# Descendant selector: create a dependency into second selector that
# will match all descendant elements of this one. Note,
elsif statement =~ /^\s+\S+/ && statement != selector
second = next_selector(statement, values)
@depends = lambda do |element, first|
matches = []
stack = element.children.reverse
while node = stack.pop
next unless node.tag?
if subset = second.match(node, first)
if first && !subset.empty?
matches << subset.first
break
else
matches.concat subset
end
elsif children = node.children
stack.concat children.reverse
end
end
matches.empty? ? nil : matches
end
@source << " " << second.to_s
else
# The last selector is where we check that we parsed
# all the parts.
unless statement.empty? || statement.strip.empty?
raise ArgumentError, "Invalid selector: #{statement}"
end
end
end Методы публичного экземпляра
Сопоставляет элемент с селектором.
Для простого селектора этот метод возвращает массив с элементом, если он соответствует, и nil в противном случае.
Для сложного селектора (свойствами "sibling" и "descendant") этот метод возвращает массив со всеми соответствующими элементами, а nil, если совпадений не найдено.
Используйте +first_only=true+, если вам нужен только первый элемент.
Например:
if selector.match(element) puts "Element is a login form" end
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 365
def match(element, first_only = false)
# Match element if no element name or element name same as element name
if matched = (!@tag_name || @tag_name == element.name)
# No match if one of the attribute matches failed
for attr in @attributes
if element.attributes[attr[0]] !~ attr[1]
matched = false
break
end
end
end
# Pseudo class matches (nth-child, empty, etc).
if matched
for pseudo in @pseudo
unless pseudo.call(element)
matched = false
break
end
end
end
# Negation. Same rules as above, but we fail if a match is made.
if matched && @negation
for negation in @negation
if negation[:tag_name] == element.name
matched = false
else
for attr in negation[:attributes]
if element.attributes[attr[0]] =~ attr[1]
matched = false
break
end
end
end
if matched
for pseudo in negation[:pseudo]
if pseudo.call(element)
matched = false
break
end
end
end
break unless matched
end
end
# If element matched but depends on another element (child,
# sibling, etc), apply the dependent matches instead.
if matched && @depends
matches = @depends.call(element, first_only)
else
matches = matched ? [element] : nil
end
# If this selector is part of the group, try all the alternative
# selectors (unless first_only).
if !first_only || !matches
@alternates.each do |alternate|
break if matches && first_only
if subset = alternate.match(element, first_only)
if matches
matches.concat subset
else
matches = subset
end
end
end
end
matches
end Возвращает следующий элемент после текущего. Пропускает текстовые узлы-сиблинги.
С аргументом name, возвращает следующий элемент с этим именем, пропуская другие элементы-сиблинги.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 495
def next_element(element, name = nil)
if siblings = element.parent.children
found = false
siblings.each do |node|
if node.equal?(element)
found = true
elsif found && node.tag?
return node if (name.nil? || node.name == name)
end
end
end
nil
end Выбирает и возвращает массив со всеми совпадающими элементами, начиная с одного узла и переходя через всех детей по принципу "depth-first". Возвращает пустой массив, если совпадений не найдено.
Узел root может быть любым элементом документа или самим документом.
Например:
selector = HTML::Selector.new "input[type=text]"
matches = selector.select(element)
matches.each do |match|
puts "Found text field with name #{match.attributes['name']}"
end
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 455
def select(root)
matches = []
stack = [root]
while node = stack.pop
if node.tag? && subset = match(node, false)
subset.each do |match|
matches << match unless matches.any? { |item| item.equal?(match) }
end
elsif children = node.children
stack.concat children.reverse
end
end
matches
end Аналогично select, но возвращает первый совпадающий элемент. Возвращает nil если ни один элемент не соответствует селектору.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 473
def select_first(root)
stack = [root]
while node = stack.pop
if node.tag? && subset = match(node, true)
return subset.first if !subset.empty?
elsif children = node.children
stack.concat children.reverse
end
end
nil
end Защищённые методы экземпляра
Создаёт регулярное выражение для сопоставления значения атрибута, основываясь на операторе равенства (=, ^=, |=, и т.д.).
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 689
def attribute_match(equality, value)
regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
case equality
when "=" then
# Match the attribute value in full
Regexp.new("^#{regexp}$")
when "~=" then
# Match a space-separated word within the attribute value
Regexp.new("(^|\s)#{regexp}($|\s)")
when "^="
# Match the beginning of the attribute value
Regexp.new("^#{regexp}")
when "$="
# Match the end of the attribute value
Regexp.new("#{regexp}$")
when "*="
# Match substring of the attribute value
regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
when "|=" then
# Match the first space-separated item of the attribute value
Regexp.new("^#{regexp}($|\s)")
else
raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
# Match all attributes values (existence check)
//
end
end Вызывается для создания зависимого селектора (sibling, descendant и т.д.). Передаёт остаток от выражения, который в итоге будет сведён к нулю, и массив значений для подстановки.
Этот метод вызывается в четырёх местах, поэтому полезно его расположить здесь для повторного использования. Единственная логика связана с необходимостью распознавания разделителей запятыми (альтернативные) и их применением к группе селекторов верхнего селектора.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 802
def next_selector(statement, values)
second = Selector.new(statement, values)
# If there are alternate selectors, we group them in the top selector.
if alternates = second.instance_variable_get(:@alternates)
second.instance_variable_set(:@alternates, [])
@alternates.concat alternates
end
second
end Возвращает лямбда-функцию, которая может сопоставлять элемент с псевдоклассом nth-child, учитывая следующие аргументы:
-
a– Значение части а. -
b– Значение части b. -
of_type– Истина, если проверять только элементы данного типа (of-type). -
reverse– Истина, если считать в обратном порядке (last-).
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 724
def nth_child(a, b, of_type, reverse)
# a = 0 means select at index b, if b = 0 nothing selected
return lambda { |element| false } if a == 0 && b == 0
# a < 0 and b < 0 will never match against an index
return lambda { |element| false } if a < 0 && b < 0
b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
lambda do |element|
# Element must be inside parent element.
return false unless element.parent && element.parent.tag?
index = 0
# Get siblings, reverse if counting from last.
siblings = element.parent.children
siblings = siblings.reverse if reverse
# Match element name if of-type, otherwise ignore name.
name = of_type ? element.name : nil
found = false
for child in siblings
# Skip text nodes/comments.
if child.tag? && (name == nil || child.name == name)
if a == 0
# Shortcut when a == 0 no need to go past count
if index == b
found = child.equal?(element)
break
end
elsif a < 0
# Only look for first b elements
break if index > b
if child.equal?(element)
found = (index % a) == 0
break
end
else
# Otherwise, break if child found and count == an+b
if child.equal?(element)
found = (index % a) == b
break
end
end
index += 1
end
end
found
end
end Создаёт лямбда-функцию для проверки единственного ребёнка. Передайте of-type чтобы рассматривать только элементы данного типа.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 774
def only_child(of_type)
lambda do |element|
# Element must be inside parent element.
return false unless element.parent && element.parent.tag?
name = of_type ? element.name : nil
other = false
for child in element.parent.children
# Skip text nodes/comments.
if child.tag? && (name == nil || child.name == name)
unless child.equal?(element)
other = true
break
end
end
end
!other
end
end Создаёт простой селектор, используя выражение и массив значений для подстановки.
Возвращает хэш со значениями tag_name, attributes, pseudo (классы) и negation.
Вызывается первый раз со значением can_negate true, чтобы разрешить отрицание. Вызывается второй раз со значением false, так как отрицание не может быть отрицаемым.
# File actionview/lib/action_view/vendor/html-scanner/html/selector.rb, line 522
def simple_selector(statement, values, can_negate = true)
tag_name = nil
attributes = []
pseudo = []
negation = []
# Element name. (Note that in negation, this can come at
# any order, but for simplicity we allow if only first).
statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
match.strip!
tag_name = match.downcase unless match == "*"
@source << match
"" # Remove
end
# Get identifier, class, attribute name, pseudo or negation.
while true
# Element identifier.
next if statement.sub!(/^#(\?|[\w\-]+)/) do
id = $1
if id == "?"
id = values.shift
end
@source << "##{id}"
id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
attributes << ["id", id]
"" # Remove
end
# Class name.
next if statement.sub!(/^\.([\w\-]+)/) do
class_name = $1
@source << ".#{class_name}"
class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
attributes << ["class", class_name]
"" # Remove
end
# Attribute value.
next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do
name, equality, value = $1, $2, $3
if value == "?"
value = values.shift
else
# Handle single and double quotes.
value.strip!
if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
value = value[1..-2]
end
end
@source << "[#{name}#{equality}'#{value}']"
attributes << [name.downcase.strip, attribute_match(equality, value)]
"" # Remove
end
# Root element only.
next if statement.sub!(/^:root/) do
pseudo << lambda do |element|
element.parent.nil? || !element.parent.tag?
end
@source << ":root"
"" # Remove
end
# Nth-child including last and of-type.
next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
reverse = $1 == "last-"
of_type = $2 == "of-type"
@source << ":nth-#{$1}#{$2}("
case $3
when "odd"
pseudo << nth_child(2, 1, of_type, reverse)
@source << "odd)"
when "even"
pseudo << nth_child(2, 2, of_type, reverse)
@source << "even)"
when /^(\d+|\?)$/ # b only
b = ($1 == "?" ? values.shift : $1).to_i
pseudo << nth_child(0, b, of_type, reverse)
@source << "#{b})"
when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
a = ($1 == "?" ? values.shift :
$1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
b = ($2 == "?" ? values.shift : $2).to_i
pseudo << nth_child(a, b, of_type, reverse)
@source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
else
raise ArgumentError, "Invalid nth-child #{match}"
end
"" # Remove
end
# First/last child (of type).
next if statement.sub!(/^:(first|last)-(child|of-type)/) do
reverse = $1 == "last"
of_type = $2 == "of-type"
pseudo << nth_child(0, 1, of_type, reverse)
@source << ":#{$1}-#{$2}"
"" # Remove
end
# Only child (of type).
next if statement.sub!(/^:only-(child|of-type)/) do
of_type = $1 == "of-type"
pseudo << only_child(of_type)
@source << ":only-#{$1}"
"" # Remove
end
# Empty: no child elements or meaningful content (whitespaces
# are ignored).
next if statement.sub!(/^:empty/) do
pseudo << lambda do |element|
empty = true
for child in element.children
if child.tag? || !child.content.strip.empty?
empty = false
break
end
end
empty
end
@source << ":empty"
"" # Remove
end
# Content: match the text content of the element, stripping
# leading and trailing spaces.
next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do
content = $1
if content == "?"
content = values.shift
elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
content = content[1..-2]
end
@source << ":content('#{content}')"
content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
pseudo << lambda do |element|
text = ""
for child in element.children
unless child.tag?
text << child.content
end
end
text.strip =~ content
end
"" # Remove
end
# Negation. Create another simple selector to handle it.
if statement.sub!(/^:not\(\s*/, "")
raise ArgumentError, "Double negatives are not missing feature" unless can_negate
@source << ":not("
negation << simple_selector(statement, values, false)
raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
@source << ")"
next
end
# No match: moving on.
break
end
# Return hash. The keys are mapped to instance variables.
{:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
end
© 2004–2016 David Heinemeier Hansson
Licensed under the MIT License.