moxml 0.5.114 → 0.5.116
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/README.adoc +18 -19
- data/lib/moxml/adapter/base.rb +12 -0
- data/lib/moxml/adapter/leptris.rb +31 -0
- data/lib/moxml/builder.rb +183 -0
- data/lib/moxml/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f05dc2b6886b453a5147c56e2936f0f0a15206c26037cf737792a1b906e8788e
|
|
4
|
+
data.tar.gz: 7bd3b318538208c43f2f71016ce207847ea3104681fef2d74aee754ec441204d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97324a4fe0132afed58055d5ed6a54933b4adb195933984ff1188c6f4276065cadbf3abd66f08beb2ba59a8eeb73c5ada02fd7e0a5f24e09f8fc7372852f4232
|
|
7
|
+
data.tar.gz: 69745ed2e541b46dad94148ddc0c8ac1240d4032f78250be494dfb01d98b83b6835fb5a28d9289967754ba4d40a8c942d5302f4066b8160526435ded02932738
|
data/README.adoc
CHANGED
|
@@ -239,7 +239,7 @@ libleptris has no first-class nodes for them.
|
|
|
239
239
|
| Feature/Operation | Nokogiri | Oga | REXML | LibXML | Ox | HeadedOx | Leptris
|
|
240
240
|
|
|
241
241
|
| *Core Operations*
|
|
242
|
-
|
|
242
|
+
|
|
|
243
243
|
|
|
|
244
244
|
|
|
|
245
245
|
|
|
|
@@ -275,9 +275,8 @@ libleptris has no first-class nodes for them.
|
|
|
275
275
|
| ✅ Full
|
|
276
276
|
|
|
277
277
|
| *Element Operations*
|
|
278
|
-
|
|
279
278
|
|
|
|
280
|
-
|
|
279
|
+
|
|
|
281
280
|
|
|
|
282
281
|
|
|
|
283
282
|
|
|
|
@@ -321,9 +320,8 @@ libleptris has no first-class nodes for them.
|
|
|
321
320
|
| ✅ Full
|
|
322
321
|
|
|
323
322
|
| *Namespace Operations*
|
|
324
|
-
|
|
325
323
|
|
|
|
326
|
-
|
|
324
|
+
|
|
|
327
325
|
|
|
|
328
326
|
|
|
|
329
327
|
|
|
|
@@ -367,9 +365,8 @@ libleptris has no first-class nodes for them.
|
|
|
367
365
|
| ✅ Full
|
|
368
366
|
|
|
369
367
|
| *XPath Queries*
|
|
370
|
-
|
|
371
368
|
|
|
|
372
|
-
|
|
369
|
+
|
|
|
373
370
|
|
|
|
374
371
|
|
|
|
375
372
|
|
|
|
@@ -467,9 +464,8 @@ libleptris has no first-class nodes for them.
|
|
|
467
464
|
| ✅ All 27
|
|
468
465
|
|
|
469
466
|
| *Special Content*
|
|
470
|
-
|
|
471
467
|
|
|
|
472
|
-
|
|
468
|
+
|
|
|
473
469
|
|
|
|
474
470
|
|
|
|
475
471
|
|
|
|
@@ -513,9 +509,8 @@ libleptris has no first-class nodes for them.
|
|
|
513
509
|
| ⚠️ Value-object sidecar^8^
|
|
514
510
|
|
|
515
511
|
| *Performance*
|
|
516
|
-
|
|
517
512
|
|
|
|
518
|
-
|
|
513
|
+
|
|
|
519
514
|
|
|
|
520
515
|
|
|
|
521
516
|
|
|
|
@@ -658,7 +653,7 @@ puts doc.to_xml(indent: 2)
|
|
|
658
653
|
=== Performance versus nokogiri
|
|
659
654
|
|
|
660
655
|
CPU-time medians (1,000-record catalog, 20 iterations) through the
|
|
661
|
-
moxml adapter layer on both sides — moxml + leptris 1.9.
|
|
656
|
+
moxml adapter layer on both sides — moxml + leptris 1.9.314 vs
|
|
662
657
|
moxml + nokogiri:
|
|
663
658
|
|
|
664
659
|
[cols="2,1,4"]
|
|
@@ -666,31 +661,35 @@ moxml + nokogiri:
|
|
|
666
661
|
|Operation |leptris vs nokogiri |Notes
|
|
667
662
|
|
|
668
663
|
|parse
|
|
669
|
-
|3.
|
|
664
|
+
|3.0x
|
|
670
665
|
|Engine pool-carved parse
|
|
671
666
|
|
|
672
667
|
|serialize
|
|
673
|
-
|
|
|
668
|
+
|2.9x
|
|
674
669
|
|
|
|
675
670
|
|
|
676
671
|
|XPath
|
|
677
|
-
|
|
|
672
|
+
|40x
|
|
678
673
|
|Native compiled-eval seams
|
|
679
674
|
|
|
680
675
|
|walk + read (`.text`, `[]`)
|
|
681
|
-
|
|
|
676
|
+
|2.0x
|
|
682
677
|
|C klass-mint wrappers + wrapper read memos
|
|
683
678
|
|
|
684
679
|
|warm re-read
|
|
685
|
-
|2.
|
|
680
|
+
|2.1x
|
|
686
681
|
|The memo family — zero string allocation per node
|
|
687
682
|
|
|
688
683
|
|build (`add_element`)
|
|
689
|
-
|
|
|
684
|
+
|3.7x
|
|
690
685
|
|C-mint construction + bulk attributes
|
|
691
686
|
|
|
687
|
+
|build (Builder DSL)
|
|
688
|
+
|2.1x
|
|
689
|
+
|Markup-accumulating face — one crossing per subtree
|
|
690
|
+
|
|
692
691
|
|attribute pairs
|
|
693
|
-
|
|
|
692
|
+
|parity
|
|
694
693
|
|One C crossing, no Attribute objects
|
|
695
694
|
|===
|
|
696
695
|
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -13,6 +13,18 @@ module Moxml
|
|
|
13
13
|
class << self
|
|
14
14
|
include XmlUtils
|
|
15
15
|
|
|
16
|
+
# Binding-level markup-accumulating builder face
|
|
17
|
+
# (leptris 1.9.313, #374 lever 2): nil = the adapter has no
|
|
18
|
+
# face and the Builder takes the per-node tree path.
|
|
19
|
+
def builder_face(_native_document)
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A built subtree carried entity references as marker text —
|
|
24
|
+
# arm the document's marker restoration (the entity
|
|
25
|
+
# preservation contract).
|
|
26
|
+
def mark_entity_markers(_native_document); end
|
|
27
|
+
|
|
16
28
|
def preprocess_entities(xml)
|
|
17
29
|
Entity.preprocess_entities(xml)
|
|
18
30
|
end
|
|
@@ -161,6 +161,16 @@ module Moxml
|
|
|
161
161
|
::Leptris::XML::Document.method_defined?(:absorb) &&
|
|
162
162
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.311.3")
|
|
163
163
|
|
|
164
|
+
# Markup-accumulating builder face (leptris-ruby#374 lever 2,
|
|
165
|
+
# binding 1.9.313): the DSL accumulates markup in a plain
|
|
166
|
+
# String and ONE native crossing (parse + attach) flushes the
|
|
167
|
+
# whole subtree. See Adapter::Base#builder_face.
|
|
168
|
+
NATIVE_BUILDER =
|
|
169
|
+
defined?(::Leptris::XML::Builder) &&
|
|
170
|
+
defined?(::Leptris::XML::Document) &&
|
|
171
|
+
::Leptris::XML::Document.method_defined?(:build) &&
|
|
172
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.313.0")
|
|
173
|
+
|
|
164
174
|
# XSLT 1.0/2.0/3.0 through the engine's compiled-stylesheet
|
|
165
175
|
# faces (Moxml::XSLT contract). Top-level parameters are not
|
|
166
176
|
# threaded through the faces yet (leptris-ruby#360) — non-empty
|
|
@@ -208,6 +218,27 @@ module Moxml
|
|
|
208
218
|
# (and on installs without the native layer — the constant
|
|
209
219
|
# check short-circuits), a Node.wrap over the shared C
|
|
210
220
|
# pointer for NativeNodes.
|
|
221
|
+
def builder_face(native_document)
|
|
222
|
+
return nil unless NATIVE_BUILDER && NATIVE_READ_LAYER
|
|
223
|
+
|
|
224
|
+
doc = if native_document.is_a?(NN)
|
|
225
|
+
to_binding(native_document)
|
|
226
|
+
else
|
|
227
|
+
native_document
|
|
228
|
+
end
|
|
229
|
+
::Leptris::XML::Builder.new(doc)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def mark_entity_markers(native_document)
|
|
233
|
+
doc = if native_document.is_a?(NN)
|
|
234
|
+
to_binding(native_document)
|
|
235
|
+
else
|
|
236
|
+
native_document
|
|
237
|
+
end
|
|
238
|
+
attachments.set(doc, :entity_markers, true)
|
|
239
|
+
bump_serialize_generation
|
|
240
|
+
end
|
|
241
|
+
|
|
211
242
|
def to_binding(node)
|
|
212
243
|
return node unless NATIVE_READ_LAYER &&
|
|
213
244
|
node.is_a?(::Leptris::XML::NativeNode)
|
data/lib/moxml/builder.rb
CHANGED
|
@@ -11,14 +11,40 @@ module Moxml
|
|
|
11
11
|
@context = context
|
|
12
12
|
@current = @document = context.create_document
|
|
13
13
|
@namespaces = {}
|
|
14
|
+
# Markup-accumulating face (leptris 1.9.313, #374 lever 2):
|
|
15
|
+
# leaf DSL ops write markup into one String and a single
|
|
16
|
+
# native crossing flushes the whole subtree at build end.
|
|
17
|
+
# Tree-only ops (declaration/doctype/PI/entity/namespace)
|
|
18
|
+
# flush first and run on the document; the face retires after
|
|
19
|
+
# its first flush.
|
|
20
|
+
@face = context.config.adapter.builder_face(@document.native)
|
|
21
|
+
@face_stack = []
|
|
22
|
+
@face_counts = Hash.new(0)
|
|
23
|
+
@face_deferred = []
|
|
24
|
+
@face_entity_markers = false
|
|
14
25
|
end
|
|
15
26
|
|
|
16
27
|
def build(&block)
|
|
17
28
|
instance_eval(&block)
|
|
29
|
+
flush_face
|
|
18
30
|
@document
|
|
19
31
|
end
|
|
20
32
|
|
|
33
|
+
# Element ops inside a face-accumulated subtree cannot touch the
|
|
34
|
+
# tree yet (nothing exists until the flush): PIs ride the raw
|
|
35
|
+
# hatch in exact position; namespace/entity/declaration/doctype
|
|
36
|
+
# defer to the flush and re-target their element by position (an
|
|
37
|
+
# element's namespace set is unordered, so deferral is exact;
|
|
38
|
+
# deferred children land at their element's end).
|
|
39
|
+
def face_defer(operation, args)
|
|
40
|
+
@face_deferred << [operation, args, @face_stack.dup]
|
|
41
|
+
end
|
|
42
|
+
|
|
21
43
|
def declaration(version: "1.0", encoding: "UTF-8", standalone: nil)
|
|
44
|
+
args = [version, encoding, standalone]
|
|
45
|
+
return face_defer(:declaration, args) if @face && @face_stack.any?
|
|
46
|
+
|
|
47
|
+
flush_face
|
|
22
48
|
@current.add_child(
|
|
23
49
|
@document.create_declaration(version, encoding, standalone),
|
|
24
50
|
)
|
|
@@ -41,34 +67,65 @@ module Moxml
|
|
|
41
67
|
end
|
|
42
68
|
|
|
43
69
|
def text(content)
|
|
70
|
+
return face_content { @face.text(content) } if @face
|
|
71
|
+
|
|
44
72
|
@current.add_child(@document.create_text(content))
|
|
45
73
|
end
|
|
46
74
|
|
|
47
75
|
def cdata(content)
|
|
76
|
+
return face_content { @face.cdata(content) } if @face
|
|
77
|
+
|
|
48
78
|
@current.add_child(@document.create_cdata(content))
|
|
49
79
|
end
|
|
50
80
|
|
|
51
81
|
def comment(content)
|
|
82
|
+
return face_content { @face.comment(content) } if @face
|
|
83
|
+
|
|
52
84
|
@current.add_child(@document.create_comment(content))
|
|
53
85
|
end
|
|
54
86
|
|
|
55
87
|
def entity_reference(name)
|
|
88
|
+
# In-position through the marker face: the reference lands as
|
|
89
|
+
# marker text exactly where the DSL puts it, and the flush
|
|
90
|
+
# arms the document's entity-marker restoration.
|
|
91
|
+
if @face && @face_stack.any?
|
|
92
|
+
@face.raw("#{Entity::MARKER}#{name};")
|
|
93
|
+
@face_entity_markers = true
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
flush_face
|
|
56
98
|
@current.add_child(@document.create_entity_reference(name))
|
|
57
99
|
end
|
|
58
100
|
|
|
59
101
|
def processing_instruction(target, content)
|
|
102
|
+
if @face
|
|
103
|
+
if @face_stack.empty?
|
|
104
|
+
flush_face
|
|
105
|
+
else
|
|
106
|
+
@face.raw("<?#{target} #{content}?>")
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
end
|
|
60
110
|
@current.add_child(
|
|
61
111
|
@document.create_processing_instruction(target, content),
|
|
62
112
|
)
|
|
63
113
|
end
|
|
64
114
|
|
|
65
115
|
def namespace(prefix, uri)
|
|
116
|
+
return face_defer(:namespace, [prefix, uri]) if @face && @face_stack.any?
|
|
117
|
+
|
|
118
|
+
flush_face
|
|
66
119
|
@current.add_namespace(prefix, uri)
|
|
67
120
|
@namespaces[prefix] = uri
|
|
68
121
|
end
|
|
69
122
|
|
|
70
123
|
# Convenience method for DOCTYPE
|
|
71
124
|
def doctype(name, external_id = nil, system_id = nil)
|
|
125
|
+
args = [name, external_id, system_id]
|
|
126
|
+
return face_defer(:doctype, args) if @face && @face_stack.any?
|
|
127
|
+
|
|
128
|
+
flush_face
|
|
72
129
|
@current.add_child(
|
|
73
130
|
@document.create_doctype(name, external_id, system_id),
|
|
74
131
|
)
|
|
@@ -109,13 +166,139 @@ module Moxml
|
|
|
109
166
|
true
|
|
110
167
|
end
|
|
111
168
|
|
|
169
|
+
FACE_NAME = /\A[a-zA-Z_][\w.-]*\z/
|
|
170
|
+
FACE_ESCAPE = { "&" => "&", "<" => "<", ">" => ">",
|
|
171
|
+
'"' => """ }.freeze
|
|
172
|
+
FACE_ESCAPE_RE = /[&<>"]/
|
|
173
|
+
|
|
112
174
|
private
|
|
113
175
|
|
|
176
|
+
# One face.element per DSL element — the face opens/closes the
|
|
177
|
+
# markup around the block itself, so nesting is free. Prefixed
|
|
178
|
+
# names (p:tag) fall outside the face's name shape and ride the
|
|
179
|
+
# raw hatch with manual escaping. Returns no wrapper: nothing
|
|
180
|
+
# exists until the flush.
|
|
181
|
+
def face_element(tag_name, attrs, text_content, block, eval_block)
|
|
182
|
+
unless FACE_NAME.match?(tag_name)
|
|
183
|
+
return face_raw_element(tag_name, attrs, text_content, block,
|
|
184
|
+
eval_block)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
face_attrs = {}
|
|
188
|
+
attrs.each do |key, value|
|
|
189
|
+
k = key.to_s
|
|
190
|
+
if k == "xmlns" || k.start_with?("xmlns:")
|
|
191
|
+
face_attrs[k] = value.to_s
|
|
192
|
+
@namespaces[k.delete_prefix("xmlns:")] = value.to_s
|
|
193
|
+
else
|
|
194
|
+
face_attrs[k] = value
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
ordinal = @face_counts[@face_stack.length]
|
|
198
|
+
@face_counts[@face_stack.length] = ordinal + 1
|
|
199
|
+
@face_stack << ordinal
|
|
200
|
+
begin
|
|
201
|
+
@face.element(tag_name, face_attrs) do
|
|
202
|
+
@face.text(text_content) if text_content
|
|
203
|
+
run_builder_block(block, eval_block) if block
|
|
204
|
+
end
|
|
205
|
+
ensure
|
|
206
|
+
@face_stack.pop
|
|
207
|
+
@face_counts.delete(@face_stack.length + 1)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def face_raw_element(tag_name, attrs, text_content, block, eval_block)
|
|
212
|
+
open = "<#{tag_name}"
|
|
213
|
+
attrs.each do |key, value|
|
|
214
|
+
k = key.to_s
|
|
215
|
+
if k == "xmlns" || k.start_with?("xmlns:")
|
|
216
|
+
@namespaces[k.delete_prefix("xmlns:")] = value.to_s
|
|
217
|
+
end
|
|
218
|
+
open << " " << k << '="' << face_escape(value) << '"'
|
|
219
|
+
end
|
|
220
|
+
ordinal = @face_counts[@face_stack.length]
|
|
221
|
+
@face_counts[@face_stack.length] = ordinal + 1
|
|
222
|
+
@face_stack << ordinal
|
|
223
|
+
begin
|
|
224
|
+
if block || text_content
|
|
225
|
+
@face.raw("#{open}>")
|
|
226
|
+
@face.text(text_content) if text_content
|
|
227
|
+
run_builder_block(block, eval_block) if block
|
|
228
|
+
@face.raw("</#{tag_name}>")
|
|
229
|
+
else
|
|
230
|
+
@face.raw("#{open}/>")
|
|
231
|
+
end
|
|
232
|
+
ensure
|
|
233
|
+
@face_stack.pop
|
|
234
|
+
@face_counts.delete(@face_stack.length + 1)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Content at document level has no root to attach to and the
|
|
239
|
+
# Document face flush demands exactly one root — retire the face
|
|
240
|
+
# and take the tree path (legacy semantics: doc-level text is a
|
|
241
|
+
# document child). Inside a subtree the text accumulates in
|
|
242
|
+
# position; @current never leaves the document in face mode, so
|
|
243
|
+
# the face stack is the nesting test.
|
|
244
|
+
def face_content
|
|
245
|
+
return yield if @face && @face_stack.any?
|
|
246
|
+
|
|
247
|
+
flush_face if @face
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def run_builder_block(block, eval_block)
|
|
251
|
+
eval_block ? instance_eval(&block) : block.call
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def face_escape(value)
|
|
255
|
+
value.to_s.gsub(FACE_ESCAPE_RE, FACE_ESCAPE)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def flush_face
|
|
259
|
+
return unless @face
|
|
260
|
+
|
|
261
|
+
face = @face
|
|
262
|
+
@face = nil
|
|
263
|
+
face.flush
|
|
264
|
+
if @face_entity_markers
|
|
265
|
+
@context.config.adapter.mark_entity_markers(@document.native)
|
|
266
|
+
end
|
|
267
|
+
apply_face_deferred
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Deferred tree-ops re-target their element by position: the
|
|
271
|
+
# stack records each element's ordinal among its parent's
|
|
272
|
+
# elements, and the flushed tree preserves markup order.
|
|
273
|
+
def apply_face_deferred
|
|
274
|
+
@face_deferred.each do |op, args, stack|
|
|
275
|
+
target = @document.root
|
|
276
|
+
stack[1..].each do |ordinal|
|
|
277
|
+
target = target.elements.to_a[ordinal]
|
|
278
|
+
end
|
|
279
|
+
case op
|
|
280
|
+
when :namespace
|
|
281
|
+
target.add_namespace(*args)
|
|
282
|
+
@namespaces[args[0]] = args[1]
|
|
283
|
+
when :declaration
|
|
284
|
+
target.add_child(@document.create_declaration(*args))
|
|
285
|
+
when :doctype
|
|
286
|
+
target.add_child(@document.create_doctype(*args))
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
@face_deferred = []
|
|
290
|
+
end
|
|
291
|
+
|
|
114
292
|
# Single method for all element creation.
|
|
115
293
|
# eval_block: true → instance_eval (build DSL context)
|
|
116
294
|
# eval_block: false → yield (preserves caller's self)
|
|
117
295
|
def create_element_node(tag_name, attrs = {}, text_content: nil,
|
|
118
296
|
block: nil, eval_block: true)
|
|
297
|
+
if @face
|
|
298
|
+
face_element(tag_name, attrs, text_content, block, eval_block)
|
|
299
|
+
return nil
|
|
300
|
+
end
|
|
301
|
+
|
|
119
302
|
el = @document.create_element(tag_name)
|
|
120
303
|
|
|
121
304
|
attrs.each do |key, value|
|
data/lib/moxml/version.rb
CHANGED