leptris 1.5.0 → 1.6.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/CLAUDE.md +76 -87
  4. data/Rakefile +1 -1
  5. data/lib/leptris/version.rb +1 -1
  6. data/lib/leptris/xml/cdata.rb +1 -0
  7. data/lib/leptris/xml/comment.rb +1 -0
  8. data/lib/leptris/xml/document.rb +47 -14
  9. data/lib/leptris/xml/element.rb +21 -3
  10. data/lib/leptris/xml/ffi.rb +70 -13
  11. data/lib/leptris/xml/iterparse.rb +57 -0
  12. data/lib/leptris/xml/node.rb +32 -10
  13. data/lib/leptris/xml/node_set.rb +6 -5
  14. data/lib/leptris/xml/processing_instruction.rb +2 -0
  15. data/lib/leptris/xml/pull.rb +123 -0
  16. data/lib/leptris/xml/searchable.rb +5 -2
  17. data/lib/leptris/xml/serialization.rb +20 -2
  18. data/lib/leptris/xml/text.rb +1 -0
  19. data/lib/leptris/xml/xpath.rb +80 -0
  20. data/lib/leptris/xml.rb +6 -2
  21. metadata +5 -37
  22. data/TODO.impl/01-architecture.md +0 -217
  23. data/TODO.impl/02-ffi-declarations.md +0 -236
  24. data/TODO.impl/03-document-node-element-nodeset.md +0 -382
  25. data/TODO.impl/04-sax-parser.md +0 -203
  26. data/TODO.impl/05-serialize-c14n-memory-specs-css.md +0 -276
  27. data/docs/ARCHITECTURE.adoc +0 -559
  28. data/docs/BUILD.md +0 -395
  29. data/docs/ERROR_MESSAGES.md +0 -458
  30. data/docs/FFI_ARCHITECTURE.md +0 -439
  31. data/docs/FUTURE_VISION.md +0 -303
  32. data/docs/GITHUB_ACTIONS.md +0 -293
  33. data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +0 -459
  34. data/docs/PERFORMANCE.adoc +0 -668
  35. data/docs/PERFORMANCE.md +0 -448
  36. data/docs/RELEASE_NOTES_v1.0.0.md +0 -515
  37. data/docs/XPATH_SPEC_COMPLIANCE.md +0 -298
  38. data/docs/completion/leptris.bash +0 -86
  39. data/docs/completion/leptris.zsh +0 -74
  40. data/docs/man/leptris-format.1 +0 -227
  41. data/docs/man/leptris-parse.1 +0 -178
  42. data/docs/man/leptris-xpath.1 +0 -312
  43. data/docs/man/leptris.1 +0 -160
  44. data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +0 -217
  45. data/docs/v0.9.0_RELEASE_SUMMARY.md +0 -281
  46. data/docs/v1.0.0_CONTINUATION_PLAN.md +0 -172
  47. data/docs/v1.0.0_CONTINUATION_PROMPT.md +0 -382
  48. data/docs/v1.0.0_SESSION_6_CONTINUATION.md +0 -434
  49. data/docs/v1.0.0_SESSION_6_PROMPT.md +0 -231
  50. data/docs/v1.0.0_STATUS_TRACKER.md +0 -224
  51. data/docs/v1.1.0_CONTINUATION_PLAN.md +0 -299
  52. data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +0 -201
  53. data/docs/v1.1.0_SESSION_3_PROMPT.md +0 -223
  54. data/docs/v1.1.0_STATUS_TRACKER.md +0 -355
  55. data/docs/xml-performance.adoc +0 -115
  56. data/docs/xpath-performance.adoc +0 -379
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad1c47626d0398f149609fa176e8641caf7c9c58a083660c240d4a363ee6600b
4
- data.tar.gz: effc7767e3164af9d32be78f67eafc44740a04aab0a50e1173ef474ea0734b12
3
+ metadata.gz: b2ea74c449e12497abc87b87d5c6196524540a586e077913e63f94a3b103fa3f
4
+ data.tar.gz: 7f80cecd901a98c265539de2be30930d0cd58276f5425defa99a4917d89e4573
5
5
  SHA512:
