leptris 1.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +76 -87
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/cdata.rb +1 -0
- data/lib/leptris/xml/comment.rb +1 -0
- data/lib/leptris/xml/document.rb +29 -14
- data/lib/leptris/xml/element.rb +21 -3
- data/lib/leptris/xml/ffi.rb +3 -13
- data/lib/leptris/xml/node.rb +32 -10
- data/lib/leptris/xml/node_set.rb +6 -5
- data/lib/leptris/xml/processing_instruction.rb +2 -0
- data/lib/leptris/xml/searchable.rb +1 -1
- data/lib/leptris/xml/serialization.rb +20 -2
- data/lib/leptris/xml/text.rb +1 -0
- data/lib/leptris/xml.rb +3 -2
- metadata +1 -36
- data/TODO.impl/01-architecture.md +0 -217
- data/TODO.impl/02-ffi-declarations.md +0 -236
- data/TODO.impl/03-document-node-element-nodeset.md +0 -382
- data/TODO.impl/04-sax-parser.md +0 -203
- data/TODO.impl/05-serialize-c14n-memory-specs-css.md +0 -276
- data/docs/ARCHITECTURE.adoc +0 -559
- data/docs/BUILD.md +0 -395
- data/docs/ERROR_MESSAGES.md +0 -458
- data/docs/FFI_ARCHITECTURE.md +0 -439
- data/docs/FUTURE_VISION.md +0 -303
- data/docs/GITHUB_ACTIONS.md +0 -293
- data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +0 -459
- data/docs/PERFORMANCE.adoc +0 -668
- data/docs/PERFORMANCE.md +0 -448
- data/docs/RELEASE_NOTES_v1.0.0.md +0 -515
- data/docs/XPATH_SPEC_COMPLIANCE.md +0 -298
- data/docs/completion/leptris.bash +0 -86
- data/docs/completion/leptris.zsh +0 -74
- data/docs/man/leptris-format.1 +0 -227
- data/docs/man/leptris-parse.1 +0 -178
- data/docs/man/leptris-xpath.1 +0 -312
- data/docs/man/leptris.1 +0 -160
- data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +0 -217
- data/docs/v0.9.0_RELEASE_SUMMARY.md +0 -281
- data/docs/v1.0.0_CONTINUATION_PLAN.md +0 -172
- data/docs/v1.0.0_CONTINUATION_PROMPT.md +0 -382
- data/docs/v1.0.0_SESSION_6_CONTINUATION.md +0 -434
- data/docs/v1.0.0_SESSION_6_PROMPT.md +0 -231
- data/docs/v1.0.0_STATUS_TRACKER.md +0 -224
- data/docs/v1.1.0_CONTINUATION_PLAN.md +0 -299
- data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +0 -201
- data/docs/v1.1.0_SESSION_3_PROMPT.md +0 -223
- data/docs/v1.1.0_STATUS_TRACKER.md +0 -355
- data/docs/xml-performance.adoc +0 -115
- data/docs/xpath-performance.adoc +0 -379
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2ea74c449e12497abc87b87d5c6196524540a586e077913e63f94a3b103fa3f
|
|
4
|
+
data.tar.gz: 7f80cecd901a98c265539de2be30930d0cd58276f5425defa99a4917d89e4573
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4267ae7e0d05eb64ddb9121af9d1ba5e99edc122cdcc338228b5b6660ece73e1d26ebde4b5a28885d512d892152feec0673bb06f2ae29fec1403885445ebcd85
|
|
7
|
+
data.tar.gz: 755e4b1d239aa5fbdac57c15ffbb7c6876013bf9057e0eee2a893c0d0f417331ae469eb8ecc9ac6148425784fe38f06e4a7869b199bf3745222413ea9c6968f6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ 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
|
+
|
|
8
33
|
## [1.6.0] - 2026-08-24
|
|
9
34
|
|
|
10
35
|
Lockstep with libleptris 1.6.0 — the moxml-adapter blockers fixed:
|
|
@@ -26,6 +51,31 @@ release: pull (StAX) API, bounded iterparse, compiled XPath
|
|
|
26
51
|
expressions, per-parse options, truthful serialization encoding
|
|
27
52
|
declarations. Binding-side adoption of the new APIs follows.
|
|
28
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
|
+
|
|
29
79
|
## [1.6.0] - 2026-08-24
|
|
30
80
|
|
|
31
81
|
Lockstep with libleptris 1.6.0 (v1.4/v1.5 were fix releases with no
|
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
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
bundle exec
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
Leptris.parse /
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
- `
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/cdata.rb
CHANGED
data/lib/leptris/xml/comment.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
54
|
-
Leptris::XML::FFI.
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -252,6 +253,20 @@ class Leptris::XML::Document
|
|
|
252
253
|
self
|
|
253
254
|
end
|
|
254
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
|
+
|
|
255
270
|
# The most recent error recorded against this document, or nil.
|
|
256
271
|
def last_error
|
|
257
272
|
msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr)
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -341,8 +341,6 @@ module Leptris
|
|
|
341
341
|
[:leptris_xpath_result], :int
|
|
342
342
|
attach_function :leptris_xpath_result_count,
|
|
343
343
|
[:leptris_xpath_result], :size_t
|
|
344
|
-
attach_function :leptris_xpath_result_get,
|
|
345
|
-
[:leptris_xpath_result, :size_t], :leptris_element
|
|
346
344
|
# Mixed nodesets (v1.1.0): unlike result_get (elements only),
|
|
347
345
|
# get_node returns the node whatever its kind; node_kind reports
|
|
348
346
|
# which (element / synthetic attribute / text / other).
|
|
@@ -359,10 +357,8 @@ module Leptris
|
|
|
359
357
|
# a heap string the library frees.
|
|
360
358
|
attach_function :leptris_xpath_register_function,
|
|
361
359
|
[:leptris_document, :string, :pointer, :pointer], :leptris_status
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
# v1.3.0: copies ALL node kinds (the legacy accessor drops
|
|
365
|
-
# non-element entries) and can also report per-entry kinds.
|
|
360
|
+
# v1.3.0: copies ALL node kinds in one call, optionally
|
|
361
|
+
# reporting per-entry kinds.
|
|
366
362
|
attach_function :leptris_xpath_result_get_nodes_ex,
|
|
367
363
|
[:leptris_xpath_result, :pointer, :pointer, :size_t], :size_t
|
|
368
364
|
attach_function :leptris_xpath_result_boolean,
|
|
@@ -393,10 +389,8 @@ module Leptris
|
|
|
393
389
|
[], :leptris_xpath_ns_set
|
|
394
390
|
attach_function :leptris_xpath_ns_set_free,
|
|
395
391
|
[:leptris_xpath_ns_set], :void
|
|
396
|
-
attach_function :leptris_xpath_ns_set_add,
|
|
397
|
-
[:leptris_xpath_ns_set, :string, :string], :leptris_status
|
|
398
392
|
# One-call constructor: flat array of 2*pair_count alternating
|
|
399
|
-
# prefix/URI strings
|
|
393
|
+
# prefix/URI strings.
|
|
400
394
|
attach_function :leptris_xpath_ns_set_new_from_pairs,
|
|
401
395
|
[:pointer, :size_t], :leptris_xpath_ns_set
|
|
402
396
|
attach_function :leptris_xpath_eval_ns,
|
|
@@ -457,10 +451,6 @@ module Leptris
|
|
|
457
451
|
|
|
458
452
|
attach_function :leptris_document_serialize,
|
|
459
453
|
[:leptris_document, :pointer], :pointer
|
|
460
|
-
# Legacy simple serializer, declared retroactively in v1.1.0.
|
|
461
|
-
# Prefer leptris_document_serialize; bound for API completeness.
|
|
462
|
-
attach_function :leptris_serialize_document,
|
|
463
|
-
[:leptris_document], :pointer
|
|
464
454
|
attach_function :leptris_document_get_dtd,
|
|
465
455
|
[:leptris_document], :pointer
|
|
466
456
|
attach_function :leptris_element_serialize,
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
class Leptris::XML::Node
|
|
4
4
|
attr_reader :c_ptr, :document
|
|
5
5
|
|
|
6
|
-
def initialize(c_ptr, document, parent: nil)
|
|
6
|
+
def initialize(c_ptr, document, parent: nil, node_type: nil)
|
|
7
7
|
@c_ptr = c_ptr
|
|
8
8
|
@document = document
|
|
9
9
|
@parent = parent
|
|
10
|
+
# wrap() already calls leptris_node_get_type for dispatch; reusing
|
|
11
|
+
# the result makes every predicate and #type call FFI-free.
|
|
12
|
+
@node_type = node_type
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def self.wrap(c_ptr, document, parent: nil)
|
|
@@ -18,20 +21,21 @@ class Leptris::XML::Node
|
|
|
18
21
|
return cached
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
node_type = Leptris::XML::FFI.leptris_node_get_type(c_ptr)
|
|
21
25
|
node =
|
|
22
|
-
case
|
|
26
|
+
case node_type
|
|
23
27
|
when Leptris::XML::FFI::NODE_ELEMENT
|
|
24
|
-
Leptris::XML::Element.new(c_ptr, document, parent: parent)
|
|
28
|
+
Leptris::XML::Element.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
25
29
|
when Leptris::XML::FFI::NODE_TEXT
|
|
26
|
-
Leptris::XML::Text.new(c_ptr, document, parent: parent)
|
|
30
|
+
Leptris::XML::Text.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
27
31
|
when Leptris::XML::FFI::NODE_COMMENT
|
|
28
|
-
Leptris::XML::Comment.new(c_ptr, document, parent: parent)
|
|
32
|
+
Leptris::XML::Comment.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
29
33
|
when Leptris::XML::FFI::NODE_CDATA
|
|
30
|
-
Leptris::XML::CDATA.new(c_ptr, document, parent: parent)
|
|
34
|
+
Leptris::XML::CDATA.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
31
35
|
when Leptris::XML::FFI::NODE_PI
|
|
32
|
-
Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent)
|
|
36
|
+
Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
33
37
|
else
|
|
34
|
-
new(c_ptr, document, parent: parent)
|
|
38
|
+
new(c_ptr, document, parent: parent, node_type: node_type)
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
document.wrapper_cache[c_ptr.address] = node if document
|
|
@@ -58,7 +62,7 @@ class Leptris::XML::Node
|
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def type
|
|
61
|
-
Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
|
|
65
|
+
@node_type ||= Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
|
|
62
66
|
end
|
|
63
67
|
alias_method :node_type, :type
|
|
64
68
|
|
|
@@ -78,6 +82,14 @@ class Leptris::XML::Node
|
|
|
78
82
|
Leptris::XML::Node.wrap(ptr, @document)
|
|
79
83
|
end
|
|
80
84
|
|
|
85
|
+
# Raises ReadOnlyError when the owning document was marked readonly.
|
|
86
|
+
def ensure_writable!
|
|
87
|
+
if @document&.readonly?
|
|
88
|
+
raise Leptris::XML::ReadOnlyError,
|
|
89
|
+
"document is readonly — mutation attempted on #{inspect}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
81
93
|
def line
|
|
82
94
|
Leptris::XML::FFI.leptris_node_line(@c_ptr)
|
|
83
95
|
end
|
|
@@ -95,13 +107,18 @@ class Leptris::XML::Node
|
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def children
|
|
110
|
+
# Immutable in readonly mode: the walk (first_child + one
|
|
111
|
+
# next_sibling per child) plus wrapper construction is paid once.
|
|
112
|
+
return @children if readonly_cached?(:@children)
|
|
98
113
|
nodes = []
|
|
99
114
|
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
100
115
|
until ptr.nil? || ptr.null?
|
|
101
116
|
nodes << Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
102
117
|
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
103
118
|
end
|
|
104
|
-
Leptris::XML::NodeSet.new(@document, nodes)
|
|
119
|
+
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
120
|
+
@children = result if @document&.readonly?
|
|
121
|
+
result
|
|
105
122
|
end
|
|
106
123
|
|
|
107
124
|
def next_sibling
|
|
@@ -150,6 +167,7 @@ class Leptris::XML::Node
|
|
|
150
167
|
end
|
|
151
168
|
|
|
152
169
|
def unlink
|
|
170
|
+
ensure_writable!
|
|
153
171
|
Leptris::XML::FFI.check_status(
|
|
154
172
|
Leptris::XML::FFI.leptris_node_unlink(@c_ptr))
|
|
155
173
|
@parent = nil
|
|
@@ -212,6 +230,10 @@ class Leptris::XML::Node
|
|
|
212
230
|
|
|
213
231
|
protected
|
|
214
232
|
|
|
233
|
+
def readonly_cached?(ivar)
|
|
234
|
+
@document&.readonly? && instance_variable_defined?(ivar)
|
|
235
|
+
end
|
|
236
|
+
|
|
215
237
|
def as_element_or_self
|
|
216
238
|
is_a?(Leptris::XML::Element) ? self : nil
|
|
217
239
|
end
|