leptris 1.9.312.0-aarch64-linux → 1.9.312.1-aarch64-linux

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: 9de71b8c23036b66ec08f42ba8d76658c4e0b434d1310edd0f25b22c67685cfa
4
- data.tar.gz: b6a93975b39dd5c601e021c4265ddc4ad50af9e4542c161d946d992110b02fbc
3
+ metadata.gz: 2eac674bc556fd667fc8e3bd72d92f73c1c939637dd46782d8fbc3c6c39bf025
4
+ data.tar.gz: 998089efd6146235e339bbbe0157d8856562fe1edbed65a8e26237216cb1e3d9
5
5
  SHA512:
6
- metadata.gz: fbd674dc47c7220e0405a473dce2d261370d4ef45a8cca93475212250ddf4859206884638901974124e8f23aff5ea78b44e5f4a8e6e078647f5279efe0ab1544
7
- data.tar.gz: 37082ea5af7b48056ea1d19cd2613d6f8d4696cce9dcb547ebc14c3ee30ef0af28d88a1d086d4857d07ef3431be03217b3ff0c9a0fa39511211890773525474c
6
+ metadata.gz: eead31d67e4f604af0e7a54a4335c63414f4fc5175abfcae74c6e77494561660e85e41e24cac24c4561b0298625c36d8553d2e6ef3f193ba8de0220796e32a5e
7
+ data.tar.gz: 1cf2990c2f592841b0e6901da330db167d596c3f8e30c87135b831690c063390b41c0be554fa371de9afce5fce006c7fe316a33c9060dffd7024cf2172df7505
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [1.9.312.1] - 2026-10-07
2
+
3
+ ### Added
4
+
5
+ - **Document#build / Element#build — the builder face (#374 lever
6
+ 2)**: markup-accumulating construction. The block turns into
7
+ well-formed markup in Ruby (method_missing element dispatch
8
+ with attrs/text, `#text`/`#cdata`/`#comment`/`#raw`, one-pass
9
+ escaping of `& < > "`), and the flush performs ONE native
10
+ crossing for the whole subtree — parse + `root=` adoption for
11
+ document sinks (exactly one root, the parser enforces it;
12
+ returns the installed root wrapper), `append_markup` for
13
+ element sinks (returns the added NodeSet). Measured DSL-vs-DSL
14
+ under load (interleaved CPU-time medians, build(500)):
15
+ leptris builder 2.68 ms vs nokogiri's builder 5.29 ms —
16
+ **~2.0x** — and ~1.2x over our own object face (3.19 ms,
17
+ itself ~1.7x nokogiri after the 1.9.311.3 trim).
18
+
1
19
  ## [1.9.312.0] - 2026-10-07
2
20
 
3
21
  ### Added — engine sync (libleptris 1.9.312)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.312.0"
