Skip to content

Commit a40965b

Browse files
committed
XPath searches now work with compiled expressions
1 parent 4ae6160 commit a40965b

File tree

6 files changed

+112
-21
lines changed

6 files changed

+112
-21
lines changed

ext/nokogiri/nokogiri.h

+2
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ VALUE noko_xml_sax_parser_context_wrap(VALUE klass, xmlParserCtxtPtr c_context);
211211
xmlParserCtxtPtr noko_xml_sax_parser_context_unwrap(VALUE rb_context);
212212
void noko_xml_sax_parser_context_set_encoding(xmlParserCtxtPtr c_context, VALUE rb_encoding);
213213

214+
xmlXPathCompExprPtr noko_xml_xpath_expression_unwrap(VALUE rb_expression);
215+
214216
#define DOC_RUBY_OBJECT_TEST(x) ((nokogiriTuplePtr)(x->_private))
215217
#define DOC_RUBY_OBJECT(x) (((nokogiriTuplePtr)(x->_private))->doc)
216218
#define DOC_UNLINKED_NODE_HASH(x) (((nokogiriTuplePtr)(x->_private))->unlinkedNodes)

ext/nokogiri/xml_xpath_context.c

+11-2
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ noko_xml_xpath_context_evaluate(int argc, VALUE *argv, VALUE rb_context)
382382
VALUE rb_expression = Qnil;
383383
VALUE rb_function_lookup_handler = Qnil;
384384
xmlChar *c_expression_str = NULL;
385+
xmlXPathCompExprPtr c_expression_comp = NULL;
385386
VALUE rb_errors = rb_ary_new();
386387
xmlXPathObjectPtr c_xpath_object;
387388
VALUE rb_xpath_object = Qnil;
@@ -390,7 +391,11 @@ noko_xml_xpath_context_evaluate(int argc, VALUE *argv, VALUE rb_context)
390391

391392
rb_scan_args(argc, argv, "11", &rb_expression, &rb_function_lookup_handler);
392393

393-
c_expression_str = (xmlChar *)StringValueCStr(rb_expression);
394+
if (rb_obj_is_kind_of(rb_expression, cNokogiriXmlXpathExpression)) {
395+
c_expression_comp = noko_xml_xpath_expression_unwrap(rb_expression);
396+
} else {
397+
c_expression_str = (xmlChar *)StringValueCStr(rb_expression);
398+
}
394399

