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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3615acd25f6233c5701c91d25ef6c94a73adf2de8433e2e1b6b3aa215f0581ab
4
- data.tar.gz: ad3e9f8134a7f26924b2283a2ac10a8dfae2f5684dd650be5903faf069f595ef
3
+ metadata.gz: f479dae3741e343ff292ef23b69fb31697f149b1e799b5eed26c2626ff5b9909
4
+ data.tar.gz: 35e0a468a1c9c94283271e66abd98663ce4ad9699613c4dea70df82f4d91e99b
5
5
  SHA512:
6
- metadata.gz: 2d84f341a6b3e1c4dcc8501ab9772e4376bc8c9acf2441753253c331ca381ff161c95cbb93f32568aada6ac03e3bedb01343bbc507d5201c468884f286008228
7
- data.tar.gz: f8a85acb4fdd063714316490289bae356a731931aa8c4202a80f677b70d2f02258f69991e0d996cfaadf80dbcad8c505c61b4a6a91456514bbc972e0d7861148
6
+ metadata.gz: 1b7c6cb9015d9b4540fb79e3fb04afb484a072f914f2480caa65f81fb66a077c099b8cbc81026aa9c8f1b4fc960ce57d9d2c17e1b1982de0f85a70c35e77cff9
7
+ data.tar.gz: 6bc107899aae62a756a84944579442fe482cece4375b49bd59a1061525d5e9a590e74308a08758b0effb6190c0f6f9920db5f6bceef43bf1caa4e8c413e68aca
@@ -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
- lxb_status_t status = lxb_dom_node_text_content_set(node, (const lxb_char_t *)c_content, content_len);
241
- if (status != LXB_STATUS_OK) {
242
- nl_raise_lexbor_error(status);
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(node, 0, 0, &str);
598
+ status = lxb_html_serialize_pretty_deep_str(serialization_root, 0, 0, &str);
578
599
  } else {
579
- status = lxb_html_serialize_deep_str(node, &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), LXB_TAG__UNDEF, &tag_name_len);
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
  }
@@ -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->node.parent);
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->node.parent);
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->parent;
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->parent;
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->parent;
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
- return (lxb_dom_node_t_ptr)lxb_dom_element_first_attribute((lxb_dom_element_t *)ctxt->context->node);
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 (lxb_dom_node_t_ptr)(((lxb_dom_attr_t *)cur)->next);
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nokolexbor
4
- VERSION = '0.6.4'
4
+ VERSION = '0.8.0'
5
5
  end
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 HTML(*args)
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.6.4
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-03-23 00:00:00.000000000 Z
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: 3.1.6
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: []