6
- metadata.gz: f5a68f13873fa5ad8506d586d01be03fb5ecb94a3f987a98d920d6f09fc02156a5f27920f2b4700d7386dc499fa85b979e7a9dbf784dbe4175a400dff0b242ef
7
- data.tar.gz: 414fac2dd071be2b84ce9c677d383865a3687482d314d1b6e726252e3a0809e2e1f9a907e9fda8926776ed9d8d85d8987de4e48cd25a51ae60299ae67c3a212b
6
+ metadata.gz: 4267ae7e0d05eb64ddb9121af9d1ba5e99edc122cdcc338228b5b6660ece73e1d26ebde4b5a28885d512d892152feec0673bb06f2ae29fec1403885445ebcd85
7
+ data.tar.gz: 755e4b1d239aa5fbdac57c15ffbb7c6876013bf9057e0eee2a893c0d0f417331ae469eb8ecc9ac6148425784fe38f06e4a7869b199bf3745222413ea9c6968f6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@ 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.6.1] - 2026-08-24
9
+
10
+ ### Added
11
+
12
+ - **Readonly mode**: `Leptris::XML.parse(xml, readonly: true)` /
13
+ `Document#readonly!` (one-way). Mutations raise
14
+ `Leptris::XML::ReadOnlyError`; read paths memoize aggressively
15
+ (names, content, children NodeSets, attribute hashes) since they can
16
+ never go stale; the C document is frozen (advisory upstream).
17
+ Detached factories (`create_element` etc.) remain usable.
18
+ Purpose: steady-state read performance for the dominant
19
+ parse-query-serialize workload — see Changed.
20
+
21
+ ### Changed
22
+
23
+ - Micro-optimizations targeting the small-document gap versus C
24
+ extension bindings:
25
+ - node type is memoized from `Node.wrap`'s dispatch call — every
26
+ predicate and `#type` is now FFI-free
27
+ - the default serialize options struct is built once and reused
28
+ - `Document.parse` skips the per-parse status MemoryPointer (the C
29
+ out-param is nullable; failure detail comes from the thread-local
30
+ last error)
31
+ - `Element#name` memoizes (invalidated by `name=`)
32
+
33
+ ## [1.6.0] - 2026-08-24
34
+
35
+ Lockstep with libleptris 1.6.0 — the moxml-adapter blockers fixed:
36
+ same-parent node moves no longer corrupt the sibling chain (#518),
37
+ detached PI/comment/CDATA mutation works on rootless documents
38
+ (#519); union nodesets keep attribute identity (#514); document-level
39
+ processing instructions gained a public API (#526).
40
+
8
41
  ## [1.5.0] - 2026-08-24
9
42
 
10
43
  Lockstep with libleptris 1.5.0 — the engine pins the TODO.engine
@@ -18,6 +51,59 @@ release: pull (StAX) API, bounded iterparse, compiled XPath
18
51
  expressions, per-parse options, truthful serialization encoding
19
52
  declarations. Binding-side adoption of the new APIs follows.
20
53
 
54
+ ## [1.6.1] - 2026-08-24
55
+
56
+ ### Added
57
+
58
+ - **Readonly mode**: `Leptris::XML.parse(xml, readonly: true)` /
59
+ `Document#readonly!` (one-way). Mutations raise
60
+ `Leptris::XML::ReadOnlyError`; read paths memoize aggressively
61
+ (names, content, children NodeSets, attribute hashes) since they can
62
+ never go stale; the C document is frozen (advisory upstream).
63
+ Detached factories (`create_element` etc.) remain usable.
64
+ Purpose: steady-state read performance for the dominant
65
+ parse-query-serialize workload — see Changed.
66
+
67
+ ### Changed
68
+
69
+ - Micro-optimizations targeting the small-document gap versus C
70
+ extension bindings:
71
+ - node type is memoized from `Node.wrap`'s dispatch call — every
72
+ predicate and `#type` is now FFI-free
73
+ - the default serialize options struct is built once and reused
74
+ - `Document.parse` skips the per-parse status MemoryPointer (the C
75
+ out-param is nullable; failure detail comes from the thread-local
76
+ last error)
77
+ - `Element#name` memoizes (invalidated by `name=`)
78
+
79
+ ## [1.6.0] - 2026-08-24
80
+
81
+ Lockstep with libleptris 1.6.0 (v1.4/v1.5 were fix releases with no
82
+ public API changes).
83
+
84
+ ### Added
85
+
86
+ - **`Leptris::XML::XPath` — compiled expressions** (parse once,
87
+ evaluate many): `XPath.compile("//item[@qty > 3]")` returns a
88
+ reusable handle (GC-managed) whose `#eval(doc_or_element[, ns])`
89
+ shares result-wrapping semantics with `Searchable#xpath`,
90
+ including namespace bindings.
91
+ - **`Leptris::XML::Pull` — StAX-style pull parsing**:
92
+ `Pull.parse(xml) { |event| }` streams start_element (with captured
93
+ attributes), end_element, text, comment, cdata, pi, end_document,
94
+ and error events; `Pull.parse_file` streams from disk.
95
+ - **`Leptris::XML::Iterparse` — incremental tree iteration**: yields
96
+ each completed top-level child element; the previous subtree is
97
+ released as the next is produced (memory bounded by the largest
98
+ subtree, not the document). Yielded elements have no parent
99
+ Document; upstream v1 limitation: namespace prefixes are not
100
+ re-resolved.
101
+ - **Document-level processing instructions**: `Document#processing_instructions`
102
+ (array of [target, data]) and `Document#add_pi(target, data)` —
103
+ document PIs are not tree nodes, per the C contract.
104
+ - `leptris_parse_string_ex` bound (options-struct parse; the flags
105
+ path remains the default).
106
+
21
107
  ## [1.3.0] - 2026-08-23
22
108
 
23
109
  Lockstep with libleptris 1.3.0 — takes full advantage of the
data/CLAUDE.md CHANGED
@@ -4,101 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Project
6
6
 
7
- `leptris-ruby` is a Ruby gem that wraps the native C library `libleptris` (built via CMake) via the `ffi` gem. It exposes an XML parser with complete XPath 1.0 support and a CLI. Target users want Nokogiri-like ergonomics with native speed.
8
-
9
- - Native dep: `libleptris` shared library, built by `ext/leptris/extconf.rb` (CMake) and copied to `lib/libleptris.{dylib,so,dll}`.
10
- - Ruby entry point: `lib/leptris.rb` (uses `require_relative` see "Conventions" below).
11
- - CLI: `bin/leptris` (Thor-based), defined in `lib/leptris/cli.rb`.
7
+ `leptris-ruby` is the official Ruby binding for `libleptris`
8
+ (https://github.com/leptris/leptris), a pure-C99 XML 1.0 parser with a
9
+ W3C-conformant XPath 1.0 engine (438/438 on the W3C suite), full SAX,
10
+ pull parsing, and C14N. The binding is **FFI-based** (no C extension,
11
+ no compilation at install) and ships **precompiled platform gems**
12
+ with the shared library vendored inside: x86_64/aarch64-linux (glibc
13
+ and musl), x86_64/arm64-darwin, x64-mingw32/ucrt, aarch64-mingw-ucrt,
14
+ plus the pure-Ruby fallback gem.
15
+
16
+ Version lockstep: the gem's major.minor tracks the C library
17
+ (`libleptris 1.6.x` <-> `leptris 1.6.x`). A C release means one PR
18
+ carrying version.rb + the Rakefile `LIBLEPTRIS_VERSION` pin + the
19
+ CHANGELOG entry, then the release workflow.
12
20
 
13
21
  ## Commands
14
22
 
15
23
  ```bash
16
- # Build the C library and install it into lib/ for FFI to load
17
- bundle exec rake compile # runs ext/leptris/extconf.rb (CMake)
18
-
19
- # Run the full test suite (builds first)
20
- bundle exec rake test # = spec, depends on :compile
21
-
22
- # Run RSpec directly (skips build dependency)
23
- bundle exec rspec # full suite
24
- bundle exec rspec spec/leptris/document_spec.rb # one file
25
- bundle exec rspec spec/leptris/document_spec.rb:42 # one example by line
26
-
27
- # Lint
28
- bundle exec rubocop
29
-
30
- # Clean build artifacts (lib/libleptris.*, ext/leptris/build, Makefile, tmp, pkg)
31
- bundle exec rake clean
24
+ bundle exec rake compile # build libleptris (pinned tarball) into lib/
25
+ bundle exec rspec # full suite (uses the vendored library)
26
+ bundle exec rspec spec/xml/xpath_spec.rb:42 # one example
27
+ bundle exec rake gem:native:arm64-darwin # platform gem
32
28
  ```
33
29
 
34
- CI (`.github/workflows/test.yml`) runs `bundle exec rspec` + `bundle exec rubocop` on Ubuntu + macOS across Ruby 3.0–3.3.
30
+ `LEPTRIS_LIB_PATH=/path/to/libleptris.dylib bundle exec rspec` runs
31
+ against an arbitrary build (e.g. a libleptris checkout under test).
32
+
33
+ ## Architecture
35
34
 
36
- ## Architecture: current state (v1.1.0)
35
+ The C DOM is the single source of truth; Ruby objects are thin FFI
36
+ handles. One Ruby method = one FFI call wherever possible.
37
37
 
38
38
  ```
39
- User Ruby code
40
-
41
- Leptris.parse / Leptris.parse_file (lib/leptris.rb)
42
- FFI call (leptris_parse) one-shot tree copy
43
- C document → FFI::Bridge.document_from_ptr (lib/leptris/ffi/bridge.rb)
44
- ↓ recursive hydration
45
- Ruby Document → Element → Node → NodeSet (lib/leptris/{document,element,node,node_set}.rb)
39
+ lib/leptris.rb — Leptris::VERSION
40
+ lib/leptris/xml.rb — autoload registry, error classes,
41
+ Leptris::XML.parse / parse_file
42
+ lib/leptris/xml/ffi.rb — every public C declaration + seam
43
+ helpers (check_status, read_owned_string,
44
+ status_message)
45
+ lib/leptris/xml/document.rb — the only C-memory owner (finalizer);
46
+ factories, PI accessors, exslt, last_error
47
+ lib/leptris/xml/node.rb — Node.wrap: the ONLY wrapper constructor
48
+ (identity cache + type dispatch)
49
+ lib/leptris/xml/element.rb — attributes via the v1.1.0 iteration
50
+ face; namespace/mutation surface
51
+ lib/leptris/xml/node_set.rb — lazy XPath results; batch fetch via
52
+ get_nodes_ex
53
+ lib/leptris/xml/searchable.rb — xpath/css/at_*; namespace-bound path
54
+ lib/leptris/xml/xpath.rb — compiled expressions (parse once,
55
+ eval many)
56
+ lib/leptris/xml/sax/ — callback SAX
57
+ lib/leptris/xml/pull.rb — StAX-style pull parsing
58
+ lib/leptris/xml/iterparse.rb — bounded-memory element iteration
59
+ lib/leptris/xml/serialization.rb — serialize/c14n (Document + Element
60
+ one-liners over this module)
61
+ lib/leptris/xml/c_string_array.rb — NULL-terminated char** adapter
62
+ lib/leptris/xml/css_to_xpath.rb — minimal CSS translation
46
63
  ```
47
64
 
48
- Key directories:
49
-
50
- - `lib/leptris.rb` top-level module, `parse`, `parse_file`, `xpath_evaluate`, error classes (`ParseError`, `XPathError`, `EvaluationError`).
51
- - `lib/leptris/ffi/` FFI plumbing: `library.rb` (bindings), `types.rb` (constants), `memory.rb` (AutoPointer wrappers), `errors.rb` (thread-local error check), `bridge.rb` (C ptr → Ruby object).
52
- - `lib/leptris/{document,element,node,node_set}.rb` pure-Ruby tree model (full hydration on parse).
53
- - `lib/leptris/xpath/` pure-Ruby XPath engine (lexer, parser, compiler, VM). XPath DOES NOT go through C currently; `lib/leptris.rb#xpath_evaluate` calls `FFI.leptris_xpath_eval` only as a wrapper, but the result materialization in `FFI::Bridge` recursively re-walks via the Ruby tree.
54
- - `lib/leptris/adapter*` third-party format adapters.
55
- - `spec/leptris/` — 250+ RSpec examples covering parser, XPath, namespaces, errors, ox-compatibility.
56
- - `ext/leptris/` CMake-based build of `libleptris` (sources come from the separate `leptris/leptris` repo at build time).
57
-
58
- ## Architecture: planned rewrite (see `TODO.impl/`)
59
-
60
- The five files under `TODO.impl/` describe a planned rewrite that has NOT been implemented yet. Read them before touching the Ruby/COM layer. Summary:
61
-
62
- 1. **`01-architecture.md`** — Rewrite `leptris-ruby` as a **thin FFI wrapper** around libleptris v0.4.2 with a **Nokogiri-compatible API**. Current code does a one-shot C→Ruby tree copy and runs XPath in Ruby; planned code keeps the C DOM as the single source of truth (Ruby objects = handles wrapping opaque pointers), so every Ruby method = one FFI call and XPath/SAX go through the C engine.
63
-
64
- 2. **`02-ffi-declarations.md`** Complete FFI attachment: every public function in libleptris v0.4.2 (document lifecycle, node access, element queries/mutation, creation, text/comment/CDATA/PI access, XPath + variable set, SAX, serialization, `leptris_free_string`). Opaque typedefs: `document`, `element`, `node_ref`, `xpath_result`, `sax_parser`, `attribute`. Structs: `SAXHandler`, `SerializeOptions`. Constants for status codes, node types, XPath result types.
65
-
66
- 3. **`03-document-node-element-nodeset.md`** Target file layout:
67
- ```
68
- lib/leptris.rb
69
- lib/leptris/xml.rb
70
- lib/leptris/xml/{ffi,document,node,element,text,comment,cdata,
71
- processing_instruction,attr,node_set,searchable,
72
- parse_options}.rb
73
- ```
74
- `Node.wrap(ptr, doc)` dispatches on the C node type. Document is the only memory-owning object; Node/Element/Text/Comment/CDATA/PI/Attr are non-owning handles valid until `Document#free`.
75
-
76
- 4. **`04-sax-parser.md`** `Leptris::XML::SAX::{Parser, Document}` wrapping `leptris_sax_parse` / `leptris_sax_parser_feed`. FFI::Function callbacks for each event; `start_element` walks the NULL-terminated `const char**` attribute array. Streaming via incremental `feed`.
77
-
78
- 5. **`05-serialize-c14n-memory-specs-css.md`** — `SerializeOptions` struct for `leptris_serialize_document`; `Document#canonicalize` (modes `C14N_1_0`, `C14N_1_1`, `C14N_EXCLUSIVE`); `UseAfterFreeError` guard; minimal CSS-to-XPath converter (`.class`, `#id`, `[attr]`, `[attr=val]`, `:first-child`, `:last-child`); spec layout under `spec/xml/{parse,document,node,element,node_set,xpath,sax,serialize,c14n,memory}_spec.rb`.
79
-
80
- ### Memory ownership rules (planned)
81
-
82
- | Ruby class | Owns C memory? | Free function |
83
- |---|---|---|
84
- | `Document` | YES | `leptris_document_free` |
85
- | `Node`/`Element`/text/CDATA/PI/Attr | NO (borrowed) | freed transitively by Document |
86
- | `NodeSet` (XPath result) | YES | `leptris_xpath_result_free` |
87
- | SAX handler closures | callback lifetime | `leptris_sax_parser_free` |
88
-
89
- GC safety net: `ObjectSpace.define_finalizer` capturing the **raw pointer value**, not the Ruby wrapper. Finalizers must not double-free after explicit `#free`.
90
-
91
- ## Reference material
92
-
93
- - Nokogiri source (`~/src/external/nokogiri/`) — `lib/nokogiri/xml/{node,node_set,document,searchable}.rb` are the API shape targets.
94
- - libleptris public headers (`src/include/leptris/{types,leptris}.h`, `src/include/leptris/{dom,xpath,sax}/*.h`) — the single source of truth for FFI declarations. Target tag: `v0.4.2`.
95
- - `docs/FFI_ARCHITECTURE.md` — describes the v0.5.0 FFI design (AutoPointer, two-pointer strategy for XPath). The planned rewrite supersedes some of this (no recursive hydration, no two-pointer — the Document pointer alone suffices because Node objects stay as C handles).
96
- - `docs/BUILD.md` — CMake build reference for libleptris itself.
97
-
98
- ## Conventions (project-specific)
99
-
100
- - **Autoload, not require_relative.** TODO 3 is explicit: `lib/leptris.rb` → `autoload :XML, 'leptris/xml'`; `lib/leptris/xml.rb` → `autoload :Document, 'leptris/xml/document'`. Autoload entries live in the **immediate parent namespace's file** (create that file if missing). The current `lib/leptris.rb` uses `require_relative` — when implementing TODO 3, do not retrofit require_relative into the new layout.
101
- - **No `instance_variable_set`/`_get` cross-object.** TODO 3 is explicit. The current `lib/leptris.rb` and `lib/leptris/ffi/bridge.rb` use `instance_variable_get(:@_c_ptr)` heavily — that pattern is debt to migrate, not a model to copy. In the rewrite, expose `c_ptr`/`document` as public `attr_reader`s and access via those.
102
- - **No `respond_to?` type checks.** Use `is_a?`. The current `lib/leptris.rb#xpath_evaluate` checks `context_node != doc` to disambiguate — fine. Don't add `respond_to?(:c_ptr)` style checks.
103
- - **No doubles in specs.** The existing `spec/leptris/` specs use real model instances (XML strings → `Leptris.parse` → real `Document`/`Element`/`NodeSet`). Keep it that way.
104
- - **Forward compatibility:** Keep `Leptris.parse` / `Leptris.parse_file` as the existing top-level API during the rewrite. The new `Leptris::XML.parse` may coexist.
65
+ ### Ownership and seams
66
+
67
+ - `Document` owns C memory (explicit `#free` or GC finalizer; wrapper
68
+ cache is a strong Hash cleared on free). Every other class is a
69
+ borrowed handle valid while its Document lives.
70
+ - All wrapper construction goes through `Node.wrap` wrapper
71
+ identity (`doc.root.equal?(doc.root)`) is guaranteed by the
72
+ per-document cache.
73
+ - Status checks go through `FFI.check_status`; C `char*` returns go
74
+ through `FFI.read_owned_string`; array wire format (both
75
+ directions) goes through `CStringArray`. Never hand-roll these.
76
+ - The release workflow's publish loop is idempotent (already-published
77
+ gems are skipped).
78
+
79
+ ## Conventions
80
+
81
+ - Autoload only no `require_relative` inside `lib/`.
82
+ - No `instance_variable_set`/`_get` across objects; no `respond_to?`
83
+ type checks; specs use real documents, never doubles.
84
+ - New C surface: attach in `ffi.rb`, sugar where it earns its keep,
85
+ specs against a locally built library, CHANGELOG, lockstep release.
86
+ - All changes via PRs; no AI attribution; `git add` explicit paths.
87
+
88
+ ## Reference
89
+
90
+ - libleptris public headers (`src/include/leptris/*.h`) are the
91
+ contract; when symbols change, bump lockstep and audit
92
+ attached-vs-exported (`nm -gU` on a fresh build).
93
+ - Upstream issues worth tracking live at leptris/leptris.
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.5.0"
11
+ LIBLEPTRIS_VERSION = "1.6.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.5.0"
4
+ VERSION = "1.6.1"
5
5
  end
@@ -8,6 +8,7 @@ class Leptris::XML::CDATA < Leptris::XML::Text
8
8
  end
9
9
 
10
10
  def content=(new_content)
11
+ ensure_writable!
11
12
  Leptris::XML::FFI.check_status(
12
13
  Leptris::XML::FFI.leptris_cdata_node_set_content(@c_ptr, new_content.to_s))
13
14
  new_content
@@ -8,6 +8,7 @@ class Leptris::XML::Comment < Leptris::XML::Node
8
8
  end
9
9
 
10
10
  def content=(new_content)
11
+ ensure_writable!
11
12
  Leptris::XML::FFI.check_status(
12
13
  Leptris::XML::FFI.leptris_comment_node_set_content(@c_ptr, new_content.to_s))
13
14
  new_content
@@ -17,6 +17,7 @@ class Leptris::XML::Document
17
17
  def initialize(c_ptr = nil, freed = Freed.new(:alive))
18
18
  @c_ptr = c_ptr
19
19
  @freed = freed
20
+ @readonly = false
20
21
  # Per-document STRONG cache for Node wrappers, keyed on c_ptr
21
22
  # address. Every wrapper is created through Node.wrap, which is the
22
23
  # single construction path, so the same C node always yields the
@@ -33,38 +34,38 @@ class Leptris::XML::Document
33
34
  @wrapper_cache = {}
34
35
  end
35
36
 
36
- def self.parse(xml_or_io, options: nil)
37
+ def self.parse(xml_or_io, options: nil, readonly: false)
37
38
  xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
38
39
  if xml.empty?
39
40
  raise Leptris::XML::ParseError, "empty input"
40
41
  end
41
42
  flags = resolve_flags(options)
42
- status_ptr = ::FFI::MemoryPointer.new(:int)
43
+ # The status out-param is nullable; the thread-local last error
44
+ # carries failure detail, and skipping the per-parse MemoryPointer
45
+ # is measurable on small documents.
43
46
  raw =
44
47
  if flags.zero?
45
- Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, status_ptr)
48
+ Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
46
49
  else
47
50
  Leptris::XML::FFI.leptris_parse_string_flags(
48
- xml, xml.bytesize, flags, status_ptr)
51
+ xml, xml.bytesize, flags, nil)
49
52
  end
50
53
  if raw.null?
51
- status = status_ptr.read_int
52
54
  raise Leptris::XML::ParseError,
53
- "leptris_parse_string failed (status=#{status}): " +
54
- Leptris::XML::FFI.status_message(status)
55
+ "leptris_parse_string failed: " +
56
+ Leptris::XML::FFI.leptris_last_error.to_s
55
57
  end
56
- wrap(raw)
58
+ wrap(raw).tap { |doc| doc.readonly! if readonly }
57
59
  end
58
60
 
59
- def self.parse_file(path)
60
- status_ptr = ::FFI::MemoryPointer.new(:int)
61
- raw = Leptris::XML::FFI.leptris_parse_file(path, status_ptr)
61
+ def self.parse_file(path, readonly: false)
62
+ raw = Leptris::XML::FFI.leptris_parse_file(path, nil)
62
63
  if raw.null?
63
- status = status_ptr.read_int
64
64
  raise Leptris::XML::ParseError,
65
- "leptris_parse_file failed (status=#{status})"
65
+ "leptris_parse_file failed: " +
66
+ Leptris::XML::FFI.leptris_last_error.to_s
66
67
  end
67
- wrap(raw)
68
+ wrap(raw).tap { |doc| doc.readonly! if readonly }
68
69
  end
69
70
 
70
71
  # Create an empty document (no root element) backed by its own memory
@@ -234,6 +235,38 @@ class Leptris::XML::Document
234
235
  self
235
236
  end
236
237
 
238
+ # Document-level processing instructions (not tree nodes):
239
+ # an array of [target, data] pairs in document order.
240
+ def processing_instructions
241
+ count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
242
+ count.times.map do |i|
243
+ [Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
244
+ Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)]
245
+ end
246
+ end
247
+
248
+ # Append a document-level processing instruction. Returns self.
249
+ def add_pi(target, data = "")
250
+ witness = Leptris::XML::FFI.leptris_document_add_pi(
251
+ @c_ptr, target.to_s, data.to_s)
252
+ raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
253
+ self
254
+ end
255
+
256
+ # Marks the document read-only: tree mutations raise
257
+ # Leptris::XML::ReadOnlyError, and read paths memoize aggressively
258
+ # (names, content, children, attributes) since they can never go
259
+ # stale. The C document is also frozen (advisory upstream). One-way.
260
+ def readonly!
261
+ Leptris::XML::FFI.leptris_document_freeze(@c_ptr)
262
+ @readonly = true
263
+ self
264
+ end
265
+
266
+ def readonly?
267
+ @readonly == true
268
+ end
269
+
237
270
  # The most recent error recorded against this document, or nil.