4
+ VERSION = "1.9.312.1"
5
5
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Markup-accumulating subtree construction (#374 lever 2): the
4
+ # builder turns a Ruby block into well-formed markup in a plain
5
+ # String (Ruby-side cost only), and the flush performs ONE native
6
+ # crossing — parse + attach — for the whole subtree. The
7
+ # per-element FFI crossing that dominated the object-face build
8
+ # profile collapses to one per #build call.
9
+ #
10
+ # doc.build do |b|
11
+ # b.catalog(id: "c") do
12
+ # b.item(id: 7) { b.name "Item 7" }
13
+ # end
14
+ # end
15
+ #
16
+ # Element names arriving through method_missing are identifier-
17
+ # shaped by construction (Ruby method names cannot carry markup
18
+ # metacharacters); the explicit #element path checks NAME_RE.
19
+ # Text and attribute values escape in one pass per string.
20
+ class Leptris::XML::Builder
21
+ NAME_RE = /\A[a-zA-Z_][\w.-]*\z/
22
+
23
+ ESCAPE = {
24
+ "&" => "&amp;",
25
+ "<" => "&lt;",
26
+ ">" => "&gt;",
27
+ '"' => "&quot;",
28
+ }.freeze
29
+ ESCAPE_RE = /[&<>"]/
30
+ private_constant :ESCAPE, :ESCAPE_RE
31
+
32
+ # +sink+ receives the flushed markup (a Document or Element).
33
+ def initialize(sink)
34
+ @sink = sink
35
+ @buf = +""
36
+ @depth = 0
37
+ end
38
+
39
+ def element(name, attributes = nil)
40
+ name = name.to_s
41
+ unless name =~ NAME_RE
42
+ raise ArgumentError,
43
+ "element name must match #{NAME_RE.inspect}, got #{name.inspect}"
44
+ end
45
+ @buf << "<" << name
46
+ write_attributes(attributes) if attributes
47
+ if block_given?
48
+ @buf << ">"
49
+ @depth += 1
50
+ begin
51
+ yield self
52
+ ensure
53
+ @depth -= 1
54
+ end
55
+ @buf << "</" << name << ">"
56
+ else
57
+ @buf << "/>"
58
+ end
59
+ self
60
+ end
61
+
62
+ def text(content)
63
+ @buf << escape_text(content.to_s)
64
+ self
65
+ end
66
+
67
+ def cdata(content)
68
+ @buf << "<![CDATA[" << content.to_s << "]]>"
69
+ self
70
+ end
71
+
72
+ def comment(content)
73
+ @buf << "<!--" << escape_text(content.to_s) << "-->"
74
+ self
75
+ end
76
+
77
+ # Verbatim markup — the escape hatch for pre-serialized
78
+ # fragments; the caller owns well-formedness.
79
+ def raw(markup)
80
+ @buf << markup.to_s
81
+ self
82
+ end
83
+
84
+ def method_missing(name, *args, &block)
85
+ # Method names are identifier-shaped: they cannot carry <, >,
86
+ # &, quotes, or whitespace, so they need no NAME_RE round-trip
87
+ # (trailing = / ? / ! are legal Ruby but never legal XML
88
+ # name tails here — reject them explicitly).
89
+ return super if name.to_s.end_with?("=", "?", "!")
90
+ case args.length
91
+ when 0
92
+ element(name.to_s, &block)
93
+ when 1
94
+ arg = args[0]
95
+ if arg.is_a?(Hash)
96
+ element(name.to_s, arg, &block)
97
+ else
98
+ element(name.to_s) { |b| b.text(arg); block&.call(b) }
99
+ end
100
+ when 2
101
+ attrs, txt = args
102
+ return super unless attrs.is_a?(Hash)
103
+ element(name.to_s, attrs) { |b| b.text(txt); block&.call(b) }
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ def respond_to_missing?(name, include_private = false)
110
+ !name.to_s.end_with?("=", "?", "!") || super
111
+ end
112
+
113
+ # One native crossing for the accumulated subtree: the flush
114
+ # parses + attaches. Document sinks take exactly one root
115
+ # (set_root_ex adopts the parsed root); element sinks append
116
+ # every fragment child.
117
+ def flush
118
+ unless @depth.zero?
119
+ raise Leptris::XML::Error,
120
+ "unbalanced builder — unclosed elements remain (depth #{@depth})"
121
+ end
122
+ markup = @buf
123
+ return @sink if markup.empty?
124
+ case @sink
125
+ when Leptris::XML::Document
126
+ parsed = Leptris::XML::Document.parse(markup)
127
+ root = parsed.root
128
+ if root.nil?
129
+ raise Leptris::XML::Error,
130
+ "builder markup produced no root element"
131
+ end
132
+ @sink.root = root
133
+ @buf = +""
134
+ # The INSTALLED root — the same wrapper #root now yields
135
+ # (root= returns the parsed source, not the adoption copy).
136
+ @sink.root
137
+ when Leptris::XML::Element
138
+ added = @sink.add_child(markup)
139
+ @buf = +""
140
+ added
141
+ else
142
+ raise ArgumentError,
143
+ "builder sink must be a Document or Element, got #{@sink.class}"
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def write_attributes(attributes)
150
+ case attributes
151
+ when Hash
152
+ attributes.each do |k, v|
153
+ @buf << " " << k.to_s << '="' << escape_attr(v) << '"'
154
+ end
155
+ when nil then # nothing
156
+ else
157
+ raise ArgumentError,
158
+ "attributes must be a Hash, got #{attributes.class}"
159
+ end
160
+ end
161
+
162
+ def escape_text(value)
163
+ value.gsub(ESCAPE_RE, ESCAPE)
164
+ end
165
+
166
+ def escape_attr(value)
167
+ value.to_s.gsub(ESCAPE_RE, ESCAPE)
168
+ end
169
+ end
@@ -232,6 +232,20 @@ class Leptris::XML::Document
232
232
  wrap(raw)
233
233
  end
234
234
 
235
+ # Markup-accumulating construction (#374 lever 2): the block
236
+ # accumulates well-formed markup in Ruby; the flush performs
237
+ # ONE native crossing for the whole subtree. Exactly one root
238
+ # element — the parsed root is adopted with set_root_ex.
239
+ # Returns the flush's value (the installed root element).
240
+ #
241
+ # doc = Leptris::XML::Document.create
242
+ # doc.build { |b| b.catalog(id: "c") { b.item { b.name "x" } } }
243
+ def build
244
+ builder = Leptris::XML::Builder.new(self)
245
+ yield builder
246
+ builder.flush
247
+ end
248
+
235
249
  # Deep copy of +node+ in a NEW document — the one authority
236
250
  # behind Node#dup / Element#dup and the indent-unit path.
237
251
  # Elements deep-copy through the C copier (every child kind and
@@ -665,6 +665,17 @@ class Leptris::XML::Element < Leptris::XML::Node
665
665
  Leptris::XML::Node.wrap_fresh(ptr, @document, Leptris::XML::FFI::NODE_ELEMENT)
666
666
  end
667
667
 
668
+ # Markup-accumulating child construction (#374 lever 2): the
669
+ # block accumulates well-formed child markup in Ruby; the flush
670
+ # appends every fragment child in ONE native crossing
671
+ # (append_markup). Returns the flush's value (the NodeSet of
672
+ # added children).
673
+ def build
674
+ builder = Leptris::XML::Builder.new(self)
675
+ yield builder
676
+ builder.flush
677
+ end
678
+
668
679
  def add_child(node_or_markup)
669
680
  case node_or_markup
670
681
  when Leptris::XML::Node
data/lib/leptris/xml.rb CHANGED
@@ -14,6 +14,7 @@ module Leptris
14
14
  autoload :Namespace, "leptris/xml/namespace"
15
15
  autoload :Document, "leptris/xml/document"
16
16
  autoload :DocumentFragment, "leptris/xml/document_fragment"
17
+ autoload :Builder, "leptris/xml/builder"
17
18
  autoload :DocType, "leptris/xml/doc_type"
18
19
  autoload :NodeSet, "leptris/xml/node_set"
19
20
  autoload :Searchable, "leptris/xml/searchable"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.312.0
4
+ version: 1.9.312.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
@@ -144,6 +144,7 @@ files:
144
144
  - lib/leptris/version.rb
145
145
  - lib/leptris/xml.rb
146
146
  - lib/leptris/xml/attr.rb
147
+ - lib/leptris/xml/builder.rb
147
148
  - lib/leptris/xml/c_string_array.rb
148
149
  - lib/leptris/xml/cdata.rb
149
150
  - lib/leptris/xml/comment.rb