519 lines
12 KiB
C
519 lines
12 KiB
C
#include <nokogiri.h>
|
|
|
|
VALUE cNokogiriXmlNodeSet ;
|
|
|
|
static ID decorate ;
|
|
|
|
static void
|
|
Check_Node_Set_Node_Type(VALUE node)
|
|
{
|
|
if (!(rb_obj_is_kind_of(node, cNokogiriXmlNode) ||
|
|
rb_obj_is_kind_of(node, cNokogiriXmlNamespace))) {
|
|
rb_raise(rb_eArgError, "node must be a Nokogiri::XML::Node or Nokogiri::XML::Namespace");
|
|
}
|
|
}
|
|
|
|
|
|
static
|
|
VALUE
|
|
ruby_object_get(xmlNodePtr c_node)
|
|
{
|
|
/* see xmlElementType in libxml2 tree.h */
|
|
switch (c_node->type) {
|
|
case XML_NAMESPACE_DECL:
|
|
/* _private is later in the namespace struct */
|
|
return (VALUE)(((xmlNsPtr)c_node)->_private);
|
|
|
|
case XML_DOCUMENT_NODE:
|
|
case XML_HTML_DOCUMENT_NODE:
|
|
/* in documents we use _private to store a tuple */
|
|
if (DOC_RUBY_OBJECT_TEST(((xmlDocPtr)c_node))) {
|
|
return DOC_RUBY_OBJECT((xmlDocPtr)c_node);
|
|
}
|
|
return (VALUE)NULL;
|
|
|
|
default:
|
|
return (VALUE)(c_node->_private);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
xml_node_set_mark(void *data)
|
|
{
|
|
xmlNodeSetPtr node_set = data;
|
|
VALUE rb_node;
|
|
int jnode;
|
|
|
|
for (jnode = 0; jnode < node_set->nodeNr; jnode++) {
|
|
rb_node = ruby_object_get(node_set->nodeTab[jnode]);
|
|
if (rb_node) {
|
|
rb_gc_mark(rb_node);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
xml_node_set_deallocate(void *data)
|
|
{
|
|
xmlNodeSetPtr node_set = data;
|
|
/*
|
|
* For reasons outlined in xml_namespace.c, here we reproduce xmlXPathFreeNodeSet() except for the
|
|
* offending call to xmlXPathNodeSetFreeNs().
|
|
*/
|
|
if (node_set->nodeTab != NULL) {
|
|
xmlFree(node_set->nodeTab);
|
|
}
|
|
|
|
xmlFree(node_set);
|
|
}
|
|
|
|
static const rb_data_type_t xml_node_set_type = {
|
|
.wrap_struct_name = "xmlNodeSet",
|
|
.function = {
|
|
.dmark = xml_node_set_mark,
|
|
.dfree = xml_node_set_deallocate,
|
|
},
|
|
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
};
|
|
|
|
static VALUE
|
|
xml_node_set_allocate(VALUE klass)
|
|
{
|
|
return TypedData_Wrap_Struct(klass, &xml_node_set_type, xmlXPathNodeSetCreate(NULL));
|
|
}
|
|
|
|
/* :nodoc: */
|
|
static VALUE
|
|
rb_xml_node_set_initialize_copy(VALUE rb_self, VALUE rb_other)
|
|
{
|
|
xmlNodeSetPtr c_self, c_other;
|
|
VALUE rb_document;
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
TypedData_Get_Struct(rb_other, xmlNodeSet, &xml_node_set_type, c_other);
|
|
|
|
xmlXPathNodeSetMerge(c_self, c_other);
|
|
|
|
rb_document = rb_iv_get(rb_other, "@document");
|
|
if (!NIL_P(rb_document)) {
|
|
rb_iv_set(rb_self, "@document", rb_document);
|
|
rb_funcall(rb_document, decorate, 1, rb_self);
|
|
}
|
|
|
|
return rb_self;
|
|
}
|
|
|
|
static void
|
|
xpath_node_set_del(xmlNodeSetPtr cur, xmlNodePtr val)
|
|
{
|
|
/*
|
|
* For reasons outlined in xml_namespace.c, here we reproduce xmlXPathNodeSetDel() except for the
|
|
* offending call to xmlXPathNodeSetFreeNs().
|
|
*/
|
|
int i;
|
|
|
|
if (cur == NULL) { return; }
|
|
if (val == NULL) { return; }
|
|
|
|
/*
|
|
* find node in nodeTab
|
|
*/
|
|
for (i = 0; i < cur->nodeNr; i++)
|
|
if (cur->nodeTab[i] == val) { break; }
|
|
|
|
if (i >= cur->nodeNr) { /* not found */
|
|
return;
|
|
}
|
|
cur->nodeNr--;
|
|
for (; i < cur->nodeNr; i++) {
|
|
cur->nodeTab[i] = cur->nodeTab[i + 1];
|
|
}
|
|
cur->nodeTab[cur->nodeNr] = NULL;
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* length
|
|
*
|
|
* Get the length of the node set
|
|
*/
|
|
static VALUE
|
|
length(VALUE rb_self)
|
|
{
|
|
xmlNodeSetPtr c_self;
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
|
|
return c_self ? INT2NUM(c_self->nodeNr) : INT2NUM(0);
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* push(node)
|
|
*
|
|
* Append +node+ to the NodeSet.
|
|
*/
|
|
static VALUE
|
|
push(VALUE rb_self, VALUE rb_node)
|
|
{
|
|
xmlNodeSetPtr c_self;
|
|
xmlNodePtr node;
|
|
|
|
Check_Node_Set_Node_Type(rb_node);
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
Noko_Node_Get_Struct(rb_node, xmlNode, node);
|
|
|
|
xmlXPathNodeSetAdd(c_self, node);
|
|
|
|
return rb_self;
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* delete(node)
|
|
*
|
|
* Delete +node+ from the Nodeset, if it is a member. Returns the deleted node
|
|
* if found, otherwise returns nil.
|
|
*/
|
|
static VALUE
|
|
delete (VALUE rb_self, VALUE rb_node)
|
|
{
|
|
xmlNodeSetPtr c_self;
|
|
xmlNodePtr node;
|
|
|
|
Check_Node_Set_Node_Type(rb_node);
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
Noko_Node_Get_Struct(rb_node, xmlNode, node);
|
|
|
|
if (xmlXPathNodeSetContains(c_self, node)) {
|
|
xpath_node_set_del(c_self, node);
|
|
return rb_node;
|
|
}
|
|
return Qnil ;
|
|
}
|
|
|
|
|
|
/*
|
|
* call-seq:
|
|
* &(node_set)
|
|
*
|
|
* Set Intersection — Returns a new NodeSet containing nodes common to the two NodeSets.
|
|
*/
|
|
static VALUE
|
|
intersection(VALUE rb_self, VALUE rb_other)
|
|
{
|
|
xmlNodeSetPtr c_self, c_other ;
|
|
xmlNodeSetPtr intersection;
|
|
|
|
if (!rb_obj_is_kind_of(rb_other, cNokogiriXmlNodeSet)) {
|
|
rb_raise(rb_eArgError, "node_set must be a Nokogiri::XML::NodeSet");
|
|
}
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
TypedData_Get_Struct(rb_other, xmlNodeSet, &xml_node_set_type, c_other);
|
|
|
|
intersection = xmlXPathIntersection(c_self, c_other);
|
|
return noko_xml_node_set_wrap(intersection, rb_iv_get(rb_self, "@document"));
|
|
}
|
|
|
|
|
|
/*
|
|
* call-seq:
|
|
* include?(node)
|
|
*
|
|
* Returns true if any member of node set equals +node+.
|
|
*/
|
|
static VALUE
|
|
include_eh(VALUE rb_self, VALUE rb_node)
|
|
{
|
|
xmlNodeSetPtr c_self;
|
|
xmlNodePtr node;
|
|
|
|
Check_Node_Set_Node_Type(rb_node);
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
Noko_Node_Get_Struct(rb_node, xmlNode, node);
|
|
|
|
return (xmlXPathNodeSetContains(c_self, node) ? Qtrue : Qfalse);
|
|
}
|
|
|
|
|
|
/*
|
|
* call-seq:
|
|
* |(node_set)
|
|
*
|
|
* Returns a new set built by merging the set and the elements of the given
|
|
* set.
|
|
*/
|
|
static VALUE
|
|
rb_xml_node_set_union(VALUE rb_self, VALUE rb_other)
|
|
{
|
|
xmlNodeSetPtr c_self, c_other;
|
|
xmlNodeSetPtr c_new_node_set;
|
|
|
|
if (!rb_obj_is_kind_of(rb_other, cNokogiriXmlNodeSet)) {
|
|
rb_raise(rb_eArgError, "node_set must be a Nokogiri::XML::NodeSet");
|
|
}
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
TypedData_Get_Struct(rb_other, xmlNodeSet, &xml_node_set_type, c_other);
|
|
|
|
c_new_node_set = xmlXPathNodeSetMerge(NULL, c_self);
|
|
c_new_node_set = xmlXPathNodeSetMerge(c_new_node_set, c_other);
|
|
|
|
return noko_xml_node_set_wrap(c_new_node_set, rb_iv_get(rb_self, "@document"));
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* -(node_set)
|
|
*
|
|
* Difference - returns a new NodeSet that is a copy of this NodeSet, removing
|
|
* each item that also appears in +node_set+
|
|
*/
|
|
static VALUE
|
|
minus(VALUE rb_self, VALUE rb_other)
|
|
{
|
|
xmlNodeSetPtr c_self, c_other;
|
|
xmlNodeSetPtr new;
|
|
int j ;
|
|
|
|
if (!rb_obj_is_kind_of(rb_other, cNokogiriXmlNodeSet)) {
|
|
rb_raise(rb_eArgError, "node_set must be a Nokogiri::XML::NodeSet");
|
|
}
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
TypedData_Get_Struct(rb_other, xmlNodeSet, &xml_node_set_type, c_other);
|
|
|
|
new = xmlXPathNodeSetMerge(NULL, c_self);
|
|
for (j = 0 ; j < c_other->nodeNr ; ++j) {
|
|
xpath_node_set_del(new, c_other->nodeTab[j]);
|
|
}
|
|
|
|
return noko_xml_node_set_wrap(new, rb_iv_get(rb_self, "@document"));
|
|
}
|
|
|
|
|
|
static VALUE
|
|
index_at(VALUE rb_self, long offset)
|
|
{
|
|
xmlNodeSetPtr c_self;
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
|
|
if (offset >= c_self->nodeNr || abs((int)offset) > c_self->nodeNr) {
|
|
return Qnil;
|
|
}
|
|
|
|
if (offset < 0) { offset += c_self->nodeNr ; }
|
|
|
|
return noko_xml_node_wrap_node_set_result(c_self->nodeTab[offset], rb_self);
|
|
}
|
|
|
|
static VALUE
|
|
subseq(VALUE rb_self, long beg, long len)
|
|
{
|
|
long j;
|
|
xmlNodeSetPtr c_self;
|
|
xmlNodeSetPtr new_set ;
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
|
|
if (beg > c_self->nodeNr) { return Qnil ; }
|
|
if (beg < 0 || len < 0) { return Qnil ; }
|
|
|
|
if ((beg + len) > c_self->nodeNr) {
|
|
len = c_self->nodeNr - beg ;
|
|
}
|
|
|
|
new_set = xmlXPathNodeSetCreate(NULL);
|
|
for (j = beg ; j < beg + len ; ++j) {
|
|
xmlXPathNodeSetAddUnique(new_set, c_self->nodeTab[j]);
|
|
}
|
|
return noko_xml_node_set_wrap(new_set, rb_iv_get(rb_self, "@document"));
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* [index] -> Node or nil
|
|
* [start, length] -> NodeSet or nil
|
|
* [range] -> NodeSet or nil
|
|
* slice(index) -> Node or nil
|
|
* slice(start, length) -> NodeSet or nil
|
|
* slice(range) -> NodeSet or nil
|
|
*
|
|
* Element reference - returns the node at +index+, or returns a NodeSet
|
|
* containing nodes starting at +start+ and continuing for +length+ elements, or
|
|
* returns a NodeSet containing nodes specified by +range+. Negative +indices+
|
|
* count backward from the end of the +node_set+ (-1 is the last node). Returns
|
|
* nil if the +index+ (or +start+) are out of range.
|
|
*/
|
|
static VALUE
|
|
slice(int argc, VALUE *argv, VALUE rb_self)
|
|
{
|
|
VALUE arg ;
|
|
long beg, len ;
|
|
xmlNodeSetPtr c_self;
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
|
|
if (argc == 2) {
|
|
beg = NUM2LONG(argv[0]);
|
|
len = NUM2LONG(argv[1]);
|
|
if (beg < 0) {
|
|
beg += c_self->nodeNr ;
|
|
}
|
|
return subseq(rb_self, beg, len);
|
|
}
|
|
|
|
if (argc != 1) {
|
|
rb_scan_args(argc, argv, "11", NULL, NULL);
|
|
}
|
|
arg = argv[0];
|
|
|
|
if (FIXNUM_P(arg)) {
|
|
return index_at(rb_self, FIX2LONG(arg));
|
|
}
|
|
|
|
/* if arg is Range */
|
|
switch (rb_range_beg_len(arg, &beg, &len, (long)c_self->nodeNr, 0)) {
|
|
case Qfalse:
|
|
break;
|
|
case Qnil:
|
|
return Qnil;
|
|
default:
|
|
return subseq(rb_self, beg, len);
|
|
}
|
|
|
|
return index_at(rb_self, NUM2LONG(arg));
|
|
}
|
|
|
|
|
|
/*
|
|
* call-seq:
|
|
* to_a
|
|
*
|
|
* Return this list as an Array
|
|
*/
|
|
static VALUE
|
|
to_array(VALUE rb_self)
|
|
{
|
|
xmlNodeSetPtr c_self ;
|
|
VALUE list;
|
|
int i;
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
|
|
list = rb_ary_new2(c_self->nodeNr);
|
|
for (i = 0; i < c_self->nodeNr; i++) {
|
|
VALUE elt = noko_xml_node_wrap_node_set_result(c_self->nodeTab[i], rb_self);
|
|
rb_ary_push(list, elt);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/*
|
|
* call-seq:
|
|
* unlink
|
|
*
|
|
* Unlink this NodeSet and all Node objects it contains from their current context.
|
|
*/
|
|
static VALUE
|
|
unlink_nodeset(VALUE rb_self)
|
|
{
|
|
xmlNodeSetPtr c_self;
|
|
int j, nodeNr ;
|
|
|
|
TypedData_Get_Struct(rb_self, xmlNodeSet, &xml_node_set_type, c_self);
|
|
|
|
nodeNr = c_self->nodeNr ;
|
|
for (j = 0 ; j < nodeNr ; j++) {
|
|
if (! NOKOGIRI_NAMESPACE_EH(c_self->nodeTab[j])) {
|
|
VALUE node ;
|
|
xmlNodePtr node_ptr;
|
|
node = noko_xml_node_wrap(Qnil, c_self->nodeTab[j]);
|
|
rb_funcall(node, rb_intern("unlink"), 0); /* modifies the C struct out from under the object */
|
|
Noko_Node_Get_Struct(node, xmlNode, node_ptr);
|
|
c_self->nodeTab[j] = node_ptr ;
|
|
}
|
|
}
|
|
return rb_self ;
|
|
}
|
|
|
|
|
|
VALUE
|
|
noko_xml_node_set_wrap(xmlNodeSetPtr c_node_set, VALUE document)
|
|
{
|
|
int j;
|
|
VALUE rb_node_set ;
|
|
|
|
if (c_node_set == NULL) {
|
|
rb_node_set = xml_node_set_allocate(cNokogiriXmlNodeSet);
|
|
} else {
|
|
rb_node_set = TypedData_Wrap_Struct(cNokogiriXmlNodeSet, &xml_node_set_type, c_node_set);
|
|
}
|
|
|
|
if (!NIL_P(document)) {
|
|
rb_iv_set(rb_node_set, "@document", document);
|
|
rb_funcall(document, decorate, 1, rb_node_set);
|
|
}
|
|
|
|
if (c_node_set) {
|
|
/* create ruby objects for all the results, so they'll be marked during the GC mark phase */
|
|
for (j = 0 ; j < c_node_set->nodeNr ; j++) {
|
|
noko_xml_node_wrap_node_set_result(c_node_set->nodeTab[j], rb_node_set);
|
|
}
|
|
}
|
|
|
|
return rb_node_set ;
|
|
}
|
|
|
|
|
|
VALUE
|
|
noko_xml_node_wrap_node_set_result(xmlNodePtr node, VALUE node_set)
|
|
{
|
|
if (NOKOGIRI_NAMESPACE_EH(node)) {
|
|
return noko_xml_namespace_wrap_xpath_copy((xmlNsPtr)node);
|
|
} else {
|
|
return noko_xml_node_wrap(Qnil, node);
|
|
}
|
|
}
|
|
|
|
|
|
xmlNodeSetPtr
|
|
noko_xml_node_set_unwrap(VALUE rb_node_set)
|
|
{
|
|
xmlNodeSetPtr c_node_set;
|
|
TypedData_Get_Struct(rb_node_set, xmlNodeSet, &xml_node_set_type, c_node_set);
|
|
return c_node_set;
|
|
}
|
|
|
|
|
|
void
|
|
noko_init_xml_node_set(void)
|
|
{
|
|
cNokogiriXmlNodeSet = rb_define_class_under(mNokogiriXml, "NodeSet", rb_cObject);
|
|
|
|
rb_define_alloc_func(cNokogiriXmlNodeSet, xml_node_set_allocate);
|
|
|
|
rb_define_method(cNokogiriXmlNodeSet, "&", intersection, 1);
|
|
rb_define_method(cNokogiriXmlNodeSet, "-", minus, 1);
|
|
rb_define_method(cNokogiriXmlNodeSet, "[]", slice, -1);
|
|
rb_define_method(cNokogiriXmlNodeSet, "delete", delete, 1);
|
|
rb_define_method(cNokogiriXmlNodeSet, "include?", include_eh, 1);
|
|
rb_define_method(cNokogiriXmlNodeSet, "length", length, 0);
|
|
rb_define_method(cNokogiriXmlNodeSet, "push", push, 1);
|
|
rb_define_method(cNokogiriXmlNodeSet, "slice", slice, -1);
|
|
rb_define_method(cNokogiriXmlNodeSet, "to_a", to_array, 0);
|
|
rb_define_method(cNokogiriXmlNodeSet, "unlink", unlink_nodeset, 0);
|
|
rb_define_method(cNokogiriXmlNodeSet, "|", rb_xml_node_set_union, 1);
|
|
|
|
rb_define_private_method(cNokogiriXmlNodeSet, "initialize_copy", rb_xml_node_set_initialize_copy, 1);
|
|
|
|
decorate = rb_intern("decorate");
|
|
}
|