238
271
  def last_error
239
272
  msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr)
@@ -2,22 +2,27 @@
2
2
 
3
3
  class Leptris::XML::Element < Leptris::XML::Node
4
4
  def name
5
- Leptris::XML::FFI.leptris_element_name(@c_ptr)
5
+ @name ||= Leptris::XML::FFI.leptris_element_name(@c_ptr)
6
6
  end
7
7
  alias_method :node_name, :name
8
8
 
9
9
  def name=(new_name)
10
+ ensure_writable!
10
11
  Leptris::XML::FFI.check_status(
11
12
  Leptris::XML::FFI.leptris_element_set_name(@c_ptr, new_name))
12
- new_name
13
+ @name = new_name
13
14
  end
14
15
  alias_method :node_name=, :name=
15
16
 
16
17
  def content
17
- Leptris::XML::FFI.leptris_element_text(@c_ptr)
18
+ return @content if readonly_cached?(:@content)
19
+ Leptris::XML::FFI.leptris_element_text(@c_ptr).tap do |text|
20
+ @content = text if @document&.readonly?
21
+ end
18
22
  end
19
23
 
20
24
  def content=(new_content)
25
+ ensure_writable!
21
26
  Leptris::XML::FFI.check_status(
22
27
  Leptris::XML::FFI.leptris_element_set_text(@c_ptr, new_content.to_s))
