leptris 1.9.121.0 → 1.9.144.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 +4 -4
- data/CHANGELOG.md +87 -0
- data/README.adoc +15 -0
- data/Rakefile +1 -1
- data/TODO.restructure/21-typeddata-variant-prototype.md +4 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/diff.rb +74 -0
- data/lib/leptris/xml/document.rb +27 -7
- data/lib/leptris/xml/element.rb +45 -2
- data/lib/leptris/xml/ffi.rb +70 -0
- data/lib/leptris/xml/node.rb +2 -3
- data/lib/leptris/xml/schematron.rb +88 -0
- data/lib/leptris/xml/searchable.rb +42 -0
- data/lib/leptris/xml/xquery.rb +21 -3
- data/lib/leptris/xml.rb +10 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '01429a81e019ba9e4ecf9d2a7c23ada3b4d2c6dc3849c5a0be469bbab52b5c92'
|
|
4
|
+
data.tar.gz: 21bc4d37174f592fe72821f9fd7074eb7521c7ff4eacad42141720f2e235c97b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 181136f9e8da6a067bba876bbbfe5e841c062470fce5c6a521de413f5abd4a33d824eb387155021b486ff5063e70d96077156b79656440ee418d4b388bc5297c
|
|
7
|
+
data.tar.gz: 3e088d8220ec7c3c40f4e9869caf8cc73e08606ac4f7035a9f8a277454fafc1d7921c11fe17aa64f4c328254362103962bf05218385e1855c27fad3663b5b915
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,93 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.144.1] - 2026-09-12
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **#161 (DOM parity for engine porters)**:
|
|
13
|
+
- `Node#dup` works for every node kind — text, comment, CDATA,
|
|
14
|
+
and PI rebuild by value through the document factories behind
|
|
15
|
+
the same `Document.copy_of` seam (elements keep the C deep
|
|
16
|
+
copy); previously `dup` raised "only supported for element
|
|
17
|
+
nodes".
|
|
18
|
+
- `Element#[]` and `#key?` read back qualified attribute names
|
|
19
|
+
the namespace-aware engine lookup misses (a value written as
|
|
20
|
+
`el["n:x"] = "9"` with an undeclared prefix now answers
|
|
21
|
+
`el["n:x"]` — the written-name face `#attributes` exposes);
|
|
22
|
+
declared-prefix reads are unchanged.
|
|
23
|
+
- `Element#qualified_name` — the written spelling ("p:c" /
|
|
24
|
+
"c") behind the `#name`/`#prefix` split.
|
|
25
|
+
- `to_xml(indent:)` was already documented in the README
|
|
26
|
+
serialization section — no change needed.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **XPATH_FUNCTION result mirror** (PR #127, libleptris TODO 07
|
|
31
|
+
lane): the type-4 constant is attached and `wrap_xpath_result`
|
|
32
|
+
raises an explicit, actionable error when an XPath 3.0 function
|
|
33
|
+
item would cross the FFI boundary (`concat#2`) instead of the
|
|
34
|
+
generic "unknown xpath result type 4".
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
|
|
38
|
+
- **#160**: README "Alternative engines" section — the plain
|
|
39
|
+
`ruby`-platform gem ships with every release, so TruffleRuby and
|
|
40
|
+
JRuby activate leptris via FFI; the section documents the
|
|
41
|
+
system-library / `LEPTRIS_LIB_PATH` path those engines need.
|
|
42
|
+
|
|
43
|
+
## [1.9.144.0] - 2026-09-12
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **Lockstep with libleptris 1.9.144** (1.9.121 → 1.9.144; audit
|
|
48
|
+
270 → 289 after attaching 19 new symbols).
|
|
49
|
+
- **`Leptris::XML::Schematron`** — ISO Schematron validation
|
|
50
|
+
(libleptris 1.9.127/1.9.128; schematron-conformance corpus
|
|
51
|
+
50/50): `Schematron.parse(schema_xml, phase: nil)` / `.parse_file`
|
|
52
|
+
compile once, then `#valid?(doc)` answers the boolean and
|
|
53
|
+
`#validate(doc)` returns the SVRL report as a queryable
|
|
54
|
+
`Document` (failed-assert / successful-report with @location).
|
|
55
|
+
- **`Leptris::XML::Diff`** — native tree diff (libleptris
|
|
56
|
+
1.9.127, namespace-aware 1.9.129): `Leptris::XML.diff(a, b,
|
|
57
|
+
ignore_ws: false)` → `#ops` (`{type:, name:, path:, before:,
|
|
58
|
+
after:}`; insert/delete/update_text/update_attr with
|
|
59
|
+
positional `/name[i]` paths) and `#to_s` (the serialized op
|
|
60
|
+
list); `Diff.identical?(a, b, ignore_ws:)` answers the boolean
|
|
61
|
+
fast path.
|
|
62
|
+
- **XQuery external variables** (libleptris 1.9.133) —
|
|
63
|
+
`XQuery#eval(doc, params)` binds `declare variable $x external`
|
|
64
|
+
through `leptris_xquery_eval_params` with QT3 `<param select>`
|
|
65
|
+
semantics (each value is an XPath expression in an empty
|
|
66
|
+
context); a binding overrides a default initializer, and an
|
|
67
|
+
empty params hash takes the same lane — the engine decides
|
|
68
|
+
between the default and failure.
|
|
69
|
+
- **`Element#expanded_name`** (libleptris 1.9.144) —
|
|
70
|
+
`{local:, prefix:, namespace_uri:}` in one C call.
|
|
71
|
+
- **`#xpath_versioned(expression, version)`** — version-pinned
|
|
72
|
+
evaluation via `leptris_xpath_eval_versioned`: `:xpath10`
|
|
73
|
+
keeps the strict 1.0 surface (3.x syntax raises), `:xpath31`
|
|
74
|
+
evaluates the full grammar. A separate method by design —
|
|
75
|
+
`#xpath`'s trailing-hash argument is the namespace-binding
|
|
76
|
+
channel.
|
|
77
|
+
- **`Node#digest(drop_ws:)`** — the #869 content-defined Merkle
|
|
78
|
+
digest behind diff pruning.
|
|
79
|
+
- **Serialization: `to_xml(..., expand_empty:)`** — the
|
|
80
|
+
`<e></e>` vs `<e/>` switch at the C seam.
|
|
81
|
+
- Engine arc: the W3C QT3 suite adoption (fn/substring 45/45,
|
|
82
|
+
fn/contains 41/41, fn/starts-with 43/43, fn/ends-with 34/34,
|
|
83
|
+
fn/concat 80/80 — int64 integer fidelity keeps
|
|
84
|
+
`xs:integer('999999999999999999')` exact) and the WHATWG HTML
|
|
85
|
+
completion arc (frameset, in-head-noscript, character
|
|
86
|
+
references, comment close forms, **full adoption agency**
|
|
87
|
+
1.9.137 — `"<h1>a<h2>b</h1>c</h2>"` now yields
|
|
88
|
+
`<h1>a</h1><h2>b</h2>c` per the spec — initial-mode comment
|
|
89
|
+
placement, in-head comments, foreign-content integration
|
|
90
|
+
scopes, in-table clear-stack, script-data escapes, RCDATA/
|
|
91
|
+
rawtext, numeric-reference end states; html5lib floor
|
|
92
|
+
928 → 933). #965: relational comparisons no longer swap
|
|
93
|
+
operands (`10 >= @n` compares as written).
|
|
94
|
+
|
|
8
95
|
## [1.9.115.0] - 2026-09-09
|
|
9
96
|
|
|
10
97
|
### Changed
|
data/README.adoc
CHANGED
|
@@ -60,6 +60,21 @@ export LEPTRIS_LIB_PATH=/usr/local/lib/libleptris.dylib
|
|
|
60
60
|
If Leptris can't find the library at startup, every `parse` call raises
|
|
61
61
|
`LoadError`.
|
|
62
62
|
|
|
63
|
+
=== Alternative engines: TruffleRuby and JRuby (#160)
|
|
64
|
+
|
|
65
|
+
The gem ships a plain `ruby`-platform variant alongside the precompiled
|
|
66
|
+
platform gems, so Bundler activates `leptris` on TruffleRuby and JRuby
|
|
67
|
+
(the binding is FFI-based — no MRI C extension anywhere). Those engines
|
|
68
|
+
filter out precompiled platform gems at activation and always resolve
|
|
69
|
+
the `ruby` variant, which does not vendor the native library:
|
|
70
|
+
|
|
71
|
+
1. Install `libleptris` on the host — build from source (above) or set
|
|
72
|
+
`LEPTRIS_LIB_PATH=/path/to/libleptris.{so,dylib}`.
|
|
73
|
+
2. `gem install leptris` (or `bundle install`) — activation succeeds
|
|
74
|
+
like on MRI, and FFI loads the system library at require time.
|
|
75
|
+
|
|
76
|
+
The startup `LoadError` names this remedy when the library is missing.
|
|
77
|
+
|
|
63
78
|
== Parsing
|
|
64
79
|
|
|
65
80
|
The top-level entry point is `Leptris::XML`. Parse a string or an IO:
|
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.9.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.144"
|
|
12
12
|
# Vendored alongside libleptris for fn:normalize-unicode (TODO
|
|
13
13
|
# .restructure/20): built per platform with a RELOCATABLE @rpath
|
|
14
14
|
# install name, loaded by ffi.rb before libleptris so the
|
|
@@ -8,6 +8,10 @@ in verification order:
|
|
|
8
8
|
- [x] Minimal C ext (one file): a TypedData class holding
|
|
9
9
|
(pointer, document-ref) — prototype built and measured
|
|
10
10
|
(ext + bench preserved under /tmp/typedproto, /tmp/bench_typed.rb).
|
|
11
|
+
- [x] NOTE (2026-09-12): /tmp was wiped — the prototype ext and
|
|
12
|
+
bench under /tmp/typedproto, /tmp/bench_typed.rb are GONE.
|
|
13
|
+
The measurements below stand (recorded here); the build
|
|
14
|
+
phase rebuilds the ext properly under ext/ anyway.
|
|
11
15
|
- [x] Measured (100k holders, GC.settled RSS deltas):
|
|
12
16
|
FFI::Pointer-backed 154 B/node vs TypedData 40 B/node —
|
|
13
17
|
**114 B/node saved**, ~40% of the wrapper layer (#147's
|
data/lib/leptris/version.rb
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
# Tree diff between two documents (libleptris >= 1.9.126):
|
|
6
|
+
# equal subtrees prune in O(1) via the #869 content-defined Merkle
|
|
7
|
+
# digest; diverging regions align through an LCS on child digests,
|
|
8
|
+
# then same-named elements recurse. Attribute add/remove are
|
|
9
|
+
# UPDATE_ATTR ops with an empty side.
|
|
10
|
+
#
|
|
11
|
+
# diff = Leptris::XML.diff(before_doc, after_doc)
|
|
12
|
+
# diff.ops # => [{type: :update_text, name: "price",
|
|
13
|
+
# # path: "/r/item[2]/price", before: "3", after: "4"}, ...]
|
|
14
|
+
# diff.to_s # => serialized op list
|
|
15
|
+
# Leptris::XML.diff(a, b, ignore_ws: true) # ws-only text absent
|
|
16
|
+
#
|
|
17
|
+
class Leptris::XML::Diff
|
|
18
|
+
class Handle < ::FFI::AutoPointer
|
|
19
|
+
def self.release(ptr)
|
|
20
|
+
Leptris::XML::FFI.leptris_diff_free(ptr)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
OP_TYPES = {
|
|
25
|
+
1 => :insert,
|
|
26
|
+
2 => :delete,
|
|
27
|
+
3 => :update_text,
|
|
28
|
+
4 => :update_attr,
|
|
29
|
+
}.freeze
|
|
30
|
+
private_constant :OP_TYPES
|
|
31
|
+
|
|
32
|
+
def self.compute(document_a, document_b, ignore_ws: false)
|
|
33
|
+
flags = ignore_ws ? 1 : 0
|
|
34
|
+
raw = Leptris::XML::FFI.leptris_diff(
|
|
35
|
+
document_a.c_ptr, document_b.c_ptr, flags, nil)
|
|
36
|
+
if raw.null?
|
|
37
|
+
raise Leptris::XML::Error,
|
|
38
|
+
"leptris_diff failed: #{Leptris::XML::FFI.leptris_last_error}"
|
|
39
|
+
end
|
|
40
|
+
new(Handle.new(raw))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The boolean fast path canon-style consumers want: are the two
|
|
44
|
+
# trees structurally identical? Zero ops == identical — the
|
|
45
|
+
# digest pruning makes the common equal case O(1) per subtree.
|
|
46
|
+
def self.identical?(document_a, document_b, ignore_ws: false)
|
|
47
|
+
compute(document_a, document_b, ignore_ws: ignore_ws).ops.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize(handle)
|
|
51
|
+
@handle = handle
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The op list in document order: {type:, name:, path:, before:,
|
|
55
|
+
# after:} — paths are root-relative ("/r/i[2]"); before/after
|
|
56
|
+
# carry payloads (attribute values / text) where the op has them.
|
|
57
|
+
def ops
|
|
58
|
+
count = Leptris::XML::FFI.leptris_diff_op_count(@handle)
|
|
59
|
+
Array.new(count) do |i|
|
|
60
|
+
{
|
|
61
|
+
type: OP_TYPES[Leptris::XML::FFI.leptris_diff_op_type(@handle, i)],
|
|
62
|
+
name: Leptris::XML::FFI.leptris_diff_op_name(@handle, i),
|
|
63
|
+
path: Leptris::XML::FFI.leptris_diff_op_path(@handle, i),
|
|
64
|
+
before: Leptris::XML::FFI.leptris_diff_op_before(@handle, i),
|
|
65
|
+
after: Leptris::XML::FFI.leptris_diff_op_after(@handle, i),
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_s
|
|
71
|
+
str_ptr = Leptris::XML::FFI.leptris_diff_serialize(@handle)
|
|
72
|
+
Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -119,18 +119,38 @@ class Leptris::XML::Document
|
|
|
119
119
|
wrap(raw)
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
# Deep copy of +
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
122
|
+
# Deep copy of +node+ in a NEW document — the one authority
|
|
123
|
+
# behind Node#dup / Element#dup and the indent-unit path.
|
|
124
|
+
# Elements deep-copy through the C copier (every child kind and
|
|
125
|
+
# namespace survives, #696/#721/#812); every other node kind
|
|
126
|
+
# rebuilds by value through this document's factories — their
|
|
127
|
+
# entire state is the payload the factory takes (#161). The
|
|
128
|
+
# returned node is detached from the original and fully usable.
|
|
129
|
+
def self.copy_of(node)
|
|
128
130
|
new_doc = create
|
|
129
|
-
|
|
131
|
+
return rebuild_node(node, new_doc) unless node.element?
|
|
132
|
+
copy = Leptris::XML::FFI.leptris_element_copy(node.c_ptr, new_doc.c_ptr)
|
|
130
133
|
raise Leptris::XML::Error, "leptris_element_copy failed" if copy.null?
|
|
131
134
|
new_doc.root = Leptris::XML::Node.wrap(copy, new_doc)
|
|
132
135
|
end
|
|
133
136
|
|
|
137
|
+
def self.rebuild_node(node, new_doc)
|
|
138
|
+
case node.type
|
|
139
|
+
when Leptris::XML::FFI::NODE_TEXT
|
|
140
|
+
new_doc.create_text_node(node.content)
|
|
141
|
+
when Leptris::XML::FFI::NODE_CDATA
|
|
142
|
+
new_doc.create_cdata(node.content)
|
|
143
|
+
when Leptris::XML::FFI::NODE_COMMENT
|
|
144
|
+
new_doc.create_comment(node.content)
|
|
145
|
+
when Leptris::XML::FFI::NODE_PI
|
|
146
|
+
new_doc.create_processing_instruction(node.name, node.content)
|
|
147
|
+
else
|
|
148
|
+
raise Leptris::XML::Error,
|
|
149
|
+
"dup is not supported for #{node.class} nodes"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
private_class_method :rebuild_node
|
|
153
|
+
|
|
134
154
|
# Convert a raw LeptrisDocument pointer into a Ruby Document with safe
|
|
135
155
|
# GC lifetime management. The finalizer captures the raw address
|
|
136
156
|
# integer (not the Document or Pointer object — those would prevent
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -79,7 +79,13 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
79
79
|
return v
|
|
80
80
|
end
|
|
81
81
|
ensure_alive!
|
|
82
|
-
Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
|
|
82
|
+
v = Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
|
|
83
|
+
# Namespace-aware misses (an undeclared prefix never resolves
|
|
84
|
+
# through in-scope declarations) fall back to the WRITTEN name
|
|
85
|
+
# — the same flat face #attributes exposes, so a value written
|
|
86
|
+
# as el["n:x"] reads back through el["n:x"] (#161).
|
|
87
|
+
return v unless v.nil?
|
|
88
|
+
attributes[name]&.value
|
|
83
89
|
end
|
|
84
90
|
alias_method :attr, :[]
|
|
85
91
|
alias_method :get_attribute, :[]
|
|
@@ -94,7 +100,11 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
94
100
|
|
|
95
101
|
def key?(name)
|
|
96
102
|
ensure_alive!
|
|
97
|
-
Leptris::XML::FFI.leptris_element_has_attribute(
|
|
103
|
+
return true if Leptris::XML::FFI.leptris_element_has_attribute(
|
|
104
|
+
@c_ptr, name.to_s) != 0
|
|
105
|
+
# Same written-name fallback as #[] — the engine lookup is
|
|
106
|
+
# namespace-aware and misses undeclared prefixes (#161).
|
|
107
|
+
attributes.key?(name.to_s)
|
|
98
108
|
end
|
|
99
109
|
alias_method :has_attribute?, :key?
|
|
100
110
|
|
|
@@ -196,6 +206,15 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
196
206
|
Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
|
|
197
207
|
end
|
|
198
208
|
|
|
209
|
+
# The written qualified name — "foo:child" with a prefix, the
|
|
210
|
+
# bare local name without. The explicit contract behind the
|
|
211
|
+
# #name/#prefix split: #name answers the local part, the
|
|
212
|
+
# serialized form uses this spelling (#161).
|
|
213
|
+
def qualified_name
|
|
214
|
+
p = prefix
|
|
215
|
+
p ? "#{p}:#{name}" : name
|
|
216
|
+
end
|
|
217
|
+
|
|
199
218
|
def prepend_child(node)
|
|
200
219
|
ensure_writable!
|
|
201
220
|
Leptris::XML::FFI.check_status(
|
|
@@ -341,6 +360,30 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
341
360
|
uri
|
|
342
361
|
end
|
|
343
362
|
|
|
363
|
+
# The local name, prefix, and resolved namespace URI in ONE
|
|
364
|
+
# call (libleptris 1.9.144) — the adapter fan-out read.
|
|
365
|
+
# Equivalent to #name + #prefix + #namespace, single entry.
|
|
366
|
+
def expanded_name
|
|
367
|
+
ensure_alive!
|
|
368
|
+
local = ::FFI::MemoryPointer.new(:pointer)
|
|
369
|
+
prefix = ::FFI::MemoryPointer.new(:pointer)
|
|
370
|
+
uri = ::FFI::MemoryPointer.new(:pointer)
|
|
371
|
+
Leptris::XML::FFI.leptris_element_expanded_name(
|
|
372
|
+
@c_ptr, local, prefix, uri)
|
|
373
|
+
local_ptr = local.read_pointer
|
|
374
|
+
prefix_ptr = prefix.read_pointer
|
|
375
|
+
uri_ptr = uri.read_pointer
|
|
376
|
+
result = {
|
|
377
|
+
local: local_ptr.read_string,
|
|
378
|
+
prefix: prefix_ptr.null? ? nil : prefix_ptr.read_string,
|
|
379
|
+
namespace_uri: uri_ptr.null? ? nil : uri_ptr.read_string,
|
|
380
|
+
}
|
|
381
|
+
local.free
|
|
382
|
+
prefix.free
|
|
383
|
+
uri.free
|
|
384
|
+
result
|
|
385
|
+
end
|
|
386
|
+
|
|
344
387
|
def namespace_definitions
|
|
345
388
|
return @namespace_definitions if memo_hit?(@namespace_definitions_version)
|
|
346
389
|
ensure_alive!
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -64,6 +64,8 @@ module Leptris
|
|
|
64
64
|
typedef :pointer, :leptris_relaxng
|
|
65
65
|
typedef :pointer, :leptris_xslt
|
|
66
66
|
typedef :pointer, :leptris_xquery
|
|
67
|
+
typedef :pointer, :leptris_schematron
|
|
68
|
+
typedef :pointer, :leptris_diff
|
|
67
69
|
typedef :pointer, :leptris_sax_recorder
|
|
68
70
|
typedef :pointer, :leptris_pull_parser
|
|
69
71
|
typedef :pointer, :leptris_iterparse
|
|
@@ -744,8 +746,72 @@ attach_function :leptris_parse_string,
|
|
|
744
746
|
attach_function :leptris_xquery_eval,
|
|
745
747
|
[:leptris_xquery, :leptris_document, :leptris_element],
|
|
746
748
|
:leptris_xpath_result
|
|
749
|
+
# External-variable XQuery evaluation (libleptris 1.9.133):
|
|
750
|
+
# names[i]/selects[i] bind `declare variable $name external`
|
|
751
|
+
# — each select is an XPath expression in an empty context
|
|
752
|
+
# (QT3 <param select> semantics), overriding any default
|
|
753
|
+
# initializer. An external with neither binding nor default
|
|
754
|
+
# fails evaluation.
|
|
755
|
+
attach_function :leptris_xquery_eval_params,
|
|
756
|
+
[:leptris_xquery, :leptris_document, :leptris_element,
|
|
757
|
+
:pointer, :pointer, :size_t], :leptris_xpath_result
|
|
747
758
|
attach_function :leptris_xquery_free, [:leptris_xquery], :void
|
|
748
759
|
|
|
760
|
+
# Schematron validation (libleptris >= 1.9.126 family):
|
|
761
|
+
# parse once (optionally selecting a PHASE), validate any
|
|
762
|
+
# number of documents — #validate returns the SVRL report as
|
|
763
|
+
# a Document; #valid? answers through the SVRL root name.
|
|
764
|
+
attach_function :leptris_schematron_parse,
|
|
765
|
+
[:string, :size_t, :pointer], :leptris_schematron
|
|
766
|
+
attach_function :leptris_schematron_parse_file,
|
|
767
|
+
[:string, :pointer], :leptris_schematron
|
|
768
|
+
attach_function :leptris_schematron_parse_phase,
|
|
769
|
+
[:string, :size_t, :string, :pointer], :leptris_schematron
|
|
770
|
+
attach_function :leptris_schematron_free,
|
|
771
|
+
[:leptris_schematron], :void
|
|
772
|
+
attach_function :leptris_schematron_valid,
|
|
773
|
+
[:leptris_schematron, :leptris_document], :int
|
|
774
|
+
attach_function :leptris_schematron_validate,
|
|
775
|
+
[:leptris_schematron, :leptris_document], :leptris_document
|
|
776
|
+
attach_function :leptris_schematron_error,
|
|
777
|
+
[:leptris_schematron], :string
|
|
778
|
+
|
|
779
|
+
# Tree diff (libleptris >= 1.9.126 family): equal subtrees
|
|
780
|
+
# prune in O(1) by the #869 Merkle digest; diverging regions
|
|
781
|
+
# align via LCS on child digests, then recurse by name.
|
|
782
|
+
attach_function :leptris_diff,
|
|
783
|
+
[:leptris_document, :leptris_document, :int, :pointer],
|
|
784
|
+
:leptris_diff
|
|
785
|
+
attach_function :leptris_diff_free, [:leptris_diff], :void
|
|
786
|
+
attach_function :leptris_diff_op_count, [:leptris_diff], :size_t
|
|
787
|
+
attach_function :leptris_diff_op_type,
|
|
788
|
+
[:leptris_diff, :size_t], :int
|
|
789
|
+
attach_function :leptris_diff_op_name,
|
|
790
|
+
[:leptris_diff, :size_t], :string
|
|
791
|
+
attach_function :leptris_diff_op_path,
|
|
792
|
+
[:leptris_diff, :size_t], :string
|
|
793
|
+
attach_function :leptris_diff_op_before,
|
|
794
|
+
[:leptris_diff, :size_t], :string
|
|
795
|
+
attach_function :leptris_diff_op_after,
|
|
796
|
+
[:leptris_diff, :size_t], :string
|
|
797
|
+
attach_function :leptris_diff_serialize,
|
|
798
|
+
[:leptris_diff], :pointer
|
|
799
|
+
|
|
800
|
+
# One-call expanded name (libleptris 1.9.144): local name +
|
|
801
|
+
# prefix + resolved namespace URI in a single entry — the
|
|
802
|
+
# FFI-adapter fan-out read.
|
|
803
|
+
attach_function :leptris_element_expanded_name,
|
|
804
|
+
[:leptris_element, :pointer, :pointer, :pointer], :void
|
|
805
|
+
|
|
806
|
+
# Versioned XPath evaluation (libleptris lane 15): XPATH_10
|
|
807
|
+
# keeps the strict 1.0 surface (3.x syntax rejected with
|
|
808
|
+
# INVALID_ARG); XPATH_31 evaluates the full grammar.
|
|
809
|
+
attach_function :leptris_xpath_eval_versioned,
|
|
810
|
+
[:leptris_document, :leptris_element, :string, :int, :pointer],
|
|
811
|
+
:leptris_xpath_result
|
|
812
|
+
XPATH_10 = 1 # strict XPath 1.0: the 3.x surface is rejected
|
|
813
|
+
XPATH_31 = 3 # the full grammar (the default entry's surface)
|
|
814
|
+
|
|
749
815
|
# One-pass entity pre-scan (libleptris 1.9.62, leptris#745):
|
|
750
816
|
# 1 when the buffer holds a named entity that is not one of
|
|
751
817
|
# the five predefined XML entities (nor numeric) — the moxml
|
|
@@ -843,6 +909,10 @@ attach_function :leptris_parse_string,
|
|
|
843
909
|
XPATH_BOOLEAN = 1
|
|
844
910
|
XPATH_NUMBER = 2
|
|
845
911
|
XPATH_STRING = 3
|
|
912
|
+
# XPath 3.0 function item (libleptris TODO 07 lane): a closure
|
|
913
|
+
# or named function reference. Not callable across the FFI
|
|
914
|
+
# boundary yet — wrap_xpath_result raises explicitly.
|
|
915
|
+
XPATH_FUNCTION = 4
|
|
846
916
|
|
|
847
917
|
XPATH_NODE_ELEMENT = 0
|
|
848
918
|
XPATH_NODE_ATTRIBUTE = 1
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -430,11 +430,10 @@ class Leptris::XML::Node
|
|
|
430
430
|
|
|
431
431
|
# Deep copy in a NEW document via Document.copy_of (the single
|
|
432
432
|
# copy seam — comment/PI children and namespaces both survive,
|
|
433
|
-
# #696/#721/#812; the copier is pool-threaded, ~2.3x Nokogiri
|
|
433
|
+
# #696/#721/#812; the copier is pool-threaded, ~2.3x Nokogiri;
|
|
434
|
+
# non-element kinds rebuild by value — #161).
|
|
434
435
|
def dup
|
|
435
436
|
ensure_alive!
|
|
436
|
-
elem_ptr = Leptris::XML::FFI.leptris_node_as_element(@c_ptr)
|
|
437
|
-
raise Leptris::XML::Error, "dup is only supported for element nodes" if elem_ptr.null?
|
|
438
437
|
Leptris::XML::Document.copy_of(self)
|
|
439
438
|
end
|
|
440
439
|
alias_method :clone, :dup
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module Leptris::XML::Schematron
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse(schema_xml, phase: nil)
|
|
9
|
+
Schema.parse(schema_xml, phase: phase)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse_file(path, phase: nil)
|
|
13
|
+
Schema.parse_file(path, phase: phase)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A compiled Schematron schema (libleptris >= 1.9.126): parse
|
|
17
|
+
# once — optionally selecting a PHASE, which activates only its
|
|
18
|
+
# patterns — then validate any number of documents. #validate
|
|
19
|
+
# returns the SVRL report as a queryable Document; #valid?
|
|
20
|
+
# answers the boolean.
|
|
21
|
+
#
|
|
22
|
+
# sch = Leptris::XML::Schematron.parse(schema_xml)
|
|
23
|
+
# sch.valid?(doc) # => true/false
|
|
24
|
+
# svrl = sch.validate(doc) # => Document (svrl:schema root)
|
|
25
|
+
# sch = Leptris::XML::Schematron.parse(schema_xml, phase: "Basic")
|
|
26
|
+
#
|
|
27
|
+
class Schema
|
|
28
|
+
class Handle < ::FFI::AutoPointer
|
|
29
|
+
def self.release(ptr)
|
|
30
|
+
Leptris::XML::FFI.leptris_schematron_free(ptr)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.parse(schema_xml, phase: nil)
|
|
35
|
+
xml = schema_xml.to_s
|
|
36
|
+
raw =
|
|
37
|
+
if phase
|
|
38
|
+
Leptris::XML::FFI.leptris_schematron_parse_phase(
|
|
39
|
+
xml, xml.bytesize, phase.to_s, nil)
|
|
40
|
+
else
|
|
41
|
+
Leptris::XML::FFI.leptris_schematron_parse(xml, xml.bytesize, nil)
|
|
42
|
+
end
|
|
43
|
+
if raw.null?
|
|
44
|
+
raise Leptris::XML::Error,
|
|
45
|
+
"schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
|
|
46
|
+
end
|
|
47
|
+
new(Handle.new(raw))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.parse_file(path, phase: nil)
|
|
51
|
+
raw =
|
|
52
|
+
if phase
|
|
53
|
+
xml = File.read(path)
|
|
54
|
+
Leptris::XML::FFI.leptris_schematron_parse_phase(
|
|
55
|
+
xml, xml.bytesize, phase.to_s, nil)
|
|
56
|
+
else
|
|
57
|
+
Leptris::XML::FFI.leptris_schematron_parse_file(path.to_s, nil)
|
|
58
|
+
end
|
|
59
|
+
if raw.null?
|
|
60
|
+
raise Leptris::XML::Error,
|
|
61
|
+
"schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
|
|
62
|
+
end
|
|
63
|
+
new(Handle.new(raw))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(handle)
|
|
67
|
+
@handle = handle
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The SVRL report as a Document (svrl:schema root — query
|
|
71
|
+
# failed-assert / successful-report elements like any other
|
|
72
|
+
# tree), or the schema-level error text.
|
|
73
|
+
def validate(document)
|
|
74
|
+
report = Leptris::XML::FFI.leptris_schematron_validate(
|
|
75
|
+
@handle, document.c_ptr)
|
|
76
|
+
if report.null?
|
|
77
|
+
Leptris::XML::FFI.leptris_schematron_error(@handle)
|
|
78
|
+
else
|
|
79
|
+
Leptris::XML::Document.wrap(report)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def valid?(document)
|
|
84
|
+
Leptris::XML::FFI.leptris_schematron_valid(
|
|
85
|
+
@handle, document.c_ptr) != 0
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -85,6 +85,27 @@ module Leptris::XML::Searchable
|
|
|
85
85
|
at_xpath(expr)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Version-pinned evaluation (libleptris lane 15,
|
|
89
|
+
# leptris_xpath_eval_versioned): :xpath10 keeps the strict XPath
|
|
90
|
+
# 1.0 surface — 3.x syntax (arrow, bang, lookup, let, inline
|
|
91
|
+
# functions, string templates) raises; :xpath31 evaluates the
|
|
92
|
+
# full grammar (identical to #xpath's default entry). A separate
|
|
93
|
+
# method by design: #xpath's trailing-hash argument is the
|
|
94
|
+
# namespace-binding channel, and a declared keyword would
|
|
95
|
+
# capture it.
|
|
96
|
+
def xpath_versioned(expression, version)
|
|
97
|
+
doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
|
|
98
|
+
context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
|
|
99
|
+
result_ptr = Leptris::XML::FFI.leptris_xpath_eval_versioned(
|
|
100
|
+
doc_ptr, context_ptr, expression,
|
|
101
|
+
Leptris::XML::Searchable.xpath_version_code(version), nil)
|
|
102
|
+
if result_ptr.null?
|
|
103
|
+
raise Leptris::XML::XPathError,
|
|
104
|
+
Leptris::XML::FFI.leptris_last_error.to_s
|
|
105
|
+
end
|
|
106
|
+
Leptris::XML::Searchable.wrap_xpath_result(document, result_ptr)
|
|
107
|
+
end
|
|
108
|
+
|
|
88
109
|
protected
|
|
89
110
|
|
|
90
111
|
# Evaluates against a caller-owned namespace binding set; the
|
|
@@ -95,6 +116,21 @@ module Leptris::XML::Searchable
|
|
|
95
116
|
end
|
|
96
117
|
end
|
|
97
118
|
|
|
119
|
+
|
|
120
|
+
# Version pin for xpath_versioned: :xpath10
|
|
121
|
+
# keeps the strict XPath 1.0 surface (3.x syntax — arrow, bang,
|
|
122
|
+
# lookup, let, inline functions — raises); :xpath31 evaluates the
|
|
123
|
+
# full grammar (identical to the default entry).
|
|
124
|
+
def self.xpath_version_code(version)
|
|
125
|
+
case version
|
|
126
|
+
when :xpath10, "1.0" then Leptris::XML::FFI::XPATH_10
|
|
127
|
+
when :xpath31, "3.1" then Leptris::XML::FFI::XPATH_31
|
|
128
|
+
else
|
|
129
|
+
raise ArgumentError,
|
|
130
|
+
"version must be :xpath10 or :xpath31, got #{version.inspect}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
98
134
|
def parse_search_args(args)
|
|
99
135
|
handler = args.find { |a| !a.is_a?(String) && !a.is_a?(Hash) && !a.is_a?(Symbol) }
|
|
100
136
|
args = args - [handler] if handler
|
|
@@ -163,6 +199,12 @@ module Leptris::XML::Searchable
|
|
|
163
199
|
v = Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
164
200
|
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
165
201
|
v
|
|
202
|
+
when Leptris::XML::FFI::XPATH_FUNCTION
|
|
203
|
+
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
204
|
+
raise Leptris::XML::XPathError,
|
|
205
|
+
'XPath function items cannot cross the FFI boundary yet ' \
|
|
206
|
+
'(libleptris TODO 07; call them inside the expression, ' \
|
|
207
|
+
'e.g. for-each((1,2), function($n){$n+1}))'
|
|
166
208
|
else
|
|
167
209
|
Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
|
|
168
210
|
raise Leptris::XML::XPathError, "unknown xpath result type #{type}"
|
data/lib/leptris/xml/xquery.rb
CHANGED
|
@@ -46,10 +46,28 @@ class Leptris::XML::XQuery
|
|
|
46
46
|
# FLWOR results arrive as the sequence channel — read them through
|
|
47
47
|
# an aggregate until the engine materializes readable sequence
|
|
48
48
|
# items (same caveat as the XPath for-return subset).
|
|
49
|
-
|
|
49
|
+
# +params+ binds external variables (libleptris 1.9.133): each
|
|
50
|
+
# value is an XPath expression evaluated in an empty context
|
|
51
|
+
# (QT3 <param select> semantics) — {"x" => "21 * 2"} binds
|
|
52
|
+
# `declare variable $x external`. A binding overrides the
|
|
53
|
+
# declaration's default initializer; an external with neither
|
|
54
|
+
# fails evaluation. An empty params hash takes the same lane —
|
|
55
|
+
# the engine decides between default initializer and failure.
|
|
56
|
+
def eval(doc_or_element, params = nil)
|
|
50
57
|
context = Leptris::XML::EvaluationContext.of(doc_or_element)
|
|
51
|
-
result_ptr =
|
|
52
|
-
|
|
58
|
+
result_ptr =
|
|
59
|
+
if params
|
|
60
|
+
names, name_anchors = Leptris::XML::CStringArray.to_c(
|
|
61
|
+
params.keys.map(&:to_s))
|
|
62
|
+
selects, select_anchors = Leptris::XML::CStringArray.to_c(
|
|
63
|
+
params.values.map(&:to_s))
|
|
64
|
+
Leptris::XML::FFI.leptris_xquery_eval_params(
|
|
65
|
+
@handle, context.document.c_ptr, context.context_node_ptr,
|
|
66
|
+
names, selects, params.size)
|
|
67
|
+
else
|
|
68
|
+
Leptris::XML::FFI.leptris_xquery_eval(
|
|
69
|
+
@handle, context.document.c_ptr, context.context_node_ptr)
|
|
70
|
+
end
|
|
53
71
|
if result_ptr.null?
|
|
54
72
|
raise Leptris::XML::XPathError,
|
|
55
73
|
Leptris::XML::FFI.leptris_last_error.to_s
|
data/lib/leptris/xml.rb
CHANGED
|
@@ -23,6 +23,8 @@ module Leptris
|
|
|
23
23
|
autoload :SAX, "leptris/xml/sax"
|
|
24
24
|
autoload :XPath, "leptris/xml/xpath"
|
|
25
25
|
autoload :EvaluationContext, "leptris/xml/evaluation_context"
|
|
26
|
+
autoload :Schematron, "leptris/xml/schematron"
|
|
27
|
+
autoload :Diff, "leptris/xml/diff"
|
|
26
28
|
autoload :IterationScope, "leptris/xml/iteration_scope"
|
|
27
29
|
autoload :ResultText, "leptris/xml/result_text"
|
|
28
30
|
autoload :ResultAttr, "leptris/xml/result_attr"
|
|
@@ -39,6 +41,14 @@ module Leptris
|
|
|
39
41
|
recover: recover)
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
# Tree diff between two documents: equal subtrees prune in
|
|
45
|
+
# O(1) by content digest; diverging regions align by LCS.
|
|
46
|
+
# +ignore_ws+ treats whitespace-only text nodes as absent.
|
|
47
|
+
def self.diff(document_a, document_b, ignore_ws: false)
|
|
48
|
+
Leptris::XML::Diff.compute(document_a, document_b,
|
|
49
|
+
ignore_ws: ignore_ws)
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
# Does +string+ contain a named entity reference that is not
|
|
43
53
|
# one of the five predefined XML entities (nor numeric)? One C
|
|
44
54
|
# pass (libleptris 1.9.62) — downstream adapters use it to skip
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.144.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -106,6 +106,7 @@ files:
|
|
|
106
106
|
- lib/leptris/xml/cdata.rb
|
|
107
107
|
- lib/leptris/xml/comment.rb
|
|
108
108
|
- lib/leptris/xml/css_to_xpath.rb
|
|
109
|
+
- lib/leptris/xml/diff.rb
|
|
109
110
|
- lib/leptris/xml/doc_type.rb
|
|
110
111
|
- lib/leptris/xml/document.rb
|
|
111
112
|
- lib/leptris/xml/document_fragment.rb
|
|
@@ -128,6 +129,7 @@ files:
|
|
|
128
129
|
- lib/leptris/xml/sax/dom_dispatch.rb
|
|
129
130
|
- lib/leptris/xml/sax/parser.rb
|
|
130
131
|
- lib/leptris/xml/sax/recorder.rb
|
|
132
|
+
- lib/leptris/xml/schematron.rb
|
|
131
133
|
- lib/leptris/xml/searchable.rb
|
|
132
134
|
- lib/leptris/xml/serialization.rb
|
|
133
135
|
- lib/leptris/xml/text.rb
|