leptris 1.1.2 → 1.2.1

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: 949fe73711448bd7857072004c75d6201b06829e9eb632124ae922a2fb7ff3f1
4
- data.tar.gz: 259bbac7ce3e90389cce4f0e79dc979e7bc8ca844caa7745680ba352c010ba86
3
+ metadata.gz: e9d9e951ee8e7b15451e02933c6e5e80d6cb78fa970ff4411153ec1358c50ab1
4
+ data.tar.gz: 5aab53987963478c27bb0d9bfe96e804672dcc6b7da1842cc3422f9e0b91c235
5
5
  SHA512:
6
- metadata.gz: cdab285d1fe462fcf7bc66681ebfbe1af74748ae99a8f0eebe1e502dfd72313b4429ac5943a18ebebd5706a308b135c33dfd7e8aab4bd313e383adcd91bb682f
7
- data.tar.gz: 18240f20ed093b9e6434d0f9036ab93545b6af47237aa5ee72f616f3b10836148a3850fec5332fb2e44f416a53d25523d69f8bc4e4e1bb17c1f1181f16515f98
6
+ metadata.gz: 7de27428d6e469b405889481a78bdbe1680d28e4b8c4bd2041ccfb49dd3b9a620359f5d66a4bc4f4573e16c332916348cf4313bedbb1c5d1876cf08cb64b243e
7
+ data.tar.gz: c71f63da347773adfd341782dd10326ff5993c673080e63554013eed453137014f76f2b4a0474b9fa452c5009f573d4d65f01213822016e296735a0a1eea30e3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,56 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.1] - 2026-08-23
9
+
10
+ Completes the libleptris 1.2.0 public surface in the binding.
11
+
12
+ ### Added
13
+
14
+ - **Namespace-bound XPath**: `doc.xpath("//p:title", "p" => "urn:p")`
15
+ (Nokogiri parity) — expression prefixes now resolve to caller-supplied
16
+ URIs regardless of the prefixes the document declared, via
17
+ `leptris_xpath_ns_set_new/_add/_free` + `leptris_xpath_eval_ns`. The
18
+ binding set is built per call and freed under `ensure`. Previously
19
+ the trailing namespace hash was silently ignored.
20
+ - `leptris_error_message` / `leptris_last_error` bound; the status seam
21
+ and parse errors now append the library's last-error detail when
22
+ present (best-effort: it is a library-global string).
23
+
24
+ ## [1.2.0] - 2026-08-23
25
+
26
+ Lockstep with libleptris 1.2.0.
27
+
28
+ ### Added
29
+
30
+ - `Leptris::XML::Document.create` — empty document with its own memory
31
+ pool (backs `leptris_document_create`); elements are created against
32
+ it via the factories and attached with `root=`.
33
+ - `Document#root=` — attach an element as the document root
34
+ (`leptris_document_set_root`). Companion to the moxml Leptris
35
+ adapter (lutaml/moxml#96).
36
+ - `Attr#to_xml` — serialized `name="value"` form with the five XML
37
+ special characters escaped.
38
+
39
+ ### Fixed
40
+
41
+ - `Node#text` / `#inner_text` now dispatch to `#content` instead of
42
+ aliasing the base implementation — the alias snapshot meant subclass
43
+ overrides (`Text#content`, `Comment#content`, …) were never seen
44
+ through `text`.
45
+ - `Element#namespace` carries the element's own prefix, so consumers
46
+ can distinguish `{"p" => "urn:p"}` from the default namespace.
47
+ - CI/Rakefile pin libleptris v1.2.0, whose fixes resolve leptris#477
48
+ (mixed-nodeset kinds, `node_name`/`node_value` on non-element
49
+ entries, and `//node()` now including the context root). NodeSet
50
+ keeps its per-index fallback for the still-under-copying batch
51
+ accessor.
52
+
53
+ ### Unbound v1.2.0 surface (follow-up)
54
+
55
+ Namespace-bound XPath (`leptris_xpath_ns_set_*`, `leptris_xpath_eval_ns`)
56
+ and error introspection (`leptris_error_message`, `leptris_last_error`).
57
+
8
58
  ## [1.1.1] - 2026-08-22
9
59
 
10
60
  ### Added
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.1.1"
11
+ LIBLEPTRIS_VERSION = "1.2.0"
12
12
 
13
13
  CMAKE_FLAGS = %w[
14
14
  -DCMAKE_BUILD_TYPE=Release
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.1.2"
4
+ VERSION = "1.2.1"
5
5
  end
@@ -29,6 +29,17 @@ class Leptris::XML::Attr
29
29
  def to_s; @value; end
30
30
  def to_str; @value; end
31
31
 
32
+ # Serialized attribute form: name="value" with the five XML special
33
+ # characters escaped in the value.
34
+ def to_xml
35
+ escaped = @value.to_s.gsub("&", "&")
36
+ .gsub("<", "&lt;")
37
+ .gsub(">", "&gt;")
38
+ .gsub('"', "&quot;")
39
+ .gsub("'", "&apos;")
40
+ "#{@name}=\"#{escaped}\""
41
+ end
42
+
32
43
  def ==(other)
33
44
  other.is_a?(Leptris::XML::Attr) &&
34
45
  @name == other.name && @value == other.value &&
@@ -50,7 +50,8 @@ class Leptris::XML::Document
50
50
  if raw.null?
51
51
  status = status_ptr.read_int
52
52
  raise Leptris::XML::ParseError,
53
- "leptris_parse_string failed (status=#{status})"
53
+ "leptris_parse_string failed (status=#{status}): " +
54
+ Leptris::XML::FFI.status_message(status)
54
55
  end
55
56
  wrap(raw)
56
57
  end
@@ -66,6 +67,16 @@ class Leptris::XML::Document
66
67
  wrap(raw)
67
68
  end
68
69
 
70
+ # Create an empty document (no root element) backed by its own memory
71
+ # pool. Elements for the tree are created against it via
72
+ # #create_element and friends, then attached with #root=.
73
+ def self.create
74
+ raw = Leptris::XML::FFI.leptris_document_create
75
+ raise Leptris::XML::Error,
76
+ "leptris_document_create failed" if raw.null?
77
+ wrap(raw)
78
+ end
79
+
69
80
  # Convert a raw LeptrisDocument pointer into a Ruby Document with safe
70
81
  # GC lifetime management. The finalizer captures the raw address
71
82
  # integer (not the Document or Pointer object — those would prevent
@@ -107,6 +118,17 @@ class Leptris::XML::Document
107
118
  Leptris::XML::Node.wrap(ptr, self)
108
119
  end
109
120
 
121
+ # Attach +element+ as the document's root element. The element must
122
+ # have been created against this document and must not already have
123
+ # a parent. Any previous root is left detached (still owned by the
124
+ # document's pool until #free).
125
+ def root=(element)
126
+ raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
127
+ Leptris::XML::FFI.check_status(
128
+ Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
129
+ element
130
+ end
131
+
110
132
  def create_element(name)
111
133
  ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
112
134
  raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
@@ -191,7 +191,12 @@ class Leptris::XML::Element < Leptris::XML::Node
191
191
  def namespace
192
192
  uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
193
193
  return nil if uri.nil? || uri.empty?
194
- Leptris::XML::Namespace.new(self, uri)
194
+ # The resolved namespace is reached via this element's own prefix,
195
+ # so carry it through: consumers that distinguish
196
+ # {"p" => "urn:p"} from the default {"nil => "urn:p"} need it.
197
+ prefix = Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
198
+ prefix = nil if prefix.nil? || prefix.empty?
199
+ Leptris::XML::Namespace.new(self, uri, prefix: prefix)
195
200
  end
196
201
 
197
202
  def namespace_definitions
@@ -23,6 +23,7 @@ module Leptris
23
23
  typedef :pointer, :leptris_attribute
24
24
  typedef :pointer, :leptris_doctype
25
25
  typedef :pointer, :leptris_xpath_result
26
+ typedef :pointer, :leptris_xpath_ns_set
26
27
  typedef :pointer, :leptris_xpath_var_set
27
28
  typedef :pointer, :leptris_sax_parser
28
29
  typedef :int, :leptris_status
@@ -66,6 +67,10 @@ module Leptris
66
67
  [:string, :pointer], :pointer
67
68
  attach_function :leptris_document_free,
68
69
  [:leptris_document], :void
70
+ attach_function :leptris_document_create,
71
+ [], :leptris_document
72
+ attach_function :leptris_document_set_root,
73
+ [:leptris_document, :leptris_element], :leptris_status
69
74
  attach_function :leptris_document_root,
70
75
  [:leptris_document], :leptris_element
71
76
  attach_function :leptris_document_encoding,
@@ -361,6 +366,18 @@ module Leptris
361
366
  attach_function :leptris_xpath_variable_set_string,
362
367
  [:leptris_xpath_var_set, :string, :string], :leptris_status
363
368
 
369
+ # Namespace-bound XPath (v1.2.0): expression prefixes resolve to
370
+ # caller-supplied URIs regardless of the document's declarations.
371
+ attach_function :leptris_xpath_ns_set_new,
372
+ [], :leptris_xpath_ns_set
373
+ attach_function :leptris_xpath_ns_set_free,
374
+ [:leptris_xpath_ns_set], :void
375
+ attach_function :leptris_xpath_ns_set_add,
376
+ [:leptris_xpath_ns_set, :string, :string], :leptris_status
377
+ attach_function :leptris_xpath_eval_ns,
378
+ [:leptris_document, :leptris_element, :string, :leptris_xpath_ns_set],
379
+ :leptris_xpath_result
380
+
364
381
  attach_function :leptris_sax_parse,
365
382
  [:string, :size_t, :pointer, :pointer], :int
366
383
  attach_function :leptris_sax_parser_create,
@@ -415,6 +432,8 @@ module Leptris
415
432
  attach_function :leptris_document_set_allocators,
416
433
  [:leptris_document, :pointer, :pointer], :leptris_status
417
434
  attach_function :leptris_status_string, [:leptris_status], :string
435
+ attach_function :leptris_error_message, [:leptris_status], :string
436
+ attach_function :leptris_last_error, [], :string
418
437
 
419
438
  LEPTRIS_OK = 0
420
439
  LEPTRIS_ERROR_MEMORY = -1
@@ -463,13 +482,22 @@ module Leptris
463
482
  ptr.read_string.tap { leptris_free_string(ptr) }
464
483
  end
465
484
 
466
- # Single status seam: turns a C status code into a Ruby error.
485
+ # Single status seam: turns a C status code into a Ruby error,
486
+ # appending the library's last-error detail when present. The
487
+ # detail is a library-global string, so it can only be treated
488
+ # as best-effort context.
467
489
  def self.check_status(status)
468
490
  unless status == LEPTRIS_OK
469
- raise Leptris::XML::Error, leptris_status_string(status)
491
+ raise Leptris::XML::Error, status_message(status)
470
492
  end
471
493
  status
472
494
  end
495
+
496
+ def self.status_message(status)
497
+ base = leptris_status_string(status).to_s
498
+ detail = leptris_last_error.to_s
499
+ detail.empty? ? base : "#{base} (#{detail})"
500
+ end
473
501
  end
474
502
  end
475
503
  end
@@ -45,8 +45,17 @@ class Leptris::XML::Node
45
45
  def content
46
46
  raise NotImplementedError, "#{self.class}#content not implemented"
47
47
  end
48
- alias_method :text, :content
49
- alias_method :inner_text, :content
48
+
49
+ # Dispatching defs, not alias_method: an alias snapshots this base
50
+ # #content (the raise), so subclass overrides would never be seen
51
+ # through the alias. A plain method resolves #content per-call.
52
+ def text
53
+ content
54
+ end
55
+
56
+ def inner_text
57
+ content
58
+ end
50
59
 
51
60
  def type
52
61
  Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
@@ -2,14 +2,19 @@
2
2
 
3
3
  module Leptris::XML::Searchable
4
4
  def xpath(*paths)
5
- handler, _ns, _vars = parse_search_args(paths)
5
+ handler, ns, _vars = parse_search_args(paths)
6
6
  raise ArgumentError, "custom XPath handlers not supported" if handler
7
7
  expr = paths.join(" | ")
8
8
 
9
9
  doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
10
10
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
11
11
 
12
- result_ptr = Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
12
+ result_ptr =
13
+ if ns && !ns.empty?
14
+ xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
15
+ else
16
+ Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
17
+ end
13
18
  if result_ptr.null?
14
19
  raise Leptris::XML::XPathError,
15
20
  Leptris::XML::FFI.leptris_status_string(Leptris::XML::FFI::LEPTRIS_ERROR_XPATH)
@@ -50,6 +55,22 @@ module Leptris::XML::Searchable
50
55
 
51
56
  protected
52
57
 
58
+ # Builds a caller-owned namespace binding set, evaluates via
59
+ # leptris_xpath_eval_ns, and frees the set regardless of outcome.
60
+ def xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
61
+ set = Leptris::XML::FFI.leptris_xpath_ns_set_new
62
+ begin
63
+ ns.each do |prefix, uri|
64
+ Leptris::XML::FFI.check_status(
65
+ Leptris::XML::FFI.leptris_xpath_ns_set_add(
66
+ set, prefix.to_s, uri.to_s))
67
+ end
68
+ Leptris::XML::FFI.leptris_xpath_eval_ns(doc_ptr, context_ptr, expr, set)
69
+ ensure
70
+ Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
71
+ end
72
+ end
73
+
53
74
  def parse_search_args(args)
54
75
  handler = args.find { |a| !a.is_a?(String) && !a.is_a?(Hash) && !a.is_a?(Symbol) }
55
76
  args = args - [handler] if handler
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.