23
28
  new_content
@@ -30,6 +35,7 @@ class Leptris::XML::Element < Leptris::XML::Node
30
35
  alias_method :get_attribute, :[]
31
36
 
32
37
  def []=(key, value)
38
+ ensure_writable!
33
39
  Leptris::XML::FFI.check_status(
34
40
  Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s))
35
41
  value
@@ -42,6 +48,7 @@ class Leptris::XML::Element < Leptris::XML::Node
42
48
  alias_method :has_attribute?, :key?
43
49
 
44
50
  def remove_attribute(name)
51
+ ensure_writable!
45
52
  Leptris::XML::FFI.check_status(
46
53
  Leptris::XML::FFI.leptris_element_remove_attribute(@c_ptr, name.to_s))
47
54
  self
@@ -72,8 +79,10 @@ class Leptris::XML::Element < Leptris::XML::Node
72
79
  end
73
80
 
74
81
  def attributes
82
+ return @attributes if readonly_cached?(:@attributes)
75
83
  result = {}
76
84
  each_attribute { |attr| result[attr.name] = attr }
85
+ @attributes = result if @document&.readonly?
77
86
  result
78
87
  end
79
88
 
@@ -88,30 +97,35 @@ class Leptris::XML::Element < Leptris::XML::Node
88
97
  end
