moxml 0.5.114 → 0.5.115

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: 3269b76bcc581101e3b7f6219c58fc5010752ab1af4f4bb35b8e505989f08e04
4
+ data.tar.gz: 1c255835027897f446de2bb88b175a5b71432636b2a7ab3810480323865a4dac
5
5
  SHA512:
6
- metadata.gz: 1edf604a33151f0079608d52231a1486a252cda307f49fd958e05f41e3229394a952245159939c2f4dac67ecba9f612ef6c4718d7f450c3260e1f78fc4d83135
7
- data.tar.gz: 92aeae211e0b8abae0ce569c9dbd650f3b2b8426e975a42e3341317ebeb6c09928adbb0265b2ea7cdb726ca40d9ac376be1e537200d2166255fc30f365afd7ed
6
+ metadata.gz: 12d97fd4ae488aee81c5e04e5b55be1336bea7bad60f14fc2481ee9fdd80689edac7dfc3113a6dc65be605a555d388d10e89566a206993c84438d0fcbd9e8d41
7
+ data.tar.gz: 1bf50772dde870a02dd80c1d4d686d2d47149b1d4b9210113bed8e2732dec6c48291105ee26969c92c1d46d6f7086931516c0f6110ce868d662cb2bfa1ea2871
@@ -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.115"
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.115
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.