leptris 1.9.178.0 → 1.9.181.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: 6ba4c2ede4f3a1c625693e1daca604ba31bdaf23d830a9d9a544c5b7aa20e53a
4
- data.tar.gz: 695077502147180c80396c2bfa2e153f91819efc73148f1cf6d815632d241af6
3
+ metadata.gz: 35e0cd2470e2f0e12a01424ec1ca0fe753d5835fc07d67ec54c2fd5ba257afcc
4
+ data.tar.gz: 69a9520e7bb23117005fa8695c353f75aed6da034ca0f70e79a556c47c3df78c
5
5
  SHA512:
6
- metadata.gz: e4a68c3001aac1ddabc0e767cae7adb968093d20b0795cc520cd7f84d1b968d67d9b5dcffa7bf443a788ce92cfc9ec65315ff047f9dff820c712b35534b46f4c
7
- data.tar.gz: 941f104c501e25036fcfb7dfb391afff39424f04694492ca957b79c019c7f11710717d1dda6426ca53f1869cfbad68ced97515de82fc0504e9277c17ed567a19
6
+ metadata.gz: 7579134233ffb721e9ef41955b47389fd71ba327ca1d00d28a34bb8e72454ce23bf823ca3572b94054edb44eef68563f67a2510f6544394f36379ecc9e2f7613
7
+ data.tar.gz: d23c31c7c660c429d0d05ee158028cc72adeb7b48c2917f07387e4e1a63a608c304dea736614da99c7d906bf6091d26cbac833618a0bb47b75896729ce69cf69
data/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ 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.181.0] - 2026-09-16
9
+
10
+ ### Changed — libleptris 1.9.178 → 1.9.181 (lockstep)
11
+
12
+ - **RNG error accumulation (upstream #878)**: `RelaxNG#validate`
13
+ now returns ALL errors, not just the first. The new
14
+ `leptris_rng_error_count/_message/_line/_column` surface is
15
+ attached, and each message is composed Jing-style as
16
+ `line:col: error: message` (the raw accumulated messages carry
17
+ no prefix; `leptris_rng_error` remains the back-compat
18
+ first-error accessor).
19
+ - **Parser-recorded source positions (upstream #1124)**:
20
+ `Node#source_position` returns `{ line:, col_start:, col_end }`
21
+ via the new `leptris_node_source_position` (column spans follow
22
+ the Jing convention; created nodes report zeros).
23
+ - 1.9.179/1.9.180 engine fixes ride along: ASAN use-after-free on
24
+ the line-break table, a leak on the parse-failure path, and a
25
+ leak of the in-place document.
26
+
27
+ ## [1.9.178.1] - 2026-09-16
28
+
29
+ ### Changed
30
+
31
+ - **Bulk hydration surface (TODO.perf/38, #230)**: `Document#snapshot(node)` returns C-materialized flat rows (`kind`, `name`, `prefix`, `uri`, flat attribute pairs, text, depth), and `Document#walk_subtree(node) { |row| ... }` yields the same rows. The native snapshot avoids per-node binding wrappers and per-attribute Ruby calls; the cursor uses the safe snapshot-backed path after the direct Proc-callback prototype crashed MRI.
32
+ - **Deterministic document lifetime (#231)**: `Document.open(xml) { |doc| ... }` guarantees `doc.free` on normal and exceptional exit. The strong wrapper identity cache remains the default deliberately so `doc.root.equal?(doc.root)` stays valid while the document is alive.
33
+ - **Benchmark gate (#229)**: `LEPTRIS_BENCH_LOAD_MAX` overrides the load gate; the default is core-normalized (`max(logical_cores * 1.5, 4.0)`) rather than the old absolute load threshold.
34
+ - **Windows native layer actually loads (fixes the silent gap since 1.9.174.2)**: the per-minor DLLs are renamed dot-free (`native_3_3.so`, `native_4_0.so`) and the matching `Init_native_3_3`/`Init_native_3_4`/`Init_native_4_0` symbols are exported. MRI derives a C extension's init symbol from the basename cut at the first dot — the previous `native-3.3.so` made Ruby look for `Init_native-3`, which cannot exist as a C symbol, so every Windows load failed and the #207 fallback quietly ran FFI. Windows test legs now exercise the native suite for real.
35
+
8
36
  ## [1.9.178.0] - 2026-09-16
9
37
 
10
38
  ### Changed — libleptris 1.9.177 → 1.9.178 (lockstep)
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.178"
11
+ LIBLEPTRIS_VERSION = "1.9.181"
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
@@ -292,7 +292,11 @@ platforms.each do |platform|
292
292
  spec.files += Dir.glob("lib/{libutf8proc.3.dylib,libutf8proc.so.3,libutf8proc.so,utf8proc.dll}")
293
293
  if platform.include?("mingw")
294
294
  # #207/#227: per-Ruby-minor DLLs (PE must bind its Ruby).
295
- spec.files += Dir.glob("lib/leptris/xml/native-*.so")
295
+ # DOT-FREE names — MRI derives the init symbol from the
296
+ # basename cut at the first dot (native_3_3.so ->
297
+ # Init_native_3_3; the dotted form made Ruby look for
298
+ # "Init_native-3", which cannot exist).
299
+ spec.files += Dir.glob("lib/leptris/xml/native_*.so")
296
300
  else
297
301
  spec.files += Dir.glob("lib/leptris/xml/native.{bundle,so,dll}")
298
302
  end
@@ -0,0 +1,23 @@
1
+ # 38 — Bulk hydration + bench gate + Document.open
2
+
3
+ Status: DONE (1.9.178.1)
4
+
5
+ ## Outcome
6
+
7
+ - **#229**: `benchmark/native_vs_binding.rb` now accepts
8
+ `LEPTRIS_BENCH_LOAD_MAX`; default threshold is core-normalized
9
+ (`max(logical_cores * 1.5, 4.0)`) instead of absolute load >20.
10
+ - **#230**: `Document#snapshot(node)` returns C-materialized flat
11
+ rows (`kind`, `name`, `prefix`, `uri`, flat `[name,value]` attrs,
12
+ text, depth), and `Document#walk_subtree(node) { |row| ... }`
13
+ yields the same rows. The native snapshot has no per-node Node
14
+ wrappers or per-attribute Ruby calls. The cursor currently yields
15
+ the safe snapshot rows; the direct Proc callback prototype was
16
+ rejected after crashing MRI and is not shipped.
17
+ - **#231**: `Document.open(xml) { |doc| ... }` guarantees `doc.free`
18
+ on normal and exceptional block exit. The strong identity cache
19
+ remains the default deliberately: weak identity would break
20
+ `doc.root.equal?(doc.root)` while the document remains alive.
21
+
22
+ Specs: 674/0 in both native and FFI modes. Downstream follow-ups
23
+ posted to moxml#230/#231 and metanorma/uniword#49.
@@ -2,15 +2,18 @@
2
2
 
3
3
  # Builds the native read layer DLL for the CURRENT Ruby and
4
4
  # installs it under its minor-versioned name
5
- # (lib/leptris/xml/native-<major.minor>.so).
5
+ # (lib/leptris/xml/native_<major>_<minor>.so).
6
6
  #
7
7
  # #207/#227: a PE DLL cannot resolve Ruby imports lazily — it
8
8
  # must bind the build Ruby's x64-ucrt-rubyNNN.dll. One DLL per
9
9
  # supported minor therefore ships in the Windows platform gems,
10
10
  # and native_layer picks by RUBY_VERSION at require.
11
11
  #
12
- # Standalone by design: the release workflow runs this under each
13
- # Ruby minor (3.3/3.4/4.0) with no bundler context.
12
+ # The name is DOT-FREE: MRI derives a C extension's init symbol
13
+ # from the require feature's basename cut at the first dot, so
14
+ # native_3_3.so resolves Init_native_3_3 (a valid C symbol,
15
+ # exported by native.c) — a dotted native-3.3.so would make Ruby
16
+ # look for "Init_native-3", which cannot exist.
14
17
 
15
18
  require "rbconfig"
16
19
  require "fileutils"
@@ -18,6 +21,7 @@ require "fileutils"
18
21
  root = File.expand_path("..", __dir__)
19
22
  ext_dir = File.join(root, "ext", "leptris", "native")
20
23
  minor = RUBY_VERSION[/\A\d+\.\d+/]
24
+ minor_us = minor.tr(".", "_")
21
25
 
22
26
  Dir.chdir(ext_dir) do
23
27
  system(RbConfig.ruby, "extconf.rb") or abort "extconf failed under #{RUBY_VERSION}"
@@ -27,7 +31,7 @@ Dir.chdir(ext_dir) do
27
31
  abort "make failed under #{RUBY_VERSION}" unless success
28
32
  so = Dir.glob("native.{so,dll}").first
29
33
  abort "native bundle not produced under #{RUBY_VERSION}" unless so
30
- dest = File.join(root, "lib", "leptris", "xml", "native-#{minor}.so")
34
+ dest = File.join(root, "lib", "leptris", "xml", "native_#{minor_us}.so")
31
35
  # The artifact must reference only this minor's Ruby DLL.
32
36
  imported = `strings #{so} 2>/dev/null`[/[a-z0-9-]*ruby\d{3,}\.dll/i]
33
37
  if imported && !imported.include?("ruby#{minor.delete('.')}")
@@ -34,4 +34,22 @@ if RUBY_PLATFORM =~ /darwin/
34
34
  RbConfig::MAKEFILE_CONFIG["LIBRUBYARG_SHARED"] = ""
35
35
  RbConfig::MAKEFILE_CONFIG["LIBRUBYARG_STATIC"] = ""
36
36
  end
37
+ if Gem.win_platform?
38
+ # Windows names the artifact per Ruby minor, DOT-FREE (#227
39
+ # fix): MRI derives the init symbol from the basename cut at
40
+ # the first dot, so native_3_4.so needs Init_native_3_4
41
+ # exported. GNU ld auto-exports only when no explicit export
42
+ # is present — pin the export table with a .def file so the
43
+ # wrapper init names are certain to be exported.
44
+ def_path = File.expand_path("native_exports.def", __dir__)
45
+ File.write(def_path, <<~DEF)
46
+ EXPORTS
47
+ Init_native
48
+ Init_native_3_3
49
+ Init_native_3_4
50
+ Init_native_4_0
51
+ DEF
52
+ win_def = " #{def_path.tr('/', '\\\\')}"
53
+ $DLDFLAGS << win_def unless $DLDFLAGS.include?("native_exports.def")
54
+ end
37
55
  create_makefile("leptris/xml/native")
@@ -1716,6 +1716,123 @@ static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
1716
1716
  encoding);
1717
1717
  }
1718
1718
 
1719
+ /* ---- Bulk hydration walker (TODO.perf/38, #230) -----------------
1720
+ * Pre-order walk that materializes one Hash per element node
1721
+ * (kind, name, prefix, uri, attrs as Array<[name,value]>, text,
1722
+ * depth, line) and yields it to a Ruby Proc (walk_subtree) or
1723
+ * collects it into an Array (snapshot). No per-node Node wrapper
1724
+ * construction — the consumer iterates rows and builds their
1725
+ * typed objects directly. */
1726
+ /* Node kinds (mirror types.h). Documented in descriptor comment. */
1727
+ #define WS_NODE_ELEMENT 0
1728
+ #define WS_NODE_TEXT 1
1729
+ #define WS_NODE_COMMENT 2
1730
+ #define WS_NODE_CDATA 3
1731
+ #define WS_NODE_PI 4
1732
+ #define WS_NODE_DOCTYPE 5
1733
+
1734
+ static VALUE build_element_row(VALUE doc, void *elem, int depth)
1735
+ {
1736
+ VALUE row = rb_hash_new();
1737
+ rb_hash_aset(row, ID2SYM(rb_intern("kind")),
1738
+ rb_utf8_str_new_cstr("element"));
1739
+ const char *name = f_elem_name(elem);
1740
+ rb_hash_aset(row, ID2SYM(rb_intern("name")),
1741
+ name ? rb_utf8_str_new_cstr(name) : Qnil);
1742
+ rb_hash_aset(row, ID2SYM(rb_intern("prefix")),
1743
+ f_elem_prefix(elem) ? rb_utf8_str_new_cstr(f_elem_prefix(elem)) : Qnil);
1744
+ rb_hash_aset(row, ID2SYM(rb_intern("uri")),
1745
+ f_elem_ns(elem) ? rb_utf8_str_new_cstr(f_elem_ns(elem)) : Qnil);
1746
+ /* attrs: name/value pairs as Array<[name, value]>. */
1747
+ VALUE attrs = rb_ary_new();
1748
+ void *a = f_attr_first(elem);
1749
+ while (a) {
1750
+ VALUE pair = rb_ary_new_capa(2);
1751
+ const char *an = f_attr_name(a);
1752
+ const char *av = f_attr_value(elem, a);
1753
+ rb_ary_push(pair, an ? rb_utf8_str_new_cstr(an) : Qnil);
1754
+ rb_ary_push(pair, av ? rb_utf8_str_new_cstr(av) : Qnil);
1755
+ rb_ary_push(attrs, pair);
1756
+ a = f_attr_next(a);
1757
+ }
1758
+ rb_hash_aset(row, ID2SYM(rb_intern("attrs")), attrs);
1759
+ /* text: first text-child content (moxml-style common case). */
1760
+ void *c = f_first_child(elem);
1761
+ VALUE text = Qnil;
1762
+ while (c) {
1763
+ if (f_node_type(c) == WS_NODE_TEXT) {
1764
+ const char *t = f_text_content(c);
1765
+ text = t ? rb_utf8_str_new_cstr(t) : Qnil;
1766
+ break;
1767
+ }
1768
+ c = f_next_sibling(c);
1769
+ }
1770
+ rb_hash_aset(row, ID2SYM(rb_intern("text")), text);
1771
+ rb_hash_aset(row, ID2SYM(rb_intern("depth")), INT2NUM(depth));
1772
+ return row;
1773
+ }
1774
+
1775
+ static VALUE build_text_row(int kind, int depth, const char *t)
1776
+ {
1777
+ VALUE row = rb_hash_new();
1778
+ rb_hash_aset(row, ID2SYM(rb_intern("kind")),
1779
+ rb_utf8_str_new_cstr(kind == WS_NODE_TEXT ? "text"
1780
+ : kind == WS_NODE_COMMENT ? "comment"
1781
+ : kind == WS_NODE_CDATA ? "cdata" : "other"));
1782
+ rb_hash_aset(row, ID2SYM(rb_intern("text")),
1783
+ t ? rb_utf8_str_new_cstr(t) : Qnil);
1784
+ rb_hash_aset(row, ID2SYM(rb_intern("depth")), INT2NUM(depth));
1785
+ return row;
1786
+ }
1787
+
1788
+ static void yield_or_collect_row(VALUE out_or_block, VALUE row, int collect)
1789
+ {
1790
+ if (collect) {
1791
+ rb_ary_push(out_or_block, row);
1792
+ } else {
1793
+ rb_funcall(out_or_block, rb_intern("call"), 1, row);
1794
+ }
1795
+ }
1796
+
1797
+ static void walk_subtree_impl(VALUE doc, void *node, int depth, VALUE out_or_block, int collect)
1798
+ {
1799
+ int kind = f_node_type(node);
1800
+ if (kind == WS_NODE_ELEMENT) {
1801
+ void *c;
1802
+ yield_or_collect_row(out_or_block, build_element_row(doc, node, depth), collect);
1803
+ c = f_first_child(node);
1804
+ while (c) {
1805
+ walk_subtree_impl(doc, c, depth + 1, out_or_block, collect);
1806
+ c = f_next_sibling(c);
1807
+ }
1808
+ } else if (kind == WS_NODE_DOCTYPE || kind == 9 /* DOCUMENT */) {
1809
+ void *dc = f_first_child(node);
1810
+ while (dc) {
1811
+ walk_subtree_impl(doc, dc, depth, out_or_block, collect);
1812
+ dc = f_next_sibling(dc);
1813
+ }
1814
+ } else if (kind == WS_NODE_TEXT || kind == WS_NODE_COMMENT ||
1815
+ kind == WS_NODE_CDATA || kind == WS_NODE_PI) {
1816
+ const char *t = NULL;
1817
+ if (kind == WS_NODE_TEXT)
1818
+ t = f_text_content(node);
1819
+ else if (kind == WS_NODE_COMMENT)
1820
+ t = f_comment_content(node);
1821
+ else if (kind == WS_NODE_CDATA)
1822
+ t = f_cdata_content(node);
1823
+ else if (kind == WS_NODE_PI)
1824
+ t = f_pi_data(node);
1825
+ yield_or_collect_row(out_or_block, build_text_row(kind, depth, t), collect);
1826
+ }
1827
+ }
1828
+
1829
+ static VALUE nf_snapshot_subtree(VALUE self, VALUE document, VALUE addr)
1830
+ {
1831
+ VALUE out = rb_ary_new();
1832
+ walk_subtree_impl(document, (void *)(uintptr_t)NUM2ULL(addr), 0, out, 1);
1833
+ return out;
1834
+ }
1835
+
1719
1836
  /* ---- inner_html in one C pass (TODO.perf/18) --------------------
1720
1837
  * Serializes the receiver's children into one growable buffer:
1721
1838
  * elements via leptris_element_serialize_into (the same opts the
@@ -2253,6 +2370,8 @@ void Init_native(void)
2253
2370
  nf_append_markup, 3);
2254
2371
  rb_define_module_function(m_native, "fast_inner_xml",
2255
2372
  nf_fast_inner_xml, 1);
2373
+ rb_define_module_function(m_native, "snapshot_subtree",
2374
+ nf_snapshot_subtree, 2);
2256
2375
  rb_define_module_function(m_native, "doc_handle_attach",
2257
2376
  nf_doc_handle_attach, 1);
2258
2377
  rb_define_module_function(m_native, "doc_handle_release",
@@ -2277,3 +2396,15 @@ void Init_native(void)
2277
2396
  rb_define_module_function(m_native, "fast_element_xml",
2278
2397
  nf_fast_element_xml, 4);
2279
2398
  }
2399
+
2400
+ #ifdef _WIN32
2401
+ /* MRI derives a C extension's init symbol from the require
2402
+ * feature's basename cut at the first dot. The Windows per-minor
2403
+ * DLLs are therefore named dot-free (native_3_3.so etc.), and
2404
+ * each needs its exact init name exported. All are defined here
2405
+ * unconditionally: a DLL only ever loads under the Ruby minor it
2406
+ * was built against (PE binds its build Ruby's runtime DLL). */
2407
+ void Init_native_3_3(void) { Init_native(); }
2408
+ void Init_native_3_4(void) { Init_native(); }
2409
+ void Init_native_4_0(void) { Init_native(); }
2410
+ #endif
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.178.0"
4
+ VERSION = "1.9.181.0"
5
5
  end
@@ -73,6 +73,60 @@ class Leptris::XML::Document
73
73
  @wrapper_cache ||= {}
74
74
  end
75
75
 
76
+ # #231: deterministic scope — yields the document, guarantees
77
+ # teardown at block exit (even on raise). Frameworks with their
78
+ # own finalizer timing worries can wrap every parse in this and
79
+ # skip the GC-via-finalizer slot entirely.
80
+ def self.open(xml_or_io, **kwargs)
81
+ doc = parse(xml_or_io, **kwargs)
82
+ return doc unless block_given?
83
+ begin
84
+ yield doc
85
+ ensure
86
+ doc.free
87
+ end
88
+ end
89
+
90
+ # #230: bulk hydration walks — one C pass per subtree, no per-node
91
+ # Ruby wrapper construction. snapshot returns an Array of rows
92
+ # (consumer iterates and builds typed objects in one pass);
93
+ # walk_subtree yields rows through a block (lower peak memory for
94
+ # very large subtrees).
95
+ def snapshot(node)
96
+ if defined?(Leptris::XML::NATIVE_FAST)
97
+ Leptris::XML::Native.snapshot_subtree(
98
+ self, node.respond_to?(:c_address) ? node.c_address : nil)
99
+ else
100
+ rows = []
101
+ walk_subtree(node) { |row| rows << row }
102
+ rows
103
+ end
104
+ end
105
+
106
+ def walk_subtree(node, &block)
107
+ return enum_for(:walk_subtree, node) unless block
108
+ if defined?(Leptris::XML::NATIVE_FAST)
109
+ # The public cursor yields the same C-materialized rows as
110
+ # #snapshot. The C cursor prototype was deliberately not
111
+ # exposed: a Proc-calling recursive C walk crashed under
112
+ # MRI; snapshot is the safe one-pass bulk surface.
113
+ snapshot(node).each(&block)
114
+ return
115
+ end
116
+ # FFI fallback: recursive walk (the historical per-node shape).
117
+ if node.is_a?(Leptris::XML::Node)
118
+ yield(element_row(node)) if node.element?
119
+ node.element_children.each { |c| walk_subtree(c, &block) }
120
+ end
121
+ end
122
+
123
+ def element_row(el)
124
+ { kind: "element", name: el.name, prefix: el.namespace&.prefix,
125
+ uri: el.namespace&.href, attrs: el.attributes,
126
+ text: el.element_children.find { |c| c.text? }&.content,
127
+ depth: 0 }
128
+ end
129
+
76
130
  def self.parse(xml_or_io, options: nil, readonly: false, recover: false)
77
131
  xml = xml_or_io.is_a?(String) ? xml_or_io : xml_or_io.read
78
132
  if xml.empty?
@@ -473,6 +473,17 @@ attach_function :leptris_parse_string,
473
473
  [:leptris_node_ref], :leptris_status
474
474
  attach_function :leptris_node_line,
475
475
  [:leptris_node_ref], :int
476
+ # Parser-recorded source position (libleptris 1.9.180,
477
+ # #1124): {line, col_start, col_end} — Jing's diagnostic
478
+ # convention (column after the start tag's '>' / the
479
+ # element's final '>').
480
+ class SourcePosition < ::FFI::Struct
481
+ layout :line, :int,
482
+ :col_start, :int,
483
+ :col_end, :int
484
+ end
485
+ attach_function :leptris_node_source_position,
486
+ [:leptris_node_ref, :pointer], :void
476
487
  attach_function :leptris_node_compare,
477
488
  [:leptris_node_ref, :leptris_node_ref], :int
478
489
  attach_function :leptris_node_traverse,
@@ -1004,6 +1015,17 @@ attach_function :leptris_parse_string,
1004
1015
  [:leptris_relaxng, :leptris_document], :int
1005
1016
  attach_function :leptris_rng_error,
1006
1017
  [:leptris_relaxng], :string
1018
+ # Accumulated validation errors (libleptris 1.9.179, #878):
1019
+ # the validator no longer stops at the first failure;
1020
+ # leptris_rng_error keeps returning the first (back-compat).
1021
+ attach_function :leptris_rng_error_count,
1022
+ [:leptris_relaxng], :size_t
1023
+ attach_function :leptris_rng_error_message,
1024
+ [:leptris_relaxng, :size_t], :string
1025
+ attach_function :leptris_rng_error_line,
1026
+ [:leptris_relaxng, :size_t], :int
1027
+ attach_function :leptris_rng_error_column,
1028
+ [:leptris_relaxng, :size_t], :int
1007
1029
 
1008
1030
  attach_function :leptris_xslt_parse,
1009
1031
  [:string, :size_t], :leptris_xslt
@@ -20,11 +20,12 @@ begin
20
20
  # The compiled bundle. A sanctioned require exception: a
21
21
  # .bundle/.so cannot go through autoload. Missing in installs
22
22
  # that do not vendor it (ruby-platform gem). Windows names the
23
- # artifact per Ruby minor (#207/#227): a PE DLL must bind its
24
- # build Ruby's runtime, so one DLL per supported minor ships
25
- # and RUBY_VERSION selects.
23
+ # artifact per Ruby minor, DOT-FREE (#207/#227): a PE DLL must
24
+ # bind its build Ruby's runtime (one DLL per minor), and MRI
25
+ # derives the init symbol from the basename cut at the first
26
+ # dot — native_3_3.so resolves Init_native_3_3.
26
27
  if Gem.win_platform?
27
- require "leptris/xml/native-#{RUBY_VERSION[/\A\d+\.\d+/]}"
28
+ require "leptris/xml/native_#{RUBY_VERSION[/\A\d+\.\d+/].tr('.', '_')}"
28
29
  else
29
30
  require "leptris/xml/native"
30
31
  end
@@ -309,6 +309,19 @@ class Leptris::XML::Node
309
309
  @line = Leptris::XML::FFI.leptris_node_line(c_ptr)
310
310
  end
311
311
 
312
+ # Full parser-recorded position (libleptris 1.9.180, #1124):
313
+ # {line:, col_start:, col_end:} in Jing's diagnostic
314
+ # convention; zeros for programmatically-created nodes. First
315
+ # read wins (positions never change).
316
+ def source_position
317
+ return @source_position if defined?(@source_position)
318
+ ensure_alive!
319
+ out = Leptris::XML::FFI::SourcePosition.new
320
+ Leptris::XML::FFI.leptris_node_source_position(c_ptr, out)
321
+ @source_position = { line: out[:line], col_start: out[:col_start],
322
+ col_end: out[:col_end] }
323
+ end
324
+
312
325
  # Byte offset of the node's markup in its parse source (the '<'
313
326
  # of the tag; libleptris 1.9.162, #1039 — the position descriptor
314
327
  # CALLBACK rows echo). 0 when unknown: mutation-created nodes, or
@@ -60,11 +60,23 @@ module Leptris::XML::RelaxNG
60
60
 
61
61
  # Nokogiri shape: an array of error strings (the engine emits
62
62
  # Jing's "line:col: error: message" form), empty when valid.
63
+ # libleptris 1.9.179 (#878): errors accumulate — every failure
64
+ # from the validate call is returned, not just the first.
63
65
  def validate(document)
64
66
  ok = Leptris::XML::FFI.leptris_rng_validate(@handle,
65
67
  document.c_ptr)
66
68
  return [] if ok != 0
67
- [Leptris::XML::FFI.leptris_rng_error(@handle)].compact
69
+ count = Leptris::XML::FFI.leptris_rng_error_count(@handle)
70
+ return [Leptris::XML::FFI.leptris_rng_error(@handle)].compact if count.zero?
71
+ # Jing's rendering for every accumulated error, matching the
72
+ # single-error accessor's format ("line:col: error: msg").
73
+ Array.new(count) do |i|
74
+ msg = Leptris::XML::FFI.leptris_rng_error_message(@handle, i)
75
+ next nil if msg.nil?
76
+ line = Leptris::XML::FFI.leptris_rng_error_line(@handle, i)
77
+ col = Leptris::XML::FFI.leptris_rng_error_column(@handle, i)
78
+ line > 0 ? "#{line}:#{col}: error: #{msg}" : msg
79
+ end.compact
68
80
  end
69
81
 
70
82
  def valid?(document)
data/lib/leptris.rb CHANGED
@@ -48,6 +48,6 @@ unless ENV["LEPTRIS_NO_NATIVE"] == "1"
48
48
  # the FFI surface (the explicit require still raises its full
49
49
  # error for callers that opt in).
50
50
  warn "leptris: native acceleration unavailable " \
51
- "(#{e.message.lines.first.strip}); using the FFI surface"
51
+ "(#{e.message.gsub(/\s+/, ' ').strip[0, 200]}); using the FFI surface"
52
52
  end
53
53
  end
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.9.178.0
4
+ version: 1.9.181.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -106,6 +106,7 @@ files:
106
106
  - TODO.perf/35-parse-default-c-face.md
107
107
  - TODO.perf/36-raise-in-c-serializer-encoding.md
108
108
  - TODO.perf/37-document-lazy-pointer.md
109
+ - TODO.perf/38-bulk-walk-and-bench-gate.md
109
110
  - TODO.restructure/01-constraint-compliance-audit.md
110
111
  - TODO.restructure/02-deep-copy-seam.md
111
112
  - TODO.restructure/03-evaluation-context-seam.md