89
98
 
90
99
  def prepend_child(node)
100
+ ensure_writable!
91
101
  Leptris::XML::FFI.check_status(
92
102
  Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
93
103
  node
94
104
  end
95
105
 
96
106
  def add_next_sibling(node)
107
+ ensure_writable!
97
108
  Leptris::XML::FFI.check_status(
98
109
  Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
99
110
  node
100
111
  end
101
112
 
102
113
  def add_previous_sibling(node)
114
+ ensure_writable!
103
115
  Leptris::XML::FFI.check_status(
104
116
  Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
105
117
  node
106
118
  end
107
119
 
108
120
  def remove_child(node)
121
+ ensure_writable!
109
122
  Leptris::XML::FFI.check_status(
110
123
  Leptris::XML::FFI.leptris_element_remove_child(@c_ptr, node.c_ptr))
111
124
  node
112
125
  end
113
126
 
114
127
  def children=(node_or_nodes)
128
+ ensure_writable!
115
129
  # Remove existing children, then attach the new ones in source order.
116
130
  Leptris::XML::FFI.check_status(
117
131
  Leptris::XML::FFI.leptris_element_remove_children(@c_ptr))
@@ -168,6 +182,7 @@ class Leptris::XML::Element < Leptris::XML::Node
168
182
  alias_method :clone, :dup
169
183
 
170
184
  def add_child(node_or_markup)
185
+ ensure_writable!
171
186
  case node_or_markup
172
187
  when Leptris::XML::Node
173
188
  Leptris::XML::FFI.check_status(
@@ -243,6 +258,7 @@ class Leptris::XML::Element < Leptris::XML::Node
243
258
  alias_method :c14n, :canonicalize
244
259
 
245
260
  def add_namespace_definition(prefix, href)
261
+ ensure_writable!
246
262
  Leptris::XML::FFI.check_status(
247
263
  Leptris::XML::FFI.leptris_element_add_namespace_definition(
248
264
  @c_ptr, prefix.to_s, href.to_s))
@@ -251,12 +267,14 @@ class Leptris::XML::Element < Leptris::XML::Node
251
267
  alias_method :add_namespace, :add_namespace_definition
252
268
 
253
269
  def default_namespace=(href)
270
+ ensure_writable!
254
271
  Leptris::XML::FFI.check_status(
255
272
  Leptris::XML::FFI.leptris_element_set_default_namespace(@c_ptr, href.to_s))
256
273
  href
257
274
  end
258
275
 
259
276
  def remove_namespace_definition(prefix)
277
+ ensure_writable!
260
278
  Leptris::XML::FFI.check_status(
261
279
  Leptris::XML::FFI.leptris_element_remove_namespace_definition(@c_ptr, prefix.to_s))
262
280
  self