Skip to content

Commit 179d74d

Browse files
committed
refactor: move namespaces into the XPathVisitor
Previously this was passed into CSS::Parser.new and used during the parsing phase. Now it's passed into the visitor and we do namespace munging there, where it should be. Note the namespaces has also been moved out of the top-level parser cache key into XPathVisitor#config. Finally, note that namespaces is nil by default (instead of being an empty Hash) which should prevent some unnecessary object creation in the most common use cases.
1 parent a84ad7f commit 179d74d

File tree

8 files changed

+71
-37
lines changed

8 files changed

+71
-37
lines changed

lib/nokogiri/css.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,20 @@ def xpath_for(
8383
selector = selector.to_str
8484
raise(Nokogiri::CSS::SyntaxError, "empty CSS selector") if selector.empty?
8585

86-
raise ArgumentError, "cannot provide both :prefix and :visitor" if prefix && visitor
86+
if visitor
87+
raise ArgumentError, "cannot provide both :prefix and :visitor" if prefix
88+
raise ArgumentError, "cannot provide both :ns and :visitor" if ns
89+
end
90+
91+
visitor ||= begin
92+
visitor_kw = {}
93+
visitor_kw[:prefix] = prefix if prefix
94+
visitor_kw[:namespaces] = ns if ns
8795

88-
ns ||= {}
89-
visitor ||= if prefix
90-
Nokogiri::CSS::XPathVisitor.new(prefix: prefix)
91-
else
92-
Nokogiri::CSS::XPathVisitor.new
96+
Nokogiri::CSS::XPathVisitor.new(**visitor_kw)
9397
end
9498

95-
Parser.new(ns).xpath_for(selector, visitor)
99+
Parser.new.xpath_for(selector, visitor)
96100
end
97101
end
98102
end

lib/nokogiri/css/parser.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22
#
33
# DO NOT MODIFY!!!!
4-
# This file is automatically generated by Racc 1.7.3
4+
# This file is automatically generated by Racc 1.8.0
55
# from Racc grammar file "parser.y".
66
#
77

@@ -470,12 +470,12 @@ def _reduce_23(val, _values, result)
470470
end
471471

472472
def _reduce_24(val, _values, result)
473-
result = Node.new(:ELEMENT_NAME, [[val[0], val[2]].compact.join(':')])
473+
result = Node.new(:ELEMENT_NAME, [val[0], val[2]])
474474
result
475475
end
476476

477477
def _reduce_25(val, _values, result)
478-
name = @namespaces.key?('xmlns') ? "xmlns:#{val[0]}" : val[0]
478+
name = val[0]
479479
result = Node.new(:ELEMENT_NAME, [name])
480480

481481
result

lib/nokogiri/css/parser.y

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ rule
6464
;
6565

6666
namespaced_ident:
67-
namespace '|' IDENT { result = Node.new(:ELEMENT_NAME, [[val[0], val[2]].compact.join(':')]) }
67+
namespace '|' IDENT { result = Node.new(:ELEMENT_NAME, [val[0], val[2]]) }
6868
| IDENT {
69-
name = @namespaces.key?('xmlns') ? "xmlns:#{val[0]}" : val[0]
69+
name = val[0]
7070
result = Node.new(:ELEMENT_NAME, [name])
7171
}
7272
;

lib/nokogiri/css/parser_extras.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,9 @@ def without_cache(&block)
5656
end
5757
end
5858

59-
# Create a new CSS parser with respect to +namespaces+
60-
def initialize(namespaces = {})
59+
def initialize
6160
@tokenizer = Tokenizer.new
62-
@namespaces = namespaces
63-
super()
61+
super
6462
end
6563

6664
def parse(string)
@@ -88,7 +86,7 @@ def on_error(error_token_id, error_value, value_stack)
8886

8987
def cache_key(query, visitor)
9088
if self.class.cache_on?
91-
[query, @namespaces, visitor.config]
89+
[query, visitor.config]
9290
end
9391
end
9492
end

lib/nokogiri/css/xpath_visitor.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ module DoctypeConfig
5353
# The visitor configuration set via the +prefix:+ keyword argument to XPathVisitor.new.
5454
attr_reader :prefix
5555

56+
# The visitor configuration set via the +namespaces:+ keyword argument to XPathVisitor.new.
57+
attr_reader :namespaces
58+
5659
# :call-seq:
5760
# new() → XPathVisitor
5861
# new(builtins:, doctype:) → XPathVisitor
@@ -66,7 +69,8 @@ module DoctypeConfig
6669
def initialize(
6770
builtins: BuiltinsConfig::NEVER,
6871
doctype: DoctypeConfig::XML,
69-
prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX
72+
prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX,
73+
namespaces: nil
7074
)
7175
unless BuiltinsConfig::VALUES.include?(builtins)
7276
raise(ArgumentError, "Invalid values #{builtins.inspect} for builtins: keyword parameter")
@@ -78,6 +82,7 @@ def initialize(
7882
@builtins = builtins
7983
@doctype = doctype
8084
@prefix = prefix
85+
@namespaces = namespaces
8186
end
8287

8388
# :call-seq: config() → Hash
@@ -86,7 +91,7 @@ def initialize(
8691
# a Hash representing the configuration of the XPathVisitor, suitable for use as
8792
# part of the CSS cache key.
8893
def config
89-
{ builtins: @builtins, doctype: @doctype, prefix: @prefix }
94+
{ builtins: @builtins, doctype: @doctype, prefix: @prefix, namespaces: @namespaces }
9095
end
9196

9297
# :stopdoc:
@@ -272,6 +277,14 @@ def visit_element_name(node)
272277
else
273278
"*[local-name()='#{node.value.first}']"
274279
end
280+
elsif node.value.length == 2 # has a namespace prefix
281+
if node.value.first.nil? # namespace prefix is empty
282+
node.value.last
283+
else
284+
node.value.join(":")
285+
end
286+
elsif @namespaces&.key?("xmlns") # apply the default namespace if it's declared
287+
"xmlns:#{node.value.first}"
275288
else
276289
node.value.first
277290
end
@@ -294,10 +307,10 @@ def validate_xpath_function_name(name)
294307
end
295308

296309
def html5_element_name_needs_namespace_handling(node)
297-
# if this is the wildcard selector "*", use it as normal
298-
node.value.first != "*" &&
299-
# if there is already a namespace (i.e., it is a prefixed QName), use it as normal
300-
!node.value.first.include?(":")
310+
# if there is already a namespace (i.e., it is a prefixed QName), use it as normal
311+
node.value.length == 1 &&
312+
# if this is the wildcard selector "*", use it as normal
313+
node.value.first != "*"
301314
end
302315

303316
def nth(node, options = {})

lib/nokogiri/xml/searchable.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,9 @@ def xpath_query_from_css_rule(rule, ns)
248248
builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::OPTIMAL,
249249
doctype: document.xpath_doctype,
250250
prefix: implied_xpath_context,
251+
namespaces: ns,
251252
)
252-
CSS.xpath_for(rule.to_s, ns: ns, visitor: visitor)
253+
CSS.xpath_for(rule.to_s, visitor: visitor)
253254
end.join(" | ")
254255
end
255256

test/css/test_parser_cache.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,19 +134,19 @@ def test_cache_key_on_ns_prefix_and_visitor_config
134134
Nokogiri::CSS.xpath_for("foo", prefix: ".//", ns: { "example" => "http://example.com/" })
135135
Nokogiri::CSS.xpath_for(
136136
"foo",
137-
ns: { "example" => "http://example.com/" },
138137
visitor: Nokogiri::CSS::XPathVisitor.new(
139138
builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS,
140139
prefix: ".//",
140+
namespaces: { "example" => "http://example.com/" },
141141
),
142142
)
143143
Nokogiri::CSS.xpath_for(
144144
"foo",
145-
ns: { "example" => "http://example.com/" },
146145
visitor: Nokogiri::CSS::XPathVisitor.new(
147146
builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS,
148147
doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5,
149148
prefix: ".//",
149+
namespaces: { "example" => "http://example.com/" },
150150
),
151151
)
152152
assert_equal(5, cache.length)

test/css/test_xpath_visitor.rb

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,6 @@
55

66
describe Nokogiri::CSS::XPathVisitor do
77
let(:parser) { Nokogiri::CSS::Parser.new }
8-
9-
let(:parser_with_ns) do
10-
Nokogiri::CSS::Parser.new({
11-
"xmlns" => "http://default.example.com/",
12-
"hoge" => "http://hoge.example.com/",
13-
})
14-
end
15-
168
let(:visitor) { Nokogiri::CSS::XPathVisitor.new }
179

1810
def assert_xpath(expecteds, asts)
@@ -50,15 +42,23 @@ def assert_xpath(expecteds, asts)
5042
Nokogiri::CSS::XPathVisitor.new(doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5).doctype,
5143
)
5244
assert_raises(ArgumentError) { Nokogiri::CSS::XPathVisitor.new(doctype: :not_valid) }
45+
46+
assert_equal({ "foo": "bar" }, Nokogiri::CSS::XPathVisitor.new(namespaces: { "foo": "bar" }).namespaces)
47+
48+
assert_equal("xxx", Nokogiri::CSS::XPathVisitor.new(prefix: "xxx").prefix)
5349
end
5450

5551
it "exposes its configuration" do
5652
expected = {
5753
builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER,
5854
doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML,
5955
prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX,
56+
namespaces: nil,
6057
}
6158
assert_equal(expected, visitor.config)
59+
60+
assert_nil(visitor.namespaces)
61+
assert_equal(Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX, visitor.prefix)
6262
end
6363

6464
it "raises an exception on single quote" do
@@ -137,9 +137,25 @@ def assert_xpath(expecteds, asts)
137137
assert_xpath("//a[@href]", parser.parse("a[|href]"))
138138
assert_xpath("//*[@flavorjones:href]", parser.parse("*[flavorjones|href]"))
139139

140-
## Default namespace is not applied to attributes, so this is @class, not @xmlns:class.
141-
assert_xpath("//xmlns:a[@class='bar']", parser_with_ns.parse("a[class='bar']"))
142-
assert_xpath("//xmlns:a[@hoge:class='bar']", parser_with_ns.parse("a[hoge|class='bar']"))
140+
ns = {
141+
"xmlns" => "http://default.example.com/",
142+
"hoge" => "http://hoge.example.com/",
143+
}
144+
145+
# An intentionally-empty namespace means "don't use the default xmlns"
146+
assert_equal(["//a"], Nokogiri::CSS.xpath_for("|a", ns: ns))
147+
148+
# The default namespace is not applied to attributes (just elements)
149+
assert_equal(
150+
["//xmlns:a[@class='bar']"],
151+
Nokogiri::CSS.xpath_for("a[class='bar']", ns: ns),
152+
)
153+
154+
# We can explicitly apply a namespace to an attribue
155+
assert_equal(
156+
["//xmlns:a[@hoge:class='bar']"],
157+
Nokogiri::CSS.xpath_for("a[hoge|class='bar']", ns: ns),
158+
)
143159
end
144160

145161
it "rhs with quotes" do
@@ -543,6 +559,7 @@ def visit_pseudo_class_aaron(node)
543559
builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS,
544560
doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML,
545561
prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX,
562+
namespaces: nil,
546563
}
547564
assert_equal(expected, visitor.config)
548565
end
@@ -606,6 +623,7 @@ def visit_pseudo_class_aaron(node)
606623
builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::OPTIMAL,
607624
doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML,
608625
prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX,
626+
namespaces: nil,
609627
}
610628
assert_equal(expected, visitor.config)
611629
end

0 commit comments

Comments
 (0)