moxml 0.5.12 → 0.5.13
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/docs/_pages/adapter-protocol.adoc +113 -0
- data/docs/_pages/adapters/index.adoc +11 -5
- data/docs/_pages/adapters/leptris.adoc +69 -0
- data/docs/_pages/adapters/libxml.adoc +1 -1
- data/docs/_pages/adapters/nokogiri.adoc +1 -1
- data/docs/_pages/conversion-apis.adoc +78 -0
- data/docs/_pages/index.adoc +6 -0
- data/lib/moxml/adapter/leptris/document_parts.rb +208 -0
- data/lib/moxml/adapter/leptris/markers.rb +51 -0
- data/lib/moxml/adapter/leptris/materialize.rb +110 -0
- data/lib/moxml/adapter/leptris/sax_bridge.rb +56 -0
- data/lib/moxml/adapter/leptris/serialize.rb +127 -0
- data/lib/moxml/adapter/leptris.rb +14 -518
- data/lib/moxml/adapter/libxml/serialize.rb +497 -0
- data/lib/moxml/adapter/libxml.rb +20 -462
- data/lib/moxml/materializer.rb +39 -17
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_spec.rb +14 -4
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f890c98078986e7be37d59762c08c6387cb173a7260212be428c705e043bd1a
|
|
4
|
+
data.tar.gz: 6a5dae862bb917c2906581412c33f0cd9f4506112ba5f24a146feed9f2dc0371
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8acaee20452b9d74ba026cc4fce32d6a8911af51afee523bf474e831786d9e71c31e089360d023c17a62c8610081a8ad4d55b774261279e56966942ab26b040
|
|
7
|
+
data.tar.gz: c9e1d227bca2d9f7f5771604de905b5efec5a61451e01b69919ec315484673cafadbe65345423fd65633fcdc1d8e8197de58d6822f5bc42a6fa6fe253ec4b7f3
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Adapter protocol
|
|
3
|
+
nav_order: 46
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
= The adapter protocol
|
|
7
|
+
|
|
8
|
+
The seam between moxml's wrapper layer (`Moxml::Node` and friends) and the
|
|
9
|
+
engine adapters. Every adapter is a class under `Moxml::Adapter` implementing
|
|
10
|
+
class methods over its library's native nodes; wrappers dispatch through
|
|
11
|
+
`context.config.adapter` and never touch a native directly.
|
|
12
|
+
|
|
13
|
+
The contract is pinned by the shared example groups under
|
|
14
|
+
`spec/moxml/adapter/shared_examples/` (the adapter contract) and the
|
|
15
|
+
consistency suite — behavior there is authoritative when this page and the
|
|
16
|
+
code disagree.
|
|
17
|
+
|
|
18
|
+
== Method groups
|
|
19
|
+
|
|
20
|
+
Parse and document lifecycle::
|
|
21
|
+
`parse(xml, options, context)`, `create_document`, `create` factories
|
|
22
|
+
(`create_native_element`, `create_native_text`, `create_native_cdata`,
|
|
23
|
+
`create_native_comment`, `create_native_processing_instruction`,
|
|
24
|
+
`create_native_declaration`, `create_native_doctype`,
|
|
25
|
+
`create_native_entity_reference`, `create_native_namespace`),
|
|
26
|
+
`free_document` (#134; no-op on GC-managed engines).
|
|
27
|
+
|
|
28
|
+
Tree navigation::
|
|
29
|
+
`children`, `parent`, `next_sibling`, `previous_sibling`, `root`,
|
|
30
|
+
`set_root`, `document`, `duplicate_node`. `children` on a document
|
|
31
|
+
lists document-level parts in document order where the engine exposes
|
|
32
|
+
them (Nokogiri-shaped contract).
|
|
33
|
+
|
|
34
|
+
Node typing and names::
|
|
35
|
+
`node_type`, `node_name`, `set_node_name`, `attribute_name`.
|
|
36
|
+
|
|
37
|
+
Attributes::
|
|
38
|
+
`get_attribute` / `get_attribute_value` (the raw layer beneath
|
|
39
|
+
`Moxml::AttributeResolver` — expanded-name resolution lives above the
|
|
40
|
+
adapter), `set_attribute_value`, `remove_attribute`,
|
|
41
|
+
`patch_node`/`unpatch_node` for engines needing wrapper natives
|
|
42
|
+
(libxml, ox).
|
|
43
|
+
|
|
44
|
+
Namespaces::
|
|
45
|
+
`namespace`, `namespace_definitions` (the element's own declarations),
|
|
46
|
+
`in_scope_namespaces`, `set_namespace`, `remove_namespace`.
|
|
47
|
+
|
|
48
|
+
Mutation::
|
|
49
|
+
`add_child`, `add_next_sibling`, `add_previous_sibling`, `remove`,
|
|
50
|
+
`replace`, `replace_children`. Return contract: methods that can swap
|
|
51
|
+
the native (`set_attribute_name`, `set_namespace`, adoptions) return
|
|
52
|
+
the tracked native; wrappers re-key through `refresh_native!`.
|
|
53
|
+
|
|
54
|
+
Serialization::
|
|
55
|
+
`serialize(node, options)`. Options: `indent`, `encoding`,
|
|
56
|
+
`declaration`, `no_declaration`, `expand_empty`, `line_ending`.
|
|
57
|
+
Entity-marker restoration is owned by the wrapper layer
|
|
58
|
+
(`Node#to_xml`), guarded by `entity_bearing?`.
|
|
59
|
+
|
|
60
|
+
XPath::
|
|
61
|
+
`xpath(expression, namespaces)`. Engines without a native engine use
|
|
62
|
+
moxml's pure-Ruby XPath 1.0 engine (`Moxml::XPath`).
|
|
63
|
+
|
|
64
|
+
== Capability predicates
|
|
65
|
+
|
|
66
|
+
The wrapper layer probes adapters rather than assuming:
|
|
67
|
+
|
|
68
|
+
`patches_children?`::
|
|
69
|
+
True when `children` results need per-child `patch_node` rewriting
|
|
70
|
+
(libxml, ox). Others skip the identity map over every child list.
|
|
71
|
+
|
|
72
|
+
`wrappers_recyclable?`::
|
|
73
|
+
False for engines that mint fresh Ruby objects per access (libxml) —
|
|
74
|
+
the context's wrapper identity map opts out.
|
|
75
|
+
|
|
76
|
+
`entity_bearing?(native)`::
|
|
77
|
+
Whether the subtree can contain entity markers; gates the
|
|
78
|
+
post-serialize restore scan.
|
|
79
|
+
|
|
80
|
+
`bulk_materialize?`::
|
|
81
|
+
Whether the adapter offers a bulk path for `Moxml::Materializer`;
|
|
82
|
+
`materialize_records` yields flattened records, returning nil falls
|
|
83
|
+
back to the generic wrapper walk.
|
|
84
|
+
|
|
85
|
+
Constants like `DOC_NODE_SUPPORTED` / `DTDATTR_SUPPORTED` /
|
|
86
|
+
`TRAVERSE_SUBTREE_BOUNDED` (Leptris) follow the same idea: one probe
|
|
87
|
+
per engine capability, computed at load.
|
|
88
|
+
|
|
89
|
+
== Return shapes
|
|
90
|
+
|
|
91
|
+
* Mutations return the tracked native (or nil when not applicable) —
|
|
92
|
+
never a wrapper.
|
|
93
|
+
* `children` returns an array of natives; the wrapper layer builds the
|
|
94
|
+
`NodeSet`.
|
|
95
|
+
* `xpath` returns `Array<native>` or a scalar for non-node results.
|
|
96
|
+
* Marker-bearing text (leptris) is split by the adapter into
|
|
97
|
+
text/entity-reference pseudo-natives before the wrapper sees it.
|
|
98
|
+
|
|
99
|
+
== Writing an adapter
|
|
100
|
+
|
|
101
|
+
Subclass `Moxml::Adapter::Base`, implement the groups above, and
|
|
102
|
+
include the shared examples:
|
|
103
|
+
|
|
104
|
+
[source,ruby]
|
|
105
|
+
----
|
|
106
|
+
RSpec.describe Moxml::Adapter::Mine do
|
|
107
|
+
it_behaves_like "xml adapter"
|
|
108
|
+
it_behaves_like "adapter contract"
|
|
109
|
+
end
|
|
110
|
+
----
|
|
111
|
+
|
|
112
|
+
The contract suite is the test surface: if it passes, the wrapper
|
|
113
|
+
layer's behavior is covered.
|
|
@@ -14,15 +14,21 @@ how to choose the right adapter for your needs.
|
|
|
14
14
|
=== What are adapters?
|
|
15
15
|
|
|
16
16
|
Moxml uses an adapter pattern to provide a unified interface over different
|
|
17
|
-
XML processing libraries. Each adapter wraps a specific XML library (
|
|
18
|
-
LibXML, Oga, REXML, or Ox) and provides consistent behavior through
|
|
19
|
-
API.
|
|
17
|
+
XML processing libraries. Each adapter wraps a specific XML library (Leptris,
|
|
18
|
+
Nokogiri, LibXML, Oga, REXML, or Ox) and provides consistent behavior through
|
|
19
|
+
Moxml's API. See link:../adapter-protocol[the adapter protocol] for the seam
|
|
20
|
+
itself.
|
|
20
21
|
|
|
21
22
|
=== Available adapters
|
|
22
23
|
|
|
24
|
+
link:leptris[Leptris adapter]::
|
|
25
|
+
Pure-C99 XML engine, the default on CRuby when the installed binding
|
|
26
|
+
supports programmatic documents — fastest on every measured operation,
|
|
27
|
+
with native XPath 1.0, SAX, and deterministic memory release.
|
|
28
|
+
|
|
23
29
|
link:nokogiri[Nokogiri adapter]::
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
The industry-standard XML library and the fallback default when Leptris
|
|
31
|
+
is not installed. Excellent performance with full XPath 1.0 support.
|
|
26
32
|
|
|
27
33
|
link:libxml[LibXML adapter]::
|
|
28
34
|
Alternative to Nokogiri using native libxml2 bindings. Excellent performance
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Leptris
|
|
3
|
+
parent: Adapters
|
|
4
|
+
nav_order: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
== Leptris adapter
|
|
8
|
+
|
|
9
|
+
Leptris is a pure-C99 XML 1.0 parser with an FFI binding (the
|
|
10
|
+
https://github.com/leptris/leptris-ruby[leptris-ruby] gem). It is the
|
|
11
|
+
preferred default on CRuby: fastest on every measured moxml operation,
|
|
12
|
+
with a native XPath 1.0 engine, SAX, and canonicalization.
|
|
13
|
+
|
|
14
|
+
== Default resolution
|
|
15
|
+
|
|
16
|
+
Leptris becomes the default when the installed binding supports
|
|
17
|
+
programmatic document construction (`Leptris::XML::Document.create`,
|
|
18
|
+
leptris >= 1.3). Otherwise the default falls back to Nokogiri. Under
|
|
19
|
+
Opal the default is rexml (stock oga requires its C extension and
|
|
20
|
+
libleptris does not build for Opal).
|
|
21
|
+
|
|
22
|
+
[source,ruby]
|
|
23
|
+
----
|
|
24
|
+
Moxml.new # => Leptris context when capable
|
|
25
|
+
ctx.config.adapter_name # => :leptris / :nokogiri / :rexml (Opal)
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
== Parse options
|
|
29
|
+
|
|
30
|
+
`readonly: true`::
|
|
31
|
+
The binding memoizes every read and refuses mutations — the
|
|
32
|
+
parse-and-read lifecycle for multi-pass consumers (comparison, diff,
|
|
33
|
+
signatures). Repeat attribute reads on a large element drop by
|
|
34
|
+
orders of magnitude. Builder documents stay read-write.
|
|
35
|
+
|
|
36
|
+
`dtdattr: true`::
|
|
37
|
+
Materialize DTD `<!ATTLIST>` defaults. Off by default, matching
|
|
38
|
+
libxml2/Nokogiri/REXML semantics (libleptris >= 1.9.8).
|
|
39
|
+
|
|
40
|
+
`strict: true`::
|
|
41
|
+
Raise `Moxml::ParseError` on malformed input instead of returning an
|
|
42
|
+
empty document.
|
|
43
|
+
|
|
44
|
+
== Lifecycle
|
|
45
|
+
|
|
46
|
+
`Document#free` releases the C tree deterministically — batch
|
|
47
|
+
workloads parsing thousands of documents otherwise hold native memory
|
|
48
|
+
until GC finalizers run. Access after `#free` raises the binding's
|
|
49
|
+
use-after-free error.
|
|
50
|
+
|
|
51
|
+
== Document model
|
|
52
|
+
|
|
53
|
+
Documents expose the libxml2-shaped document node: prolog PIs and
|
|
54
|
+
comments, the root element, epilog PIs and comments, in document
|
|
55
|
+
order (leptris >= 1.9.26). The DOCTYPE round-trips with its internal
|
|
56
|
+
subset. See link:../conversion-apis[Conversion and memory lifecycle APIs]
|
|
57
|
+
for `materialize` — the flattened-record conversion path with a bulk
|
|
58
|
+
implementation over one `leptris_node_traverse` call.
|
|
59
|
+
|
|
60
|
+
== Known limitations (tracked upstream)
|
|
61
|
+
|
|
62
|
+
* Tab indentation (`indent_text`) needs a serialize-time parameter in
|
|
63
|
+
libleptris; post-hoc rewriting would corrupt mixed content.
|
|
64
|
+
* Document-level PIs are read-only natives (`target=`/`data=`/
|
|
65
|
+
`unlink` rejected); write-through and removal are pending C
|
|
66
|
+
surface (leptris/leptris#612).
|
|
67
|
+
* A PI preceding the DOCTYPE reorders on serialization — libleptris
|
|
68
|
+
normalizes DOCTYPE-first and the document node does not record its
|
|
69
|
+
relative position.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Conversion and memory lifecycle APIs
|
|
3
|
+
nav_order: 45
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
= Conversion and Memory Lifecycle APIs
|
|
7
|
+
|
|
8
|
+
Scanned streaming (`materialize`), deterministic release (`Document#free`), and parse-lifecycle options (`readonly:`, `dtdattr:`) — the seams for conversion-heavy and batch workloads.
|
|
9
|
+
|
|
10
|
+
== Bulk materialization — `materialize` (issue #132)
|
|
11
|
+
|
|
12
|
+
Flattened, post-order records for a subtree with no `Moxml::Node` allocation — the conversion path for consumers that build their own tree from a document:
|
|
13
|
+
|
|
14
|
+
[source,ruby]
|
|
15
|
+
----
|
|
16
|
+
context.materialize(xml) { |record| ... } # parse + flatten
|
|
17
|
+
document.materialize.each # Enumerator, root subtree
|
|
18
|
+
any_element.materialize # any subtree
|
|
19
|
+
----
|
|
20
|
+
|
|
21
|
+
Record shape (all seven keys on every record):
|
|
22
|
+
|
|
23
|
+
[source,ruby]
|
|
24
|
+
----
|
|
25
|
+
{ kind: :element|:text|:cdata|:comment|:processing_instruction|:entity_reference,
|
|
26
|
+
qname: "tag"|"pi-target"|nil,
|
|
27
|
+
prefix: "p"|nil,
|
|
28
|
+
namespace_uri: "urn:x"|nil,
|
|
29
|
+
namespaces: [[prefix, uri], ...], # element's OWN declarations; nil prefix = default
|
|
30
|
+
attributes: [[name, value, namespace_uri, prefix], ...],
|
|
31
|
+
text: String|nil,
|
|
32
|
+
depth: Integer }
|
|
33
|
+
----
|
|
34
|
+
|
|
35
|
+
* Post-order: children arrive before their parent — build with a stack.
|
|
36
|
+
* `Document#materialize` yields exactly the root subtree (issue #140); document-level parts (prolog/epilog PIs and comments, DOCTYPE) stay enumerable via `children`.
|
|
37
|
+
* The leptris adapter has a bulk path (one `leptris_node_traverse` call); every other adapter uses the generic wrapper walk. Both emit identical streams — the parity is spec-pinned.
|
|
38
|
+
* Entity-bearing documents fall back to the generic walk so entity references still split.
|
|
39
|
+
|
|
40
|
+
== Deterministic release — `Document#free` (issue #134)
|
|
41
|
+
|
|
42
|
+
Adapters backed by C trees hold native memory until a GC finalizer runs. Batch workloads (parse → convert → discard, thousands of documents) release it deterministically:
|
|
43
|
+
|
|
44
|
+
[source,ruby]
|
|
45
|
+
----
|
|
46
|
+
doc = context.parse(xml)
|
|
47
|
+
doc.free # C tree released now; attachments swept
|
|
48
|
+
doc.root # => raises the engine's use-after-free error (leptris)
|
|
49
|
+
----
|
|
50
|
+
|
|
51
|
+
`#free` is a no-op on GC-managed engines (nokogiri, oga, ox, rexml). Normally-scoped documents keep working via the finalizer either way. Measured with GC disabled, 1000 × ~115KB parses: +1.2GB RSS without `#free` vs +121MB with it.
|
|
52
|
+
|
|
53
|
+
== Parse-lifecycle options
|
|
54
|
+
|
|
55
|
+
Both options pass through `Context#parse` to the engine and are no-ops where the concept does not apply:
|
|
56
|
+
|
|
57
|
+
* `readonly: true` (issue #133, leptris) — memoized reads, refused mutations. The parse-and-read lifecycle for multi-pass consumers (comparison, diff, signature): repeat attribute reads on a 100-attribute element drop from ~99µs to ~0.15µs (680×). Builder documents stay read-write.
|
|
58
|
+
|
|
59
|
+
* `dtdattr: true` (libleptris 1.9.8+, leptris/leptris#606) — materialize DTD `<!ATTLIST>` defaults. Off by default, matching libxml2/Nokogiri/REXML semantics:
|
|
60
|
+
|
|
61
|
+
[source,ruby]
|
|
62
|
+
----
|
|
63
|
+
context.parse(dtd_xml) # plain: ATTLIST defaults excluded
|
|
64
|
+
context.parse(dtd_xml, dtdattr: true) # opt-in: attr="default" materialized
|
|
65
|
+
----
|
|
66
|
+
|
|
67
|
+
== Pretty-print parity (issue #129)
|
|
68
|
+
|
|
69
|
+
Through the leptris adapter, byte-identical output to raw Nokogiri:
|
|
70
|
+
|
|
71
|
+
[source,ruby]
|
|
72
|
+
----
|
|
73
|
+
context.parse(src).to_xml(
|
|
74
|
+
indent: 2, declaration: true, expand_empty: false, encoding: "UTF-8",
|
|
75
|
+
) == Nokogiri::XML(src).to_xml(indent: 2, encoding: "UTF-8") # => true
|
|
76
|
+
----
|
|
77
|
+
|
|
78
|
+
Known residual divergences (engine-level, tracked upstream): tab indentation needs a serialize-time parameter in libleptris, and a PI preceding the DOCTYPE reorders (the engine normalizes DOCTYPE-first).
|
data/docs/_pages/index.adoc
CHANGED
|
@@ -28,6 +28,12 @@ Compare features, performance, and compatibility across adapters.
|
|
|
28
28
|
link:configuration[Configuration]::
|
|
29
29
|
Configure Moxml for your application's requirements.
|
|
30
30
|
|
|
31
|
+
link:conversion-apis[Conversion and memory lifecycle APIs]::
|
|
32
|
+
Bulk materialization, deterministic document release, and parse-lifecycle options for conversion-heavy workloads.
|
|
33
|
+
|
|
34
|
+
link:adapter-protocol[The adapter protocol]::
|
|
35
|
+
The seam between the wrapper layer and engine adapters — method groups, capability predicates, return shapes, and how to add an adapter.
|
|
36
|
+
|
|
31
37
|
link:error-handling[Error handling]::
|
|
32
38
|
Comprehensive guide to Moxml's error classes and debugging.
|
|
33
39
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Leptris
|
|
6
|
+
module DocumentParts
|
|
7
|
+
# Issue #134: deterministic release of the C tree. The binding
|
|
8
|
+
# clears its wrapper cache and raises UseAfterFreeError on
|
|
9
|
+
# later access; moxml-side attachments for the document are
|
|
10
|
+
# swept too (the context wrapper identity map self-cleans via
|
|
11
|
+
# its size valve).
|
|
12
|
+
DOCUMENT_ATTACHMENT_KEYS = %i[
|
|
13
|
+
entity_markers doc_pi_nodes declaration doctype
|
|
14
|
+
had_source_declaration document_text
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def free_document(native)
|
|
18
|
+
DOCUMENT_ATTACHMENT_KEYS.each { |key| attachments.delete(native, key) }
|
|
19
|
+
native.free
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def assemble_document_children(doc)
|
|
24
|
+
children = []
|
|
25
|
+
|
|
26
|
+
native_doctype = doc.doctype
|
|
27
|
+
children << native_doctype if native_doctype
|
|
28
|
+
|
|
29
|
+
doctype_wrapper = attachments.get(doc, :doctype)
|
|
30
|
+
children << doctype_wrapper if doctype_wrapper
|
|
31
|
+
|
|
32
|
+
if DOC_NODE_SUPPORTED
|
|
33
|
+
# The libxml2-model document node lists prolog PIs/comments,
|
|
34
|
+
# the root, and epilog PIs/comments in document order —
|
|
35
|
+
# the Nokogiri-shaped contract, epilog anchoring included
|
|
36
|
+
# (issue #130). Built (programmatic) documents are not yet
|
|
37
|
+
# fully reflected by the node (binding gap: an attached
|
|
38
|
+
# root does not appear); document_node_children answers
|
|
39
|
+
# nil there for the legacy parts path.
|
|
40
|
+
doc_children = document_node_children(doc)
|
|
41
|
+
if doc_children
|
|
42
|
+
children.concat(doc_children)
|
|
43
|
+
else
|
|
44
|
+
children.concat(document_pi_nodes(doc))
|
|
45
|
+
children << doc.root if doc.root
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
# Legacy path: document-level PIs live outside the element
|
|
49
|
+
# tree in a flat pre-root list (libleptris < 1.9.7 C
|
|
50
|
+
# model); epilog anchoring is not representable there.
|
|
51
|
+
children.concat(document_pi_nodes(doc))
|
|
52
|
+
|
|
53
|
+
children << doc.root if doc.root
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
texts = attachments.get(doc, :document_text)
|
|
57
|
+
children.concat(texts) if texts
|
|
58
|
+
children
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Document-level PI pseudo-nodes, materialized once from the C
|
|
62
|
+
# list and cached per document: children and serialization read
|
|
63
|
+
# the same objects, so wrapper mutations round-trip. Build the
|
|
64
|
+
# cache BEFORE appending a PI with add_pi, or the C-side
|
|
65
|
+
# addition would be double-counted.
|
|
66
|
+
# The document node's children, or nil when the node does not
|
|
67
|
+
# reflect reality: parsed documents always list the root
|
|
68
|
+
# element among their children, but programmatically built
|
|
69
|
+
# ones do not (binding gap) — those keep the legacy parts
|
|
70
|
+
# path.
|
|
71
|
+
def document_node_children(doc)
|
|
72
|
+
doc_children = doc.children.to_a
|
|
73
|
+
has_root = doc_children.any?(::Leptris::XML::Element)
|
|
74
|
+
return doc_children if has_root
|
|
75
|
+
return doc_children if doc.root.nil?
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def document_pi_nodes(doc)
|
|
81
|
+
attachments.get(doc, :doc_pi_nodes) || begin
|
|
82
|
+
nodes = doc.processing_instructions.map do |(target, data)|
|
|
83
|
+
CustomizedLeptris::DocumentPI.new(target, data, doc)
|
|
84
|
+
end
|
|
85
|
+
attachments.set(doc, :doc_pi_nodes, nodes)
|
|
86
|
+
nodes
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def add_document_child(doc, child)
|
|
91
|
+
case child
|
|
92
|
+
when CustomizedLeptris::Declaration
|
|
93
|
+
child.parent_doc = doc
|
|
94
|
+
attachments.set(doc, :declaration, child)
|
|
95
|
+
when CustomizedLeptris::Doctype
|
|
96
|
+
child.parent_doc = doc
|
|
97
|
+
attachments.set(doc, :doctype, child)
|
|
98
|
+
when ::Leptris::XML::DocType
|
|
99
|
+
raise Moxml::DocumentStructureError.new(
|
|
100
|
+
"libleptris does not support attaching a native DocType to a document",
|
|
101
|
+
)
|
|
102
|
+
when ::Leptris::XML::Element
|
|
103
|
+
doc.root = child
|
|
104
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
105
|
+
document_pi_nodes(doc)
|
|
106
|
+
doc.add_pi(child.target, child.content.to_s)
|
|
107
|
+
document_pi_nodes(doc) << CustomizedLeptris::DocumentPI.new(
|
|
108
|
+
child.target, child.content.to_s, doc
|
|
109
|
+
)
|
|
110
|
+
when CustomizedLeptris::DocumentPI
|
|
111
|
+
document_pi_nodes(doc)
|
|
112
|
+
doc.add_pi(child.target, child.data)
|
|
113
|
+
document_pi_nodes(doc) << child
|
|
114
|
+
when ::Leptris::XML::Text
|
|
115
|
+
texts = attachments.get(doc, :document_text) || []
|
|
116
|
+
texts << child
|
|
117
|
+
attachments.set(doc, :document_text, texts)
|
|
118
|
+
child
|
|
119
|
+
else
|
|
120
|
+
raise Moxml::DocumentStructureError.new(
|
|
121
|
+
"Unsupported document child: #{child.class}",
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
child
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Documents compose from their parts: the native serializer
|
|
128
|
+
# only walks the root subtree, so declaration, DOCTYPE, PIs and
|
|
129
|
+
# document-level text are assembled around it explicitly.
|
|
130
|
+
def serialize_document(doc, options)
|
|
131
|
+
# Nokogiri's document shape: every top-level part is
|
|
132
|
+
# newline-terminated, at any indent — declaration, DOCTYPE,
|
|
133
|
+
# document PIs, the root element, trailing newline after it.
|
|
134
|
+
# Document-level text is content, not structure: no added
|
|
135
|
+
# newline.
|
|
136
|
+
parts = []
|
|
137
|
+
|
|
138
|
+
include_decl = !options[:no_declaration] && options.fetch(:declaration) do
|
|
139
|
+
document_has_declaration?(doc)
|
|
140
|
+
end
|
|
141
|
+
if include_decl
|
|
142
|
+
declaration = attachments.get(doc, :declaration)
|
|
143
|
+
parts << (declaration ? declaration.to_xml : default_declaration_xml(doc, options)) << "\n"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
doctype = attachments.get(doc, :doctype)
|
|
147
|
+
parts << doctype.to_xml << "\n" if doctype
|
|
148
|
+
|
|
149
|
+
native = native_doctype_xml(doc)
|
|
150
|
+
parts << native << "\n" if native
|
|
151
|
+
|
|
152
|
+
if DOC_NODE_SUPPORTED
|
|
153
|
+
# The libxml2-model document node: prolog PIs/comments,
|
|
154
|
+
# the root, epilog PIs/comments — in document order, so
|
|
155
|
+
# epilog parts serialize after the root (issue #130).
|
|
156
|
+
doc_children = document_node_children(doc)
|
|
157
|
+
if doc_children
|
|
158
|
+
doc_children.each { |child| parts << raw_serialize(child, options) << "\n" }
|
|
159
|
+
else
|
|
160
|
+
document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
|
|
161
|
+
parts << raw_serialize(doc.root, options) << "\n" if doc.root
|
|
162
|
+
end
|
|
163
|
+
else
|
|
164
|
+
document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
|
|
165
|
+
|
|
166
|
+
parts << raw_serialize(doc.root, options) << "\n" if doc.root
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
texts = attachments.get(doc, :document_text)
|
|
170
|
+
texts&.each { |text| parts << XmlEmitter.escape_text(text.content.to_s) }
|
|
171
|
+
|
|
172
|
+
parts.join
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def native_doctype_xml(doc)
|
|
176
|
+
dt = doc.doctype
|
|
177
|
+
return nil unless dt
|
|
178
|
+
|
|
179
|
+
subset = dt.internal_subset if dt.class.method_defined?(:internal_subset)
|
|
180
|
+
XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id, subset)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def default_declaration_xml(doc, options)
|
|
184
|
+
encoding = options[:encoding] || doc.encoding
|
|
185
|
+
encoding = "UTF-8" if encoding.to_s.empty?
|
|
186
|
+
XmlEmitter.declaration_xml("1.0", encoding, nil)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def document_has_declaration?(native)
|
|
190
|
+
return false unless native.is_a?(::Leptris::XML::Document)
|
|
191
|
+
|
|
192
|
+
return true if attachments.get(native, :declaration)
|
|
193
|
+
|
|
194
|
+
attachments.get(native, :had_source_declaration) ? true : false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def marker_text_for(parent, name)
|
|
198
|
+
return nil unless parent.is_a?(::Leptris::XML::Element)
|
|
199
|
+
|
|
200
|
+
marker = "#{Entity::MARKER}#{name};"
|
|
201
|
+
parent.children.to_a.find do |child|
|
|
202
|
+
child.is_a?(::Leptris::XML::Text) && child.content == marker
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Leptris
|
|
6
|
+
module Markers
|
|
7
|
+
# Marker presence is a document-level fact: parse records it,
|
|
8
|
+
# the ER builder path flips it, and the split/restore scans
|
|
9
|
+
# consult it. Customized natives only exist as split products
|
|
10
|
+
# of marker-bearing text, so they always report true.
|
|
11
|
+
def entity_bearing?(native)
|
|
12
|
+
case native
|
|
13
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
14
|
+
CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
|
|
15
|
+
CustomizedLeptris::DocumentPI
|
|
16
|
+
true
|
|
17
|
+
else
|
|
18
|
+
doc = native.document
|
|
19
|
+
doc.nil? || attachments.get(doc, :entity_markers) != false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Expand marker-bearing text nodes into the child sequence the
|
|
24
|
+
# moxml contract exposes: text, EntityReference, text, ...
|
|
25
|
+
def split_entity_markers(natives, parent)
|
|
26
|
+
result = []
|
|
27
|
+
natives.each do |child|
|
|
28
|
+
# FFI Text#content returns a fresh unfrozen BINARY string:
|
|
29
|
+
# retag in place (dup would be a throwaway allocation), but
|
|
30
|
+
# before include? — BINARY.include? with the UTF-8 marker raises
|
|
31
|
+
if child.is_a?(::Leptris::XML::Text)
|
|
32
|
+
content = child.content
|
|
33
|
+
content.force_encoding("UTF-8")
|
|
34
|
+
end
|
|
35
|
+
if content&.include?(Entity::MARKER)
|
|
36
|
+
content.scan(/([^#{Entity::MARKER}]*)(?:#{Entity::MARKER}([\w.:-]+);)?/o) do
|
|
37
|
+
text_part = Regexp.last_match(1)
|
|
38
|
+
name = Regexp.last_match(2)
|
|
39
|
+
result << CustomizedLeptris::TextSegment.new(text_part, parent) unless text_part.empty?
|
|
40
|
+
result << CustomizedLeptris::EntityReference.new(name) if name
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
result << child
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|