Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/nokolexbor/libxml/tree.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extern "C" {

static size_t tmp_len;
#define NODE_NAME(node) lxb_dom_node_name_qualified((lxb_dom_node_t *)(node), &tmp_len)
#define NODE_NS_HREF(node) ((node)->prefix ? lxb_ns_by_id((node)->owner_document->ns, (node)->ns, &tmp_len) : NULL)
#define NODE_NS_HREF(node) (((node)->ns != 0 && (node)->ns != LXB_NS_HTML) ? lxb_ns_by_id((node)->owner_document->ns, (node)->ns, &tmp_len) : NULL)
#define NODE_NS_PREFIX(node) lxb_ns_by_id((node)->owner_document->prefix, (node)->prefix, &tmp_len)

/*
Expand Down
68 changes: 68 additions & 0 deletions ext/nokolexbor/nl_node.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "nokolexbor.h"
#include "config.h"
#include "libxml/tree.h"
#include <lexbor/ns/const.h>

#ifdef HAVE_PTHREAD_H
#include <pthread.h>
Expand Down Expand Up @@ -1202,6 +1203,70 @@ nl_node_path(VALUE self)
return ret;
}

/**
* Get the namespace of this Node
*
* @return [String] The namespace of this Node
*/
static VALUE
nl_node_namespace_href(VALUE self)
{
lxb_dom_node_t *node = nl_rb_node_unwrap(self);
/* ns == 0 means no namespace; LXB_NS_HTML is the implicit HTML namespace
* which we do not expose (matches Nokogiri::HTML behaviour) */
if (node->ns == 0 || node->ns == LXB_NS_HTML) return Qnil;
size_t len;
const lxb_char_t *href = lxb_ns_by_id(node->owner_document->ns, node->ns, &len);
if (href == NULL || href[0] == '\0') return Qnil;
return rb_utf8_str_new((const char *)href, len);
}

static VALUE
nl_node_namespace_prefix(VALUE self)
{
lxb_dom_node_t *node = nl_rb_node_unwrap(self);
if (node->prefix == 0) return Qnil;
const lxb_char_t *prefix = NODE_NS_PREFIX(node);
if (prefix == NULL || prefix[0] == '\0') return Qnil;
return rb_utf8_str_new((const char *)prefix, tmp_len);
}

static VALUE
nl_node_namespace_definitions(VALUE self)
{
lxb_dom_node_t *node = nl_rb_node_unwrap(self);
VALUE ary = rb_ary_new();

if (node->type != LXB_DOM_NODE_TYPE_ELEMENT) return ary;

VALUE rb_doc = nl_rb_document_get(self);
VALUE cNamespace = rb_const_get(mNokolexbor, rb_intern("Namespace"));

lxb_dom_attr_t *attr = lxb_dom_element_first_attribute(lxb_dom_interface_element(node));
while (attr != NULL) {
size_t name_len;
const lxb_char_t *name = lxb_dom_attr_qualified_name(attr, &name_len);
if (name_len >= 5 && strncmp((const char *)name, "xmlns", 5) == 0) {
size_t val_len;
const lxb_char_t *val = lxb_dom_attr_value(attr, &val_len);
VALUE rb_href = val ? rb_utf8_str_new((const char *)val, val_len) : rb_str_new("", 0);
VALUE rb_prefix;
if (name_len == 5) {
rb_prefix = Qnil;
} else if (name[5] == ':') {
rb_prefix = rb_utf8_str_new((const char *)name + 6, name_len - 6);
} else {
attr = attr->next;
continue;
}
VALUE ns = rb_funcall(cNamespace, rb_intern("new"), 3, rb_doc, rb_prefix, rb_href);
rb_ary_push(ary, ns);
}
attr = attr->next;
}
return ary;
}

static void
free_css_parser(void *data)
{
Expand Down Expand Up @@ -1270,6 +1335,9 @@ void Init_nl_node(void)
rb_define_method(cNokolexborNode, "inspect", nl_node_inspect, -1);
rb_define_method(cNokolexborNode, "source_location", nl_node_source_location, 0);
rb_define_method(cNokolexborNode, "path", nl_node_path, 0);
rb_define_method(cNokolexborNode, "namespace_href", nl_node_namespace_href, 0);
rb_define_method(cNokolexborNode, "namespace_prefix", nl_node_namespace_prefix, 0);
rb_define_method(cNokolexborNode, "namespace_definitions", nl_node_namespace_definitions, 0);

rb_define_alias(cNokolexborNode, "attr", "[]");
rb_define_alias(cNokolexborNode, "get_attribute", "[]");
Expand Down
1 change: 1 addition & 0 deletions lib/nokolexbor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
end

require 'nokolexbor/version'
require 'nokolexbor/namespace'
require 'nokolexbor/node'
require 'nokolexbor/document'
require 'nokolexbor/node_set'
Expand Down
7 changes: 7 additions & 0 deletions lib/nokolexbor/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ def document
self
end

# Get the namespaces in scope on the root element.
#
# @return [Hash{String => String}]
def namespaces
root&.namespaces || {}
end

# Get the meta tag encoding for this document. If there is no meta tag, nil is returned.
#
# @return [String]
Expand Down
21 changes: 21 additions & 0 deletions lib/nokolexbor/namespace.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Nokolexbor
class Namespace
attr_reader :prefix, :href, :document

def initialize(document, prefix, href)
@document = document
@prefix = prefix
@href = href
end

def inspect
if prefix
"#<#{self.class.name} prefix=#{prefix.inspect} href=#{href.inspect}>"
else
"#<#{self.class.name} href=#{href.inspect}>"
end
end
end
end
49 changes: 47 additions & 2 deletions lib/nokolexbor/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,52 @@ def document?
is_a?(Nokolexbor::Document)
end

# Get the {Namespace} of this node, or +nil+ if there is no namespace.
#
# @return [Namespace, nil]
def namespace
href = namespace_href
return nil if href.nil?
Nokolexbor::Namespace.new(document, namespace_prefix, href)
end

# Get the namespaces in scope for this node — those defined on the node
# itself and its ancestors.
#
# @return [Array<Namespace>]
def namespace_scopes
result = {}
node = self
while node && !node.document?
node.namespace_definitions.each do |ns|
result[ns.prefix] ||= ns
end
node = node.respond_to?(:parent) ? node.parent : nil
end
result.values
end

# Get the namespaces in scope for this node as a Hash.
#
# @return [Hash{String => String}] e.g. <tt>{"xmlns:foo" => "http://example.com"}</tt>
def namespaces
namespace_scopes.each_with_object({}) do |ns, hash|
key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"
hash[key] = ns.href
end
end

# Add a namespace definition to this node.
#
# @param prefix [String, nil] The namespace prefix, or +nil+ for the default namespace.
# @param href [String] The namespace URI.
# @return [Namespace]
def add_namespace_definition(prefix, href)
attr_name = prefix ? "xmlns:#{prefix}" : "xmlns"
set_attr(attr_name, href)
Nokolexbor::Namespace.new(document, prefix, href)
end

# Get the path to this node as a CSS expression
def css_path
path.split(%r{/}).filter_map do |part|
Expand Down Expand Up @@ -764,8 +810,7 @@ def extract_params(params)
end
ns, binds = hashes.reverse

# ns ||= (document.root&.namespaces || {})
ns ||= {}
ns ||= (document.root&.namespaces || {})

[params, handler, ns, binds]
end
Expand Down
133 changes: 133 additions & 0 deletions spec/node_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1053,4 +1053,137 @@
_(doc.at_css('.a').path).must_equal '/html/body/div/span/a[1]'
_(doc.at_css('.b').path).must_equal '/html/body/div/span/a[2]'
end

describe 'namespace' do
it 'returns nil for plain HTML elements' do
doc = Nokolexbor::HTML('<div></div>')
_(doc.at_css('div').namespace).must_be_nil
end

it 'returns Namespace for SVG elements' do
doc = Nokolexbor::HTML('<html><body><svg><circle/></svg></body></html>')
svg = doc.at_css('svg')
ns = svg.namespace
_(ns).must_be_instance_of Nokolexbor::Namespace
_(ns.href).must_equal 'http://www.w3.org/2000/svg'
_(ns.prefix).must_be_nil
end

it 'SVG children inherit SVG namespace' do
doc = Nokolexbor::HTML('<html><body><svg><circle/></svg></body></html>')
circle = doc.at_css('circle')
_(circle.namespace_href).must_equal 'http://www.w3.org/2000/svg'
end

it 'returns Namespace for MathML elements' do
doc = Nokolexbor::HTML('<html><body><math><mi>x</mi></math></body></html>')
math = doc.at_css('math')
ns = math.namespace
_(ns).must_be_instance_of Nokolexbor::Namespace
_(ns.href).must_equal 'http://www.w3.org/1998/Math/MathML'
end
end

describe 'namespace_definitions' do
it 'returns empty array for node without xmlns attributes' do
doc = Nokolexbor::HTML('<div></div>')
_(doc.at_css('div').namespace_definitions).must_equal []
end

it 'returns Namespace objects for xmlns:prefix attributes' do
doc = Nokolexbor::HTML('<div xmlns:foo="http://example.com/foo"><span></span></div>')
defs = doc.at_css('div').namespace_definitions
_(defs.size).must_equal 1
_(defs.first).must_be_instance_of Nokolexbor::Namespace
_(defs.first.prefix).must_equal 'foo'
_(defs.first.href).must_equal 'http://example.com/foo'
end

it 'returns Namespace with nil prefix for default xmlns attribute' do
doc = Nokolexbor::HTML('<div xmlns="http://example.com/default"></div>')
defs = doc.at_css('div').namespace_definitions
_(defs.size).must_equal 1
_(defs.first.prefix).must_be_nil
_(defs.first.href).must_equal 'http://example.com/default'
end

it 'returns empty array for non-element nodes' do
doc = Nokolexbor::HTML('<div>text</div>')
_(doc.at_css('div').children.first.namespace_definitions).must_equal []
end
end

describe 'namespaces' do
it 'returns empty hash for plain node' do
doc = Nokolexbor::HTML('<div></div>')
_(doc.at_css('div').namespaces).must_equal({})
end

it 'returns hash with xmlns: keys for prefixed namespaces' do
doc = Nokolexbor::HTML('<div xmlns:foo="http://example.com/foo"></div>')
_(doc.at_css('div').namespaces).must_equal('xmlns:foo' => 'http://example.com/foo')
end

it 'returns xmlns key for default namespace' do
doc = Nokolexbor::HTML('<div xmlns="http://example.com/default"></div>')
_(doc.at_css('div').namespaces).must_equal('xmlns' => 'http://example.com/default')
end

it 'includes ancestor namespaces' do
doc = Nokolexbor::HTML('<div xmlns:foo="http://example.com/foo"><span xmlns:bar="http://example.com/bar"></span></div>')
ns = doc.at_css('span').namespaces
_(ns['xmlns:foo']).must_equal 'http://example.com/foo'
_(ns['xmlns:bar']).must_equal 'http://example.com/bar'
end

it 'child namespace does not override ancestor namespace with same prefix' do
doc = Nokolexbor::HTML('<div xmlns:foo="http://ancestor.com"><span xmlns:foo="http://child.com"></span></div>')
ns = doc.at_css('span').namespaces
_(ns['xmlns:foo']).must_equal 'http://child.com'
end
end

describe 'add_namespace_definition' do
it 'adds xmlns attribute and returns Namespace object' do
doc = Nokolexbor::HTML('<div></div>')
node = doc.at_css('div')
ns = node.add_namespace_definition('foo', 'http://example.com/foo')
_(ns).must_be_instance_of Nokolexbor::Namespace
_(ns.prefix).must_equal 'foo'
_(ns.href).must_equal 'http://example.com/foo'
_(node['xmlns:foo']).must_equal 'http://example.com/foo'
end

it 'adds default namespace with nil prefix' do
doc = Nokolexbor::HTML('<div></div>')
node = doc.at_css('div')
ns = node.add_namespace_definition(nil, 'http://example.com/default')
_(ns.prefix).must_be_nil
_(node['xmlns']).must_equal 'http://example.com/default'
end
end

describe 'Namespace class' do
it 'has prefix, href and document' do
doc = Nokolexbor::HTML('<div></div>')
ns = Nokolexbor::Namespace.new(doc, 'foo', 'http://example.com')
_(ns.prefix).must_equal 'foo'
_(ns.href).must_equal 'http://example.com'
_(ns.document).must_equal doc
end

it 'inspect includes prefix and href' do
doc = Nokolexbor::HTML('<div></div>')
ns = Nokolexbor::Namespace.new(doc, 'foo', 'http://example.com')
_(ns.inspect).must_include 'prefix="foo"'
_(ns.inspect).must_include 'href="http://example.com"'
end

it 'inspect without prefix' do
doc = Nokolexbor::HTML('<div></div>')
ns = Nokolexbor::Namespace.new(doc, nil, 'http://example.com')
_(ns.inspect).wont_include 'prefix'
_(ns.inspect).must_include 'href="http://example.com"'
end
end
end