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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cc0d0436680fb9c46169bc93e45197b20b71bf6970d737750e63fb110a5e75e
4
- data.tar.gz: 8f71ca799b845b4fb28a977f6515ac1b702b6557259667c5b034f5b612ef6b67
3
+ metadata.gz: f05dc2b6886b453a5147c56e2936f0f0a15206c26037cf737792a1b906e8788e
4
+ data.tar.gz: 7bd3b318538208c43f2f71016ce207847ea3104681fef2d74aee754ec441204d
5
5
  SHA512:
6
- metadata.gz: 1edf604a33151f0079608d52231a1486a252cda307f49fd958e05f41e3229394a952245159939c2f4dac67ecba9f612ef6c4718d7f450c3260e1f78fc4d83135
7
- data.tar.gz: 92aeae211e0b8abae0ce569c9dbd650f3b2b8426e975a42e3341317ebeb6c09928adbb0265b2ea7cdb726ca40d9ac376be1e537200d2166255fc30f365afd7ed
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.307 vs
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.27x
664
+ |3.0x
670
665
  |Engine pool-carved parse
671
666
 
672
667
  |serialize
673
- |3.38x
668
+ |2.9x
674
669
  |
675
670
 
676
671
  |XPath
677
- |50x
672
+ |40x
678
673
  |Native compiled-eval seams
679
674
 
680
675
  |walk + read (`.text`, `[]`)
681
- |1.90x
676
+ |2.0x
682
677
  |C klass-mint wrappers + wrapper read memos
683
678
 
684
679
  |warm re-read
685
- |2.10x
680
+ |2.1x
686
681
  |The memo family — zero string allocation per node
687
682
 
688
683
  |build (`add_element`)
689
- |4.24x
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
- |1.73x
692
+ |parity
694
693
  |One C crossing, no Attribute objects
695
694
  |===
696
695
 
@@ -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 = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;",
171
+ '"' => "&quot;" }.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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.114"
4
+ VERSION = "0.5.116"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.114
4
+ version: 0.5.116
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.