395400
if (Qnil != rb_function_lookup_handler) {
396401
/* FIXME: not sure if this is the correct place to shove private data. */
@@ -405,7 +410,11 @@ noko_xml_xpath_context_evaluate(int argc, VALUE *argv, VALUE rb_context)
405410
xmlSetStructuredErrorFunc((void *)rb_errors, noko__error_array_pusher);
406411
xmlSetGenericErrorFunc((void *)rb_errors, generic_exception_pusher);
407412

408-
c_xpath_object = xmlXPathEvalExpression(c_expression_str, c_context);
413+
if (c_expression_comp) {
414+
c_xpath_object = xmlXPathCompiledEval(c_expression_comp, c_context);
415+
} else {
416+
c_xpath_object = xmlXPathEvalExpression(c_expression_str, c_context);
417+
}
409418

410419
xmlSetStructuredErrorFunc(NULL, NULL);
411420
xmlSetGenericErrorFunc(NULL, NULL);

ext/nokogiri/xml_xpath_expression.c

+18-3
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,32 @@ static const rb_data_type_t _noko_xml_xpath_expression_type = {
2727
static VALUE
2828
noko_xml_xpath_expression_s_new(VALUE klass, VALUE rb_input)
2929
{
30-
xmlXPathCompExprPtr c_expr;
30+
xmlXPathCompExprPtr c_expr = NULL;
3131
VALUE rb_expr = Qnil;
32+
VALUE rb_errors = rb_ary_new();
33+
34+
xmlSetStructuredErrorFunc((void *)rb_errors, noko__error_array_pusher);
3235

3336
c_expr = xmlXPathCompile((const xmlChar *)StringValueCStr(rb_input));
34-
if (c_expr) {
35-
rb_expr = TypedData_Wrap_Struct(klass, &_noko_xml_xpath_expression_type, c_expr);
37+
38+
xmlSetStructuredErrorFunc(NULL, NULL);
39+
40+
if (c_expr == NULL) {
41+
rb_exc_raise(rb_ary_entry(rb_errors, 0));
3642
}
3743

44+
rb_expr = TypedData_Wrap_Struct(klass, &_noko_xml_xpath_expression_type, c_expr);
3845
return rb_expr;
3946
}
4047

48+
xmlXPathCompExprPtr
49+
noko_xml_xpath_expression_unwrap(VALUE rb_expression)
50+
{
51+
xmlXPathCompExprPtr c_expression;
52+
TypedData_Get_Struct(rb_expression, xmlXPathCompExpr, &_noko_xml_xpath_expression_type, c_expression);
53+
return c_expression;
54+
}
55+
4156
void
4257
noko_init_xml_xpath_expression(void)
4358
{

lib/nokogiri/xml/searchable.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def >(selector) # rubocop:disable Naming/BinaryOperatorParameterName
209209

210210
def extract_params(params) # :nodoc:
211211
handler = params.find do |param|
212-
![Hash, String, Symbol].include?(param.class)
212+
![Hash, String, Symbol, XPath::Expression].include?(param.class)
213213
end
214214
params -= [handler] if handler
215215

test/xml/test_xpath.rb

+59-15
Original file line numberDiff line numberDiff line change
@@ -719,9 +719,9 @@ def collision(nodes)
719719
node = doc.at_xpath("//ns:child", { "ns" => "http://nokogiri.org/ns1" })
720720
assert_equal("ns1", node.text)
721721

722-
assert_raises(XPath::SyntaxError) {
722+
assert_raises(XPath::SyntaxError) do
723723
doc.at_xpath("//ns:child")
724-
}
724+
end
725725

726726
node = doc.at_xpath("//child")
727727
assert_nil(node)
@@ -743,9 +743,9 @@ def collision(nodes)
743743
doc.xpath("//xmlns:child[nokogiri:thing(.)]", @handler)
744744
assert_equal(1, @handler.things.length)
745745

746-
assert_raises(XPath::SyntaxError) {
746+
assert_raises(XPath::SyntaxError) do
747747
doc.xpath("//xmlns:child[nokogiri:thing(.)]")
748-
}
748+
end
749749

750750
doc.xpath("//xmlns:child[nokogiri:thing(.)]", @handler)
751751
assert_equal(2, @handler.things.length)
@@ -763,34 +763,36 @@ def collision(nodes)
763763
nodes = @xml.xpath("//address[@domestic=$value]", nil, value: "Yes")
764764
assert_equal(4, nodes.length)
765765

766-
assert_raises(XPath::SyntaxError) {
766+
assert_raises(XPath::SyntaxError) do
767767
@xml.xpath("//address[@domestic=$value]")
768-
}
768+
end
769769

770770
nodes = @xml.xpath("//address[@domestic=$value]", nil, value: "Qwerty")
771771
assert_empty(nodes)
772772

773-
assert_raises(XPath::SyntaxError) {
773+
assert_raises(XPath::SyntaxError) do
774774
@xml.xpath("//address[@domestic=$value]")
775-
}
775+
end
776776

777777
nodes = @xml.xpath("//address[@domestic=$value]", nil, value: "Yes")
778778
assert_equal(4, nodes.length)
779779
end
780780
end
781781

782782
describe "compiled" do
783-
let(:doc) {
784-
Nokogiri::XML::Document.parse(<<~XML)
783+
let(:xml) {
784+
<<~XML
785785
<root xmlns="http://nokogiri.org/default" xmlns:ns1="http://nokogiri.org/ns1">
786786
<child>default</child>
787787
<ns1:child>ns1</ns1:child>
788788
</root>
789789
XML
790790
}
791791

792+
let(:doc) { Nokogiri::XML::Document.parse(xml) }
793+
792794
describe "XPath expressions" do
793-
it "works" do
795+
it "works in the trivial case" do
794796
expr = Nokogiri::XML::XPath.expression("//xmlns:child")
795797

796798
result = doc.xpath(expr)
@@ -800,13 +802,55 @@ def collision(nodes)
800802
end
801803
end
802804

803-
it "can be evaluated in different documents"
805+
it "works as expected with namespace bindings" do
806+
expr = Nokogiri::XML::XPath.expression("//ns:child")
804807

805-
it "work with function handlers"
808+
node = doc.at_xpath(expr, { "ns" => "http://nokogiri.org/ns1" })
809+
assert_equal("ns1", node.text)
806810

807-
it "work with variable bindings"
811+
assert_raises(XPath::SyntaxError) do
812+
doc.at_xpath("//ns:child")
813+
end
814+
end
808815

809-
it "work with namespace bindings"
816+
it "works as expected with a function handler" do
817+
expr = Nokogiri::XML::XPath.expression("//xmlns:child[nokogiri:thing(.)]")
818+
819+
doc.xpath(expr, @handler)
820+
assert_equal(1, @handler.things.length)
821+
822+
assert_raises(XPath::SyntaxError) do
823+
doc.xpath("//xmlns:child[nokogiri:thing(.)]")
824+
end
825+
end
826+
827+
it "works as expected with bound variables" do
828+
expr = Nokogiri::XML::XPath.expression("//address[@domestic=$value]")
829+
830+
nodes = @xml.xpath("//address[@domestic=$value]", nil, value: "Yes")
831+
assert_equal(4, nodes.length)
832+
833+
assert_raises(XPath::SyntaxError) do
834+
@xml.xpath(expr)
835+
end
836+
end
837+
838+
it "can be evaluated in different documents" do
839+
doc1 = Nokogiri::XML::Document.parse(xml)
840+
doc2 = Nokogiri::XML::Document.parse(xml)
841+
842+
expr = Nokogiri::XML::XPath.expression("//xmlns:child")
843+
844+
result1 = doc1.xpath(expr)
845+
result2 = doc2.xpath(expr)
846+
847+
assert_pattern do
848+
result1 => [{name: "child", namespace: { href: "http://nokogiri.org/default" }}]
849+
end
850+
assert_pattern do
851+
result2 => [{name: "child", namespace: { href: "http://nokogiri.org/default" }}]
852+
end
853+
end
810854
end
811855

812856
describe "CSS selectors" do

test/xml/test_xpath_expression.rb

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
require "helper"
4+
5+
describe Nokogiri::XML::XPath::Expression do
6+
it ".new" do
7+
assert_kind_of(Nokogiri::XML::XPath::Expression, Nokogiri::XML::XPath::Expression.new("//foo"))
8+
end
9+
10+
it "raises an exception when there are compile-time errors" do
11+
assert_raises(Nokogiri::XML::XPath::SyntaxError) do
12+
Nokogiri::XML::XPath.expression("//foo[")
13+
end
14+
end
15+
end
16+
17+
describe Nokogiri::XML::XPath do
18+
it "XPath.expression" do
19+
assert_kind_of(Nokogiri::XML::XPath::Expression, Nokogiri::XML::XPath.expression("//foo"))
20+
end
21+
end

0 commit comments

Comments
 (0)