nokolexbor 0.6.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/nokolexbor/nl_node.c +37 -6
- data/ext/nokolexbor/xml_xpath.c +20 -7
- data/lib/nokolexbor/builder.rb +223 -0
- data/lib/nokolexbor/version.rb +1 -1
- data/lib/nokolexbor.rb +13 -1
- data/patches/0006-lexbor-fix-sibling-combinator-in-pseudo-class-functions.patch +174 -0
- metadata +5 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f479dae3741e343ff292ef23b69fb31697f149b1e799b5eed26c2626ff5b9909
|
|
4
|
+
data.tar.gz: 35e0a468a1c9c94283271e66abd98663ce4ad9699613c4dea70df82f4d91e99b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b7c6cb9015d9b4540fb79e3fb04afb484a072f914f2480caa65f81fb66a077c099b8cbc81026aa9c8f1b4fc960ce57d9d2c17e1b1982de0f85a70c35e77cff9
|
|
7
|
+
data.tar.gz: 6bc107899aae62a756a84944579442fe482cece4375b49bd59a1061525d5e9a590e74308a08758b0effb6190c0f6f9920db5f6bceef43bf1caa4e8c413e68aca
|
data/ext/nokolexbor/nl_node.c
CHANGED
|
@@ -237,9 +237,23 @@ nl_node_content_set(VALUE self, VALUE content)
|
|
|
237
237
|
|
|
238
238
|
const char *c_content = StringValuePtr(content);
|
|
239
239
|
size_t content_len = RSTRING_LEN(content);
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
240
|
+
|
|
241
|
+
if (node->type == LXB_DOM_NODE_TYPE_DOCUMENT_FRAGMENT || node->type == LXB_DOM_NODE_TYPE_ELEMENT) {
|
|
242
|
+
lxb_dom_text_t *text = lxb_dom_document_create_text_node(node->owner_document, (const lxb_char_t *)c_content, content_len);
|
|
243
|
+
if (text == NULL) {
|
|
244
|
+
nl_raise_lexbor_error(LXB_STATUS_ERROR_MEMORY_ALLOCATION);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Ruby wrappers may still reference replaced nodes, so detach rather than destroy them.
|
|
248
|
+
while (node->first_child != NULL) {
|
|
249
|
+
lxb_dom_node_remove(node->first_child);
|
|
250
|
+
}
|
|
251
|
+
lxb_dom_node_insert_child(node, lxb_dom_interface_node(text));
|
|
252
|
+
} else {
|
|
253
|
+
lxb_status_t status = lxb_dom_node_text_content_set(node, (const lxb_char_t *)c_content, content_len);
|
|
254
|
+
if (status != LXB_STATUS_OK) {
|
|
255
|
+
nl_raise_lexbor_error(status);
|
|
256
|
+
}
|
|
243
257
|
}
|
|
244
258
|
return content;
|
|
245
259
|
}
|
|
@@ -561,12 +575,19 @@ static VALUE
|
|
|
561
575
|
nl_node_inner_html(int argc, VALUE *argv, VALUE self)
|
|
562
576
|
{
|
|
563
577
|
lxb_dom_node_t *node = nl_rb_node_unwrap(self);
|
|
578
|
+
lxb_dom_node_t *serialization_root = node;
|
|
564
579
|
lexbor_str_t str = {0};
|
|
565
580
|
VALUE options;
|
|
566
581
|
lxb_status_t status;
|
|
567
582
|
size_t indent = 0;
|
|
568
583
|
rb_scan_args(argc, argv, "01", &options);
|
|
569
584
|
|
|
585
|
+
if (node->type == LXB_DOM_NODE_TYPE_ELEMENT
|
|
586
|
+
&& node->local_name == LXB_TAG_TEMPLATE
|
|
587
|
+
&& node->ns == LXB_NS_HTML) {
|
|
588
|
+
serialization_root = &lxb_html_interface_template(node)->content->node;
|
|
589
|
+
}
|
|
590
|
+
|
|
570
591
|
if (TYPE(options) == T_HASH) {
|
|
571
592
|
VALUE rb_indent = rb_hash_aref(options, ID2SYM(rb_intern("indent")));
|
|
572
593
|
if (!NIL_P(rb_indent)) {
|
|
@@ -574,9 +595,9 @@ nl_node_inner_html(int argc, VALUE *argv, VALUE self)
|
|
|
574
595
|
}
|
|
575
596
|
}
|
|
576
597
|
if (indent > 0) {
|
|
577
|
-
status = lxb_html_serialize_pretty_deep_str(
|
|
598
|
+
status = lxb_html_serialize_pretty_deep_str(serialization_root, 0, 0, &str);
|
|
578
599
|
} else {
|
|
579
|
-
status = lxb_html_serialize_deep_str(
|
|
600
|
+
status = lxb_html_serialize_deep_str(serialization_root, &str);
|
|
580
601
|
}
|
|
581
602
|
if (status != LXB_STATUS_OK) {
|
|
582
603
|
if (str.data != NULL) {
|
|
@@ -921,6 +942,13 @@ nl_node_equals(VALUE self, VALUE other)
|
|
|
921
942
|
return node1 == node2 ? Qtrue : Qfalse;
|
|
922
943
|
}
|
|
923
944
|
|
|
945
|
+
static VALUE
|
|
946
|
+
nl_node_pointer_id(VALUE self)
|
|
947
|
+
{
|
|
948
|
+
lxb_dom_node_t *node = nl_rb_node_unwrap(self);
|
|
949
|
+
return ULL2NUM((unsigned long long)(uintptr_t)node);
|
|
950
|
+
}
|
|
951
|
+
|
|
924
952
|
const lxb_char_t *
|
|
925
953
|
lxb_dom_node_name_qualified(lxb_dom_node_t *node, size_t *len)
|
|
926
954
|
{
|
|
@@ -951,7 +979,7 @@ nl_node_parse_fragment(lxb_dom_document_t *doc, lxb_dom_element_t *element, lxb_
|
|
|
951
979
|
size_t tag_name_len;
|
|
952
980
|
lxb_html_document_t *html_doc = lxb_html_interface_document(doc);
|
|
953
981
|
if (element == NULL) {
|
|
954
|
-
const lxb_char_t *tag_name = lxb_tag_name_by_id(lxb_html_document_tags(html_doc),
|
|
982
|
+
const lxb_char_t *tag_name = lxb_tag_name_by_id(lxb_html_document_tags(html_doc), LXB_TAG_TEMPLATE, &tag_name_len);
|
|
955
983
|
if (tag_name == NULL) {
|
|
956
984
|
rb_raise(rb_eRuntimeError, "Error getting tag name");
|
|
957
985
|
}
|
|
@@ -1241,6 +1269,7 @@ void Init_nl_node(void)
|
|
|
1241
1269
|
rb_define_method(cNokolexborNode, "[]=", nl_node_set_attr, 2);
|
|
1242
1270
|
rb_define_method(cNokolexborNode, "remove_attr", nl_node_remove_attr, 1);
|
|
1243
1271
|
rb_define_method(cNokolexborNode, "==", nl_node_equals, 1);
|
|
1272
|
+
rb_define_method(cNokolexborNode, "pointer_id", nl_node_pointer_id, 0);
|
|
1244
1273
|
rb_define_method(cNokolexborNode, "css_impl", nl_node_css, 1);
|
|
1245
1274
|
rb_define_method(cNokolexborNode, "at_css_impl", nl_node_at_css, 1);
|
|
1246
1275
|
rb_define_method(cNokolexborNode, "inner_html", nl_node_inner_html, -1);
|
|
@@ -1289,4 +1318,6 @@ void Init_nl_node(void)
|
|
|
1289
1318
|
rb_define_alias(cNokolexborNode, "unlink", "remove");
|
|
1290
1319
|
rb_define_alias(cNokolexborNode, "type", "node_type");
|
|
1291
1320
|
rb_define_alias(cNokolexborNode, "dup", "clone");
|
|
1321
|
+
rb_define_alias(cNokolexborNode, "eql?", "==");
|
|
1322
|
+
rb_define_alias(cNokolexborNode, "hash", "pointer_id");
|
|
1292
1323
|
}
|
data/ext/nokolexbor/xml_xpath.c
CHANGED
|
@@ -7998,7 +7998,7 @@ nl_xmlXPathNextParent(xmlXPathParserContextPtr ctxt, lxb_dom_node_t_ptr cur) {
|
|
|
7998
7998
|
case LXB_DOM_NODE_TYPE_ATTRIBUTE: {
|
|
7999
7999
|
lxb_dom_attr_t_ptr att = (lxb_dom_attr_t_ptr) ctxt->context->node;
|
|
8000
8000
|
|
|
8001
|
-
return(att->
|
|
8001
|
+
return((lxb_dom_node_t_ptr) att->owner);
|
|
8002
8002
|
}
|
|
8003
8003
|
case LXB_DOM_NODE_TYPE_DOCUMENT:
|
|
8004
8004
|
case LXB_DOM_NODE_TYPE_DOCUMENT_TYPE:
|
|
@@ -8068,7 +8068,7 @@ nl_xmlXPathNextAncestor(xmlXPathParserContextPtr ctxt, lxb_dom_node_t_ptr cur) {
|
|
|
8068
8068
|
case LXB_DOM_NODE_TYPE_ATTRIBUTE: {
|
|
8069
8069
|
lxb_dom_attr_t_ptr tmp = (lxb_dom_attr_t_ptr) ctxt->context->node;
|
|
8070
8070
|
|
|
8071
|
-
return(tmp->
|
|
8071
|
+
return((lxb_dom_node_t_ptr) tmp->owner);
|
|
8072
8072
|
}
|
|
8073
8073
|
case LXB_DOM_NODE_TYPE_DOCUMENT:
|
|
8074
8074
|
case LXB_DOM_NODE_TYPE_DOCUMENT_TYPE:
|
|
@@ -8235,7 +8235,7 @@ nl_xmlXPathNextFollowing(xmlXPathParserContextPtr ctxt, lxb_dom_node_t_ptr cur)
|
|
|
8235
8235
|
if (cur == NULL) {
|
|
8236
8236
|
cur = ctxt->context->node;
|
|
8237
8237
|
if (cur->type == LXB_DOM_NODE_TYPE_ATTRIBUTE) {
|
|
8238
|
-
cur = cur->
|
|
8238
|
+
cur = (lxb_dom_node_t_ptr)((lxb_dom_attr_t *)cur)->owner;
|
|
8239
8239
|
} else if (cur->type == XML_NAMESPACE_DECL) {
|
|
8240
8240
|
xmlNsPtr ns = (xmlNsPtr) cur;
|
|
8241
8241
|
|
|
@@ -8305,7 +8305,7 @@ nl_xmlXPathNextPreceding(xmlXPathParserContextPtr ctxt, lxb_dom_node_t_ptr cur)
|
|
|
8305
8305
|
if (cur == NULL) {
|
|
8306
8306
|
cur = ctxt->context->node;
|
|
8307
8307
|
if (cur->type == LXB_DOM_NODE_TYPE_ATTRIBUTE) {
|
|
8308
|
-
cur = cur->
|
|
8308
|
+
cur = (lxb_dom_node_t_ptr)((lxb_dom_attr_t *)cur)->owner;
|
|
8309
8309
|
} else if (cur->type == XML_NAMESPACE_DECL) {
|
|
8310
8310
|
xmlNsPtr ns = (xmlNsPtr) cur;
|
|
8311
8311
|
|
|
@@ -8359,7 +8359,7 @@ xmlXPathNextPrecedingInternal(xmlXPathParserContextPtr ctxt,
|
|
|
8359
8359
|
if (cur == NULL)
|
|
8360
8360
|
return (NULL);
|
|
8361
8361
|
if (cur->type == LXB_DOM_NODE_TYPE_ATTRIBUTE) {
|
|
8362
|
-
cur = cur->
|
|
8362
|
+
cur = (lxb_dom_node_t_ptr)((lxb_dom_attr_t *)cur)->owner;
|
|
8363
8363
|
} else if (cur->type == XML_NAMESPACE_DECL) {
|
|
8364
8364
|
xmlNsPtr ns = (xmlNsPtr) cur;
|
|
8365
8365
|
|
|
@@ -8452,9 +8452,22 @@ nl_xmlXPathNextAttribute(xmlXPathParserContextPtr ctxt, lxb_dom_node_t_ptr cur)
|
|
|
8452
8452
|
if (cur == NULL) {
|
|
8453
8453
|
if (ctxt->context->node == (lxb_dom_node_t_ptr) ctxt->context->doc)
|
|
8454
8454
|
return(NULL);
|
|
8455
|
-
|
|
8455
|
+
cur = (lxb_dom_node_t_ptr)lxb_dom_element_first_attribute((lxb_dom_element_t *)ctxt->context->node);
|
|
8456
|
+
} else {
|
|
8457
|
+
cur = (lxb_dom_node_t_ptr)(((lxb_dom_attr_t *)cur)->next);
|
|
8458
|
+
}
|
|
8459
|
+
/*
|
|
8460
|
+
* In Lexbor, attr->owner is not set during tree construction.
|
|
8461
|
+
* Set it here so axis traversal functions that need to find the
|
|
8462
|
+
* owning element (preceding::, following::, ancestor::, parent::)
|
|
8463
|
+
* can use attr->owner when attributes are context nodes.
|
|
8464
|
+
*/
|
|
8465
|
+
if (cur != NULL) {
|
|
8466
|
+
lxb_dom_attr_t *attr = (lxb_dom_attr_t *)cur;
|
|
8467
|
+
if (attr->owner == NULL)
|
|
8468
|
+
attr->owner = (lxb_dom_element_t *)ctxt->context->node;
|
|
8456
8469
|
}
|
|
8457
|
-
return
|
|
8470
|
+
return cur;
|
|
8458
8471
|
}
|
|
8459
8472
|
|
|
8460
8473
|
/************************************************************************
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Nokolexbor
|
|
6
|
+
# A DSL for programmatically building HTML documents.
|
|
7
|
+
#
|
|
8
|
+
# == No-arg block (instance_eval style) – tag names are called bare:
|
|
9
|
+
#
|
|
10
|
+
# Nokolexbor do
|
|
11
|
+
# body do
|
|
12
|
+
# h1 'Hello world'
|
|
13
|
+
# p 'This little p'
|
|
14
|
+
# ul do
|
|
15
|
+
# li 'Go to market'
|
|
16
|
+
# li 'Go to bed'
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# == Block-parameter style – tag names are called on the builder argument:
|
|
22
|
+
#
|
|
23
|
+
# Nokolexbor::Builder.new do |b|
|
|
24
|
+
# b.body do
|
|
25
|
+
# b.h1 'Hello world'
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# == Building into an existing node:
|
|
30
|
+
#
|
|
31
|
+
# Nokolexbor::Builder.with(existing_node) do
|
|
32
|
+
# span 'injected'
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
class Builder
|
|
36
|
+
# The Document used to create new nodes
|
|
37
|
+
attr_accessor :doc
|
|
38
|
+
|
|
39
|
+
# The node that currently receives new children
|
|
40
|
+
attr_accessor :parent
|
|
41
|
+
|
|
42
|
+
# Arity of the outermost block (drives instance_eval vs yield dispatch)
|
|
43
|
+
attr_accessor :arity
|
|
44
|
+
|
|
45
|
+
# A context object for use when the block has no arguments
|
|
46
|
+
attr_accessor :context
|
|
47
|
+
|
|
48
|
+
# Build into an existing +root+ node.
|
|
49
|
+
def self.with(root, &block)
|
|
50
|
+
new(root, &block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param root [Node, nil]
|
|
54
|
+
# When given, new nodes are appended under +root+ and +root.document+
|
|
55
|
+
# is used as the owning document. When omitted a fresh {Document} and
|
|
56
|
+
# a {DocumentFragment} container are created.
|
|
57
|
+
def initialize(root = nil, &block)
|
|
58
|
+
if root
|
|
59
|
+
@doc = root.document
|
|
60
|
+
@parent = root
|
|
61
|
+
else
|
|
62
|
+
@doc = Document.new
|
|
63
|
+
@parent = DocumentFragment.new(@doc)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@context = nil
|
|
67
|
+
@arity = nil
|
|
68
|
+
|
|
69
|
+
return unless block
|
|
70
|
+
|
|
71
|
+
@arity = block.arity
|
|
72
|
+
if @arity <= 0
|
|
73
|
+
# Capture outer self so that outer methods / ivars remain accessible
|
|
74
|
+
# via method_missing delegation while the builder acts as self.
|
|
75
|
+
@context = eval("self", block.binding)
|
|
76
|
+
instance_eval(&block)
|
|
77
|
+
else
|
|
78
|
+
yield self
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Insert a Text node containing +string+.
|
|
83
|
+
def text(string)
|
|
84
|
+
insert(@doc.create_text_node(string))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Insert a CDATA node containing +string+.
|
|
88
|
+
def cdata(string)
|
|
89
|
+
insert(@doc.create_cdata(string))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Insert a Comment node containing +string+.
|
|
93
|
+
def comment(string)
|
|
94
|
+
insert(@doc.create_comment(string))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Append raw HTML (parsed into nodes) under the current parent.
|
|
98
|
+
def <<(string)
|
|
99
|
+
@doc.fragment(string).children.each { |x| insert(x) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Serialize the built content to an HTML string.
|
|
103
|
+
def to_html
|
|
104
|
+
@parent.to_html
|
|
105
|
+
end
|
|
106
|
+
alias_method :to_s, :to_html
|
|
107
|
+
|
|
108
|
+
# Ruby's Kernel#p, Kernel#pp, etc. are defined on Object and therefore on
|
|
109
|
+
# Builder itself. They must be overridden so they fall through to
|
|
110
|
+
# method_missing and create the corresponding HTML element instead of
|
|
111
|
+
# printing values to stdout.
|
|
112
|
+
def p(*args, &block) # :nodoc:
|
|
113
|
+
method_missing(:p, *args, &block)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def method_missing(method, *args, &block) # :nodoc:
|
|
117
|
+
# Only delegate to the outer context for methods the user defined there.
|
|
118
|
+
# Standard Object / Kernel methods (like :p, :pp, :puts …) must not be
|
|
119
|
+
# forwarded to @context because all objects respond to them, which would
|
|
120
|
+
# bypass element creation entirely.
|
|
121
|
+
if @context && !OBJECT_INSTANCE_METHODS.include?(method) && @context.respond_to?(method)
|
|
122
|
+
@context.send(method, *args, &block)
|
|
123
|
+
else
|
|
124
|
+
node = @doc.create_element(method.to_s.sub(/[_!]$/, ""), *args)
|
|
125
|
+
insert(node, &block)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def respond_to_missing?(method, include_private = false) # :nodoc:
|
|
130
|
+
(@context && !OBJECT_INSTANCE_METHODS.include?(method) && @context.respond_to?(method)) || super
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Methods defined on plain Object that should never be forwarded to @context
|
|
134
|
+
# as HTML element creation fallbacks.
|
|
135
|
+
OBJECT_INSTANCE_METHODS = Object.instance_methods.to_set.freeze
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# Add +node+ as a child of @parent; if a block is given, push @parent
|
|
140
|
+
# to +node+ for the duration of that block, then restore it.
|
|
141
|
+
# Returns a {NodeBuilder} for the inserted node so attributes / classes
|
|
142
|
+
# can be chained: <tt>div.container.hero!</tt>
|
|
143
|
+
def insert(node, &block)
|
|
144
|
+
node = @parent.add_child(node)
|
|
145
|
+
if block
|
|
146
|
+
old_parent = @parent
|
|
147
|
+
@parent = node
|
|
148
|
+
@arity ||= block.arity
|
|
149
|
+
begin
|
|
150
|
+
if @arity <= 0
|
|
151
|
+
instance_eval(&block)
|
|
152
|
+
else
|
|
153
|
+
yield(self)
|
|
154
|
+
end
|
|
155
|
+
ensure
|
|
156
|
+
@parent = old_parent
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
NodeBuilder.new(node, self)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Wraps a built node and exposes a fluent API for setting classes and ids:
|
|
163
|
+
#
|
|
164
|
+
# div.container # => <div class="container">
|
|
165
|
+
# div.box.highlight # => <div class="box highlight">
|
|
166
|
+
# div.thing! # => <div id="thing">
|
|
167
|
+
# div.container.hero! # => <div class="container" id="hero">
|
|
168
|
+
#
|
|
169
|
+
class NodeBuilder # :nodoc:
|
|
170
|
+
def initialize(node, doc_builder)
|
|
171
|
+
@node = node
|
|
172
|
+
@doc_builder = doc_builder
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def []=(k, v)
|
|
176
|
+
@node[k] = v
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def [](k)
|
|
180
|
+
@node[k]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def method_missing(method, *args, &block)
|
|
184
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
185
|
+
|
|
186
|
+
case method.to_s
|
|
187
|
+
when /^(.*)!$/
|
|
188
|
+
@node["id"] = Regexp.last_match(1)
|
|
189
|
+
@node.content = args.first if args.first
|
|
190
|
+
when /^(.*)=$/
|
|
191
|
+
@node[Regexp.last_match(1)] = args.first
|
|
192
|
+
else
|
|
193
|
+
@node["class"] =
|
|
194
|
+
((@node["class"] || "").split(/\s/) + [method.to_s]).join(" ")
|
|
195
|
+
@node.content = args.first if args.first
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
opts.each do |k, v|
|
|
199
|
+
@node[k.to_s] = ((@node[k.to_s] || "").split(/\s/) + [v.to_s]).join(" ")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if block
|
|
203
|
+
old_parent = @doc_builder.parent
|
|
204
|
+
@doc_builder.parent = @node
|
|
205
|
+
arity = @doc_builder.arity || block.arity
|
|
206
|
+
value = if arity <= 0
|
|
207
|
+
@doc_builder.instance_eval(&block)
|
|
208
|
+
else
|
|
209
|
+
yield(@doc_builder)
|
|
210
|
+
end
|
|
211
|
+
@doc_builder.parent = old_parent
|
|
212
|
+
return value
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
self
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def respond_to_missing?(_method, _include_private = false) # :nodoc:
|
|
219
|
+
true
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
data/lib/nokolexbor/version.rb
CHANGED
data/lib/nokolexbor.rb
CHANGED
|
@@ -28,11 +28,23 @@ require 'nokolexbor/node_set'
|
|
|
28
28
|
require 'nokolexbor/document_fragment'
|
|
29
29
|
require 'nokolexbor/xpath'
|
|
30
30
|
require 'nokolexbor/xpath_context'
|
|
31
|
+
require 'nokolexbor/builder'
|
|
31
32
|
|
|
32
33
|
module Nokolexbor
|
|
33
34
|
class << self
|
|
34
|
-
def
|
|
35
|
+
def parse(*args)
|
|
35
36
|
Document.parse(*args)
|
|
36
37
|
end
|
|
38
|
+
|
|
39
|
+
alias_method :HTML, :parse
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Parse an HTML document, or build one with the DSL when a block is given.
|
|
44
|
+
def Nokolexbor(string_or_io = nil, &block)
|
|
45
|
+
if block
|
|
46
|
+
Nokolexbor::Builder.new(&block).parent
|
|
47
|
+
else
|
|
48
|
+
Nokolexbor.parse(string_or_io)
|
|
37
49
|
end
|
|
38
50
|
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
diff --git a/source/lexbor/selectors/selectors.c b/source/lexbor/selectors/selectors.c
|
|
2
|
+
index 37b825e..3fec327 100644
|
|
3
|
+
--- a/source/lexbor/selectors/selectors.c
|
|
4
|
+
+++ b/source/lexbor/selectors/selectors.c
|
|
5
|
+
@@ -87,6 +87,16 @@ static lxb_status_t
|
|
6
|
+
lxb_selectors_first_match(lxb_dom_node_t *node,
|
|
7
|
+
lxb_css_selector_specificity_t *spec, void *ctx);
|
|
8
|
+
|
|
9
|
+
+static bool
|
|
10
|
+
+lxb_selectors_match_chain_rtl(lxb_selectors_t *selectors,
|
|
11
|
+
+ lxb_css_selector_t *selector,
|
|
12
|
+
+ lxb_dom_node_t *node);
|
|
13
|
+
+
|
|
14
|
+
+static bool
|
|
15
|
+
+lxb_selectors_match_node_rtl(lxb_selectors_t *selectors,
|
|
16
|
+
+ lxb_dom_node_t *node,
|
|
17
|
+
+ lxb_css_selector_list_t *list);
|
|
18
|
+
+
|
|
19
|
+
|
|
20
|
+
lxb_selectors_t *
|
|
21
|
+
lxb_selectors_create(void)
|
|
22
|
+
@@ -1254,6 +1264,106 @@ lxb_selectors_pseudo_class(lxb_css_selector_t *selector, lxb_dom_node_t *node)
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
+/*
|
|
27
|
+
+ * Recursively match a selector chain (right-to-left) anchored at `node`,
|
|
28
|
+
+ * with backtracking: when a combinator has multiple candidate elements
|
|
29
|
+
+ * (descendant, following sibling), each candidate is tried until the rest
|
|
30
|
+
+ * of the chain matches.
|
|
31
|
+
+ */
|
|
32
|
+
+static bool
|
|
33
|
+
+lxb_selectors_match_chain_rtl(lxb_selectors_t *selectors,
|
|
34
|
+
+ lxb_css_selector_t *selector,
|
|
35
|
+
+ lxb_dom_node_t *node)
|
|
36
|
+
+{
|
|
37
|
+
+ lxb_css_selector_t *prev;
|
|
38
|
+
+ lxb_dom_node_t *cur;
|
|
39
|
+
+ lxb_selectors_entry_t tmp_entry;
|
|
40
|
+
+
|
|
41
|
+
+ memset(&tmp_entry, 0, sizeof(tmp_entry));
|
|
42
|
+
+ tmp_entry.selector = selector;
|
|
43
|
+
+
|
|
44
|
+
+ if (!lxb_selectors_match(selectors, &tmp_entry, selector, node)) {
|
|
45
|
+
+ return false;
|
|
46
|
+
+ }
|
|
47
|
+
+
|
|
48
|
+
+ prev = selector->prev;
|
|
49
|
+
+ if (prev == NULL) {
|
|
50
|
+
+ return true;
|
|
51
|
+
+ }
|
|
52
|
+
+
|
|
53
|
+
+ switch (selector->combinator) {
|
|
54
|
+
+ case LXB_CSS_SELECTOR_COMBINATOR_CLOSE:
|
|
55
|
+
+ /* Same node - check prev selector on same element. */
|
|
56
|
+
+ return lxb_selectors_match_chain_rtl(selectors, prev, node);
|
|
57
|
+
+
|
|
58
|
+
+ case LXB_CSS_SELECTOR_COMBINATOR_DESCENDANT:
|
|
59
|
+
+ /* node is a descendant of prev - try each ancestor. */
|
|
60
|
+
+ for (cur = node->parent; cur != NULL; cur = cur->parent) {
|
|
61
|
+
+ if (cur->type == LXB_DOM_NODE_TYPE_ELEMENT
|
|
62
|
+
+ && lxb_selectors_match_chain_rtl(selectors, prev, cur))
|
|
63
|
+
+ {
|
|
64
|
+
+ return true;
|
|
65
|
+
+ }
|
|
66
|
+
+ }
|
|
67
|
+
+ return false;
|
|
68
|
+
+
|
|
69
|
+
+ case LXB_CSS_SELECTOR_COMBINATOR_CHILD:
|
|
70
|
+
+ /* node is a child of prev - check parent. */
|
|
71
|
+
+ cur = node->parent;
|
|
72
|
+
+ return cur != NULL
|
|
73
|
+
+ && cur->type == LXB_DOM_NODE_TYPE_ELEMENT
|
|
74
|
+
+ && lxb_selectors_match_chain_rtl(selectors, prev, cur);
|
|
75
|
+
+
|
|
76
|
+
+ case LXB_CSS_SELECTOR_COMBINATOR_SIBLING:
|
|
77
|
+
+ /* node is adjacent to prev (+) - preceding element. */
|
|
78
|
+
+ cur = node->prev;
|
|
79
|
+
+ while (cur != NULL && cur->type != LXB_DOM_NODE_TYPE_ELEMENT) {
|
|
80
|
+
+ cur = cur->prev;
|
|
81
|
+
+ }
|
|
82
|
+
+ return cur != NULL
|
|
83
|
+
+ && lxb_selectors_match_chain_rtl(selectors, prev, cur);
|
|
84
|
+
+
|
|
85
|
+
+ case LXB_CSS_SELECTOR_COMBINATOR_FOLLOWING:
|
|
86
|
+
+ /* node follows prev (~) - try each preceding sibling. */
|
|
87
|
+
+ for (cur = node->prev; cur != NULL; cur = cur->prev) {
|
|
88
|
+
+ if (cur->type == LXB_DOM_NODE_TYPE_ELEMENT
|
|
89
|
+
+ && lxb_selectors_match_chain_rtl(selectors, prev, cur))
|
|
90
|
+
+ {
|
|
91
|
+
+ return true;
|
|
92
|
+
+ }
|
|
93
|
+
+ }
|
|
94
|
+
+ return false;
|
|
95
|
+
+
|
|
96
|
+
+ default:
|
|
97
|
+
+ return false;
|
|
98
|
+
+ }
|
|
99
|
+
+}
|
|
100
|
+
+
|
|
101
|
+
+/*
|
|
102
|
+
+ * Match a node against a selector list using right-to-left evaluation.
|
|
103
|
+
+ * This is needed for :not(), :is(), :where() etc. where we test a specific
|
|
104
|
+
+ * node against a selector that may contain sibling/following combinators.
|
|
105
|
+
+ *
|
|
106
|
+
+ * The left-to-right algorithm (lxb_selectors_next) can't correctly test
|
|
107
|
+
+ * whether a node matches "A ~ B" because it searches forward from the node
|
|
108
|
+
+ * for A, but A should be BEFORE the node.
|
|
109
|
+
+ */
|
|
110
|
+
+static bool
|
|
111
|
+
+lxb_selectors_match_node_rtl(lxb_selectors_t *selectors,
|
|
112
|
+
+ lxb_dom_node_t *node,
|
|
113
|
+
+ lxb_css_selector_list_t *list)
|
|
114
|
+
+{
|
|
115
|
+
+ while (list != NULL) {
|
|
116
|
+
+ if (lxb_selectors_match_chain_rtl(selectors, list->last, node)) {
|
|
117
|
+
+ return true;
|
|
118
|
+
+ }
|
|
119
|
+
+
|
|
120
|
+
+ list = list->next;
|
|
121
|
+
+ }
|
|
122
|
+
+
|
|
123
|
+
+ return false;
|
|
124
|
+
+}
|
|
125
|
+
+
|
|
126
|
+
static bool
|
|
127
|
+
lxb_selectors_pseudo_class_function(lxb_selectors_t *selectors,
|
|
128
|
+
lxb_css_selector_t *selector,
|
|
129
|
+
@@ -1268,14 +1378,7 @@ lxb_selectors_pseudo_class_function(lxb_selectors_t *selectors,
|
|
130
|
+
|
|
131
|
+
switch (pseudo->type) {
|
|
132
|
+
case LXB_CSS_SELECTOR_PSEUDO_CLASS_FUNCTION_CURRENT:
|
|
133
|
+
- entry->child = lxb_selectors_next(selectors, node, entry->child,
|
|
134
|
+
- pseudo->data,
|
|
135
|
+
- lxb_selectors_first_match, &found);
|
|
136
|
+
- if (entry->child == NULL) {
|
|
137
|
+
- return false;
|
|
138
|
+
- }
|
|
139
|
+
-
|
|
140
|
+
- return found;
|
|
141
|
+
+ return lxb_selectors_match_node_rtl(selectors, node, pseudo->data);
|
|
142
|
+
|
|
143
|
+
case LXB_CSS_SELECTOR_PSEUDO_CLASS_FUNCTION_DIR:
|
|
144
|
+
break;
|
|
145
|
+
@@ -1291,27 +1394,13 @@ lxb_selectors_pseudo_class_function(lxb_selectors_t *selectors,
|
|
146
|
+
return found;
|
|
147
|
+
|
|
148
|
+
case LXB_CSS_SELECTOR_PSEUDO_CLASS_FUNCTION_IS:
|
|
149
|
+
- entry->child = lxb_selectors_next(selectors, node, entry->child,
|
|
150
|
+
- pseudo->data,
|
|
151
|
+
- lxb_selectors_first_match, &found);
|
|
152
|
+
- if (entry->child == NULL) {
|
|
153
|
+
- return false;
|
|
154
|
+
- }
|
|
155
|
+
-
|
|
156
|
+
- return found;
|
|
157
|
+
+ return lxb_selectors_match_node_rtl(selectors, node, pseudo->data);
|
|
158
|
+
|
|
159
|
+
case LXB_CSS_SELECTOR_PSEUDO_CLASS_FUNCTION_LANG:
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case LXB_CSS_SELECTOR_PSEUDO_CLASS_FUNCTION_NOT:
|
|
163
|
+
- entry->child = lxb_selectors_next(selectors, node, entry->child,
|
|
164
|
+
- pseudo->data,
|
|
165
|
+
- lxb_selectors_first_match, &found);
|
|
166
|
+
- if (entry->child == NULL) {
|
|
167
|
+
- return false;
|
|
168
|
+
- }
|
|
169
|
+
-
|
|
170
|
+
- return !found;
|
|
171
|
+
+ return !lxb_selectors_match_node_rtl(selectors, node, pseudo->data);
|
|
172
|
+
|
|
173
|
+
case LXB_CSS_SELECTOR_PSEUDO_CLASS_FUNCTION_NTH_CHILD:
|
|
174
|
+
index = 0;
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nokolexbor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yicheng Zhou
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
10
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rake-compiler
|
|
@@ -120,6 +119,7 @@ files:
|
|
|
120
119
|
- ext/nokolexbor/xml_tree.c
|
|
121
120
|
- ext/nokolexbor/xml_xpath.c
|
|
122
121
|
- lib/nokolexbor.rb
|
|
122
|
+
- lib/nokolexbor/builder.rb
|
|
123
123
|
- lib/nokolexbor/document.rb
|
|
124
124
|
- lib/nokolexbor/document_fragment.rb
|
|
125
125
|
- lib/nokolexbor/node.rb
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- patches/0003-lexbor-attach-template-content-to-self.patch
|
|
133
133
|
- patches/0004-lexbor-fix-template-clone.patch
|
|
134
134
|
- patches/0005-lexbor-add-source-location-to-node.patch
|
|
135
|
+
- patches/0006-lexbor-fix-sibling-combinator-in-pseudo-class-functions.patch
|
|
135
136
|
- vendor/lexbor/CMakeLists.txt
|
|
136
137
|
- vendor/lexbor/config.cmake
|
|
137
138
|
- vendor/lexbor/feature.cmake
|
|
@@ -523,7 +524,6 @@ licenses:
|
|
|
523
524
|
- MIT
|
|
524
525
|
metadata:
|
|
525
526
|
msys2_mingw_dependencies: cmake
|
|
526
|
-
post_install_message:
|
|
527
527
|
rdoc_options: []
|
|
528
528
|
require_paths:
|
|
529
529
|
- lib
|
|
@@ -538,8 +538,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
538
538
|
- !ruby/object:Gem::Version
|
|
539
539
|
version: '0'
|
|
540
540
|
requirements: []
|
|
541
|
-
rubygems_version:
|
|
542
|
-
signing_key:
|
|
541
|
+
rubygems_version: 4.0.16
|
|
543
542
|
specification_version: 4
|
|
544
543
|
summary: High-performance HTML5 parser, with support for both CSS selectors and XPath.
|
|
545
544
|
test_files: []
|