moxml 0.5.97 → 0.5.98

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: ec3d9011525010958b918f5c9665d47f54c202c96d0e3917c65d90ced667233b
4
- data.tar.gz: 4d7d535d9960f935a6419006c586db0b49981de555cfa362eee641c548c51c1b
3
+ metadata.gz: 8c17ad7b7e8250a5f143af509bfe551cb5c29deb1a2ae7f5cc64f275d8d460df
4
+ data.tar.gz: 9c6302666df0ca9a3761b2c072dc098ff3bc49a419b9cba404161e7d352bc70b
5
5
  SHA512:
6
- metadata.gz: 8e39fcd9157a62fc686befcae1e4ca077e757c6b9bfc399b590f02377199be55e45f281f9cd50dcac5eaff9d32abd8aa2d40de7fc3250b3d02dd69aadd1dfefe
7
- data.tar.gz: d947c13ae056a34a65d91d27648b54b89d4bca1c0d7a56712a77c560e7ef89cf798538af3871298ddcf5146379cc57442a7f02d359013228057a707765003f11
6
+ metadata.gz: f47afe09d0f5cbe9016846c3ca7d8b43bd7f8f79b63b7dfdc51013578a71b82ef4b28701a7dea4be55edbb96e9a2f5cc011c7cc51a7029dcdcad557e542c8c37
7
+ data.tar.gz: 1d7d971a95328b9397b71c49ff0c01df4af8ffc1e38c461a4801dd294993d58faa6eae05018000dd5c02e719a9162d25fcdf912b334409b2d3731f6fb67b36c5
@@ -103,3 +103,37 @@ See also:
103
103
 
104
104
  * link:../pages/compatibility[Adapter compatibility]
105
105
  * link:../pages/adapters/[Individual adapter documentation]
106
+
107
+ === XSLT transformation (nokogiri, leptris)
108
+
109
+ Compile-once, apply-many transformations through an implementation-agnostic
110
+ interface. Engines: nokogiri (libxslt, XSLT 1.0) and leptris (XSLT 1.0-3.0
111
+ per its engine). Adapters without an engine raise
112
+ `Moxml::NotImplementedError` at compile; `Moxml::XSLT.supported?(context)`
113
+ answers without raising.
114
+
115
+ [source,ruby]
116
+ ----
117
+ ctx = Moxml.new(:leptris)
118
+ sheet = ctx.xslt(<<~XSL)
119
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
120
+ <xsl:template match="/"><out><xsl:value-of select="count(//item)"/></out></xsl:template>
121
+ </xsl:stylesheet>
122
+ XSL
123
+
124
+ result = sheet.apply_to(doc) # Moxml::Document (same adapter)
125
+ text = sheet.apply_to_string(doc) # String — fragments and text output
126
+ ----
127
+
128
+ `apply_to` raises `Moxml::XsltError` when the output method cannot produce
129
+ XML (method="text"); `apply_to_string` carries non-XML output. Documents
130
+ parsed by another adapter serialize through their own adapter and re-parse
131
+ into the engine. Top-level parameters are part of the contract
132
+ (`params: { "name" => "value" }`); leptris does not thread them yet
133
+ (leptris-ruby#360) and raises `Moxml::XsltError` when non-empty params are
134
+ passed.
135
+
136
+ See also:
137
+
138
+ * link:../pages/compatibility[Adapter compatibility]
139
+ * link:../pages/adapters/[Individual adapter documentation]
@@ -5,6 +5,11 @@ module Moxml
5
5
  class Base
6
6
  # include XmlUtils
7
7
 
8
+ # Whether this adapter ships an XSLT engine (nokogiri/libxslt
9
+ # and leptris do). Class-body level so the capability probe is
10
+ # a plain constant read from outside.
11
+ XSLT_SUPPORTED = false
12
+
8
13
  class << self
9
14
  include XmlUtils
10
15
 
@@ -184,6 +189,37 @@ module Moxml
184
189
  one == other
185
190
  end
186
191
 
192
+ # XSLT transformation. Only engines with an XSLT processor
193
+ # implement the trio below (currently nokogiri/libxslt and
194
+ # leptris; leptris covering XSLT 2.0/3.0 per its engine) and
195
+ # declare XSLT_SUPPORTED = true at class-body level. Base
196
+ # raises so unsupported adapters fail fast at the seam.
197
+
198
+ # Compile a stylesheet source string into an engine handle.
199
+ # @raise [Moxml::XsltError] on a malformed stylesheet
200
+ def xslt_compile(_sheet_source)
201
+ raise Moxml::NotImplementedError, XSLT_UNSUPPORTED_MESSAGE
202
+ end
203
+
204
+ # Apply a compiled stylesheet; returns the engine's result
205
+ # document, or nil when the output method cannot produce one
206
+ # (text). @params is the flat quoted name/value list.
207
+ def xslt_apply_document(_sheet, _document_native, _params)
208
+ raise Moxml::NotImplementedError, XSLT_UNSUPPORTED_MESSAGE
209
+ end
210
+
211
+ # Apply a compiled stylesheet and serialize in one call —
212
+ # fragments and text output survive.
213
+ def xslt_apply_string(_sheet, _document_native, _params)
214
+ raise Moxml::NotImplementedError, XSLT_UNSUPPORTED_MESSAGE
215
+ end
216
+
217
+ # Plain string: no Moxml::XSLT reference at load time (Opal
218
+ # resolves the autoload differently); keep in sync with
219
+ # Moxml::XSLT::SUPPORTED_ADAPTERS.
220
+ XSLT_UNSUPPORTED_MESSAGE =
221
+ "XSLT requires an engine; supported adapters: nokogiri, leptris"
222
+
187
223
  # Owner element native for an attribute native, where the
188
224
  # engine exposes it (nil elsewhere).
189
225
  def attribute_element(_attribute)
@@ -142,6 +142,12 @@ module Moxml
142
142
  NATIVE_SAX_RECORDS =
143
143
  Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.242")
144
144
 
145
+ # XSLT 1.0/2.0/3.0 through the engine's compiled-stylesheet
146
+ # faces (Moxml::XSLT contract). Top-level parameters are not
147
+ # threaded through the faces yet (leptris-ruby#360) — non-empty
148
+ # params raise Moxml::XsltError.
149
+ XSLT_SUPPORTED = true
150
+
145
151
  # Defined unconditionally: the body self-guards, and call
146
152
  # sites must never depend on the layer having loaded
147
153
  # (issue #217 — binding-only installs crashed on the
@@ -149,7 +155,17 @@ module Moxml
149
155
  # Class-body level: a bare @ivar inside `class << self` lands
150
156
  # on the singleton's singleton, invisible to the methods at
151
157
  # call time (same shape as @native_doc_roots below).
152
- @binding_of = ObjectSpace::WeakMap.new
158
+ @binding_of = if Context::WEAK_WRAPPERS
159
+ ObjectSpace::WeakMap.new
160
+ else
161
+ {}.compare_by_identity
162
+ end
163
+ # Strong-mode valve (MRI < 3.4 only): the registry is
164
+ # class-level and per-node, so a hard bound keeps parse-heavy
165
+ # processes flat. A clear re-bridges on the next to_binding —
166
+ # identity loss is the same trade the 65,536 context valve
167
+ # made before the weak registry replaced it.
168
+ BINDING_OF_CAP = 8_192
153
169
 
154
170
  class << self
155
171
  # native -> bridged binding node. The bridge object is
@@ -171,6 +187,7 @@ module Moxml
171
187
  doc = doc_for(node)
172
188
  ptr = ::FFI::Pointer.new(node.address)
173
189
  bridged = ::Leptris::XML::Node.wrap(ptr, doc)
190
+ @binding_of.clear if @binding_of.size >= BINDING_OF_CAP
174
191
  @binding_of[node] = bridged unless bridged.nil?
175
192
  bridged
176
193
  end
@@ -179,7 +196,13 @@ module Moxml
179
196
  if NATIVE_READ_LAYER
180
197
  # root NativeNode -> binding document (recorded at #root
181
198
  # mint; the native layer exposes no document accessor).
182
- @native_doc_roots = ObjectSpace::WeakMap.new
199
+ @native_doc_roots = if Context::WEAK_WRAPPERS
200
+ ObjectSpace::WeakMap.new
201
+ else
202
+ {}.compare_by_identity
203
+ end
204
+ # Same strong-mode valve, keyed per document root (tiny).
205
+ NATIVE_DOC_ROOTS_CAP = 1_024
183
206
 
184
207
  class << self
185
208
  # Extend-in-place mint (#230 stage 2): a TypedData native
@@ -405,6 +428,7 @@ module Moxml
405
428
  end
406
429
 
407
430
  def record_native_doc(root_native, doc)
431
+ @native_doc_roots.clear if @native_doc_roots.size >= NATIVE_DOC_ROOTS_CAP
408
432
  @native_doc_roots[root_native] = doc
409
433
  end
410
434
 
@@ -459,6 +483,36 @@ module Moxml
459
483
  # (Document/serialize/C14n), which is circular while THIS
460
484
  # adapter file is still loading — the load-time form rescued
461
485
  # to false on every build and masked a landed engine fix.
486
+ # --- XSLT (Moxml::XSLT contract, engine faces) ---
487
+
488
+ def self.xslt_compile(sheet_source)
489
+ ::Leptris::XML::XSLT.parse(sheet_source)
490
+ rescue ::Leptris::XML::XPathError => e
491
+ raise Moxml::XsltError, "stylesheet compile failed: #{e.message}"
492
+ end
493
+
494
+ def self.xslt_apply_document(sheet, document_native, params)
495
+ assert_xslt_no_params(params)
496
+ sheet.apply_to(document_native)
497
+ rescue ::Leptris::XML::XPathError => e
498
+ raise Moxml::XsltError, "transform failed: #{e.message}"
499
+ end
500
+
501
+ def self.xslt_apply_string(sheet, document_native, params)
502
+ assert_xslt_no_params(params)
503
+ sheet.serialize(document_native)
504
+ rescue ::Leptris::XML::XPathError => e
505
+ raise Moxml::XsltError, "transform failed: #{e.message}"
506
+ end
507
+
508
+ def self.assert_xslt_no_params(params)
509
+ return if params.nil? || params.empty?
510
+
511
+ raise Moxml::XsltError,
512
+ "XSLT params are not threaded by the leptris engine yet " \
513
+ "(leptris-ruby#360); pass an empty params hash"
514
+ end
515
+
462
516
  def self.native_c14n_byte_safe?
463
517
  return @native_c14n_byte_safe unless @native_c14n_byte_safe.nil?
464
518
 
@@ -7,6 +7,9 @@ require "nokogiri"
7
7
  module Moxml
8
8
  module Adapter
9
9
  class Nokogiri < Base
10
+ # libxslt-backed XSLT 1.0 (Moxml::XSLT contract).
11
+ XSLT_SUPPORTED = true
12
+
10
13
  class << self
11
14
  def attachments
12
15
  @attachments ||= Moxml::NativeAttachment.new
@@ -50,6 +53,32 @@ module Moxml
50
53
  true
51
54
  end
52
55
 
56
+ # --- XSLT (Moxml::XSLT contract, libxslt-backed) ---
57
+
58
+ def xslt_compile(sheet_source)
59
+ ::Nokogiri::XSLT.parse(sheet_source)
60
+ rescue ::RuntimeError => e
61
+ # Nokogiri raises bare RuntimeError for compilation errors.
62
+ raise Moxml::XsltError, "stylesheet compile failed: #{e.message}"
63
+ end
64
+
65
+ def xslt_apply_document(sheet, document_native, params)
66
+ sheet.transform(document_native, params)
67
+ rescue ::RuntimeError => e
68
+ raise Moxml::XsltError, "transform failed: #{e.message}"
69
+ end
70
+
71
+ def xslt_apply_string(sheet, document_native, params)
72
+ # libxslt always yields a document; the string face is its
73
+ # serialization AS_XML (compact — the default to_xml would
74
+ # re-indent and diverge from engines that serialize without
75
+ # formatting).
76
+ result = xslt_apply_document(sheet, document_native, params)
77
+ result.to_xml(
78
+ save_with: ::Nokogiri::XML::Node::SaveOptions::AS_XML,
79
+ )
80
+ end
81
+
53
82
  # libxml2-native inclusive C14N: delegated for the default
54
83
  # shape (Inclusive 1.0, no comments, no InclusiveNamespaces
55
84
  # prefix list) when the probe below proves libxml2's output
data/lib/moxml/context.rb CHANGED
@@ -5,7 +5,17 @@ module Moxml
5
5
  # Opal's default runtime excludes ObjectSpace entirely; the weak
6
6
  # registry needs WeakMap, so the strong Hash (with its
7
7
  # wholesale-clear valve) covers that platform.
8
- WEAK_WRAPPERS = !defined?(ObjectSpace::WeakMap).nil?
8
+ # MRI 3.3's ObjectSpace::WeakMap has a mark-time hazard: a
9
+ # dangling VALUE in the weak table is dereferenced during
10
+ # incremental-GC marking (SIGSEGV in wmap_mark_weak_table_i —
11
+ # lutaml/moxml#303, reproduced with the leptris adapter on the
12
+ # xmi suite; 100% crash rate 1.9.261-1.9.282, clean with these
13
+ # registries strong). Ruby 3.4 rewrote WeakMap and is safe.
14
+ # MRI < 3.4 takes the strong identity-Hash fallback (the Opal
15
+ # shape, with the 65,536 valve in register_wrapper).
16
+ WEAK_WRAPPERS = !defined?(ObjectSpace::WeakMap).nil? &&
17
+ !(RUBY_ENGINE == "ruby" &&
18
+ Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4"))
9
19
  attr_reader :config
10
20
 
11
21
  def initialize(adapter = nil)
@@ -111,6 +121,26 @@ module Moxml
111
121
  config.adapter.parse_html(html, options, self)
112
122
  end
113
123
 
124
+ # Compile an XSLT stylesheet with this context's adapter
125
+ # (implementation-agnostic; nokogiri/libxslt and leptris ship
126
+ # engines, leptris covering XSLT 2.0/3.0 per its engine). Apply
127
+ # the returned Stylesheet to documents parsed by this context.
128
+ #
129
+ # @param stylesheet [String, Moxml::Document] the stylesheet
130
+ # source; documents from other adapters serialize through their
131
+ # own adapter first
132
+ # @return [Moxml::XSLT::Stylesheet]
133
+ # @raise [Moxml::NotImplementedError] on adapters without an engine
134
+ # @raise [Moxml::XsltError] on a malformed stylesheet
135
+ def xslt(stylesheet)
136
+ source = case stylesheet
137
+ when Moxml::Document then stylesheet.to_xml
138
+ else stylesheet.to_s
139
+ end
140
+ sheet = config.adapter.xslt_compile(source)
141
+ Moxml::XSLT::Stylesheet.new(sheet, self)
142
+ end
143
+
114
144
  # Streaming incremental parse (leptris): yields each completed
115
145
  # element while parsing — :top_level yields the root's children,
116
146
  # :full_document every element in completion (post-order) order.
data/lib/moxml/error.rb CHANGED
@@ -144,4 +144,7 @@ module Moxml
144
144
  msg
145
145
  end
146
146
  end
147
+
148
+ # Error raised when XSLT compilation or transformation fails
149
+ class XsltError < Error; end
147
150
  end
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.97"
4
+ VERSION = "0.5.98"
5
5
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module XSLT
5
+ # A compiled stylesheet bound to one adapter context. Apply to
6
+ # documents parsed by the same adapter; cross-adapter documents
7
+ # take the serialized path (to_xml -> engine -> parse).
8
+ class Stylesheet
9
+ # @api adapter — the engine's compiled stylesheet handle
10
+ attr_reader :native
11
+
12
+ def initialize(native_sheet, context)
13
+ @native = native_sheet
14
+ @context = context
15
+ @adapter = context.config.adapter
16
+ end
17
+
18
+ # Transform +document+ and return the result as a
19
+ # Moxml::Document of this stylesheet's adapter.
20
+ #
21
+ # Stylesheets whose output method is not XML raise
22
+ # Moxml::XsltError — the serialized form is available through
23
+ # apply_to_string.
24
+ #
25
+ # @param params [Hash] top-level stylesheet parameters
26
+ # (name => string value); engines without a params face
27
+ # raise Moxml::XsltError when non-empty
28
+ def apply_to(document, params: {})
29
+ quoted = quote_params(params)
30
+ return apply_cross_adapter(document, quoted) unless same_adapter?(document)
31
+
32
+ result = @adapter.xslt_apply_document(@native, document.native, quoted)
33
+ return parse_serialized(document, quoted) if result.nil?
34
+
35
+ wrapped = Wrappers::Document.new(result, @context)
36
+ # Engines differ on non-XML output: leptris returns nil, nokogiri
37
+ # wraps the text into a rootless document. Normalize to a typed
38
+ # error — apply_to_string carries non-XML output.
39
+ raise XsltError, non_xml_message if wrapped.root.nil?
40
+
41
+ wrapped
42
+ end
43
+
44
+ # Transform +document+ and return the engine-serialized output.
45
+ # Preserves result fragments and text output that the document
46
+ # face cannot represent.
47
+ def apply_to_string(document, params: {})
48
+ quoted = quote_params(params)
49
+ return apply_cross_adapter_string(document, quoted) unless same_adapter?(document)
50
+
51
+ @adapter.xslt_apply_string(@native, document.native, quoted)
52
+ end
53
+
54
+ private
55
+
56
+ def same_adapter?(document)
57
+ document.context.config.adapter.equal?(@adapter)
58
+ end
59
+
60
+ # Cross-adapter documents serialize through their own adapter
61
+ # and re-parse into the stylesheet's engine — the engine cannot
62
+ # share its tree across adapters.
63
+ def apply_cross_adapter(document, quoted)
64
+ engine_doc = @context.parse(document.to_xml)
65
+ result = @adapter.xslt_apply_document(@native, engine_doc.native, quoted)
66
+ return parse_serialized(engine_doc, quoted) if result.nil?
67
+
68
+ Wrappers::Document.new(result, @context)
69
+ end
70
+
71
+ def apply_cross_adapter_string(document, quoted)
72
+ engine_doc = @context.parse(document.to_xml)
73
+ @adapter.xslt_apply_string(@native, engine_doc.native, quoted)
74
+ end
75
+
76
+ def parse_serialized(document, quoted)
77
+ @context.parse(@adapter.xslt_apply_string(@native, document.native, quoted))
78
+ rescue Moxml::ParseError
79
+ raise XsltError, non_xml_message
80
+ end
81
+
82
+ def non_xml_message
83
+ "transform output is not XML (output method text?); use apply_to_string"
84
+ end
85
+
86
+ def quote_params(params)
87
+ return [] if params.nil? || params.empty?
88
+
89
+ XSLT.quote_params(params)
90
+ end
91
+ end
92
+ end
93
+ end
data/lib/moxml/xslt.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ # XSLT transformation. Implementation-agnostic over engines that
5
+ # ship an XSLT processor (currently nokogiri/libxslt and leptris;
6
+ # leptris additionally covers XSLT 2.0/3.0 per its engine).
7
+ #
8
+ # ctx = Moxml.new(:nokogiri)
9
+ # sheet = ctx.xslt(<<~XSL)
10
+ # <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
11
+ # <xsl:template match="/"><out><xsl:value-of select="count(//i)"/></out></xsl:template>
12
+ # </xsl:stylesheet>
13
+ # XSL
14
+ # sheet.apply_to(doc) # => Moxml::Document (same adapter)
15
+ # sheet.apply_to_string(doc) # => String (engine-serialized output;
16
+ # # fragments and text output preserved)
17
+ #
18
+ # Adapters without an engine raise Moxml::NotImplementedError at
19
+ # compile; Moxml::XSLT.supported?(context) answers without raising.
20
+ #
21
+ # Stylesheet top-level parameters are part of the contract but not
22
+ # every engine threads them yet — passing non-empty params to an
23
+ # engine without the face raises Moxml::XsltError (leptris:
24
+ # leptris-ruby#360).
25
+ module XSLT
26
+ autoload :Stylesheet, "moxml/xslt/stylesheet"
27
+
28
+ SUPPORTED_ADAPTERS = %i[nokogiri leptris].freeze
29
+
30
+ class << self
31
+ # Whether the context's adapter ships an XSLT engine. Answers
32
+ # without raising; compile raises Moxml::NotImplementedError
33
+ # regardless (the capability probe is advisory).
34
+ def supported?(context)
35
+ adapter = context.config.adapter
36
+ adapter.const_defined?(:XSLT_SUPPORTED) &&
37
+ adapter.const_get(:XSLT_SUPPORTED)
38
+ end
39
+
40
+ # Quote XSLT top-level string parameters into the flat
41
+ # name/value list libxslt-family engines accept. Values are
42
+ # single-quoted; embedded quotes are escaped.
43
+ def quote_params(params)
44
+ params.flat_map do |name, value|
45
+ quoted = "'#{value.to_s.gsub("'", %q{'\''})}'"
46
+ [name.to_s, quoted]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/moxml.rb CHANGED
@@ -100,6 +100,7 @@ module Moxml
100
100
  autoload :SAX, "moxml/sax"
101
101
  autoload :Signature, "moxml/signature"
102
102
  autoload :C14n, "moxml/c14n"
103
+ autoload :XSLT, "moxml/xslt"
103
104
 
104
105
  # Error hierarchy — each subclass autoloads from the same file
105
106
  autoload :Error, "moxml/error"
@@ -111,4 +112,5 @@ module Moxml
111
112
  autoload :EntityDataError, "moxml/error"
112
113
  autoload :XPathError, "moxml/error"
113
114
  autoload :AdapterError, "moxml/error"
115
+ autoload :XsltError, "moxml/error"
114
116
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/c14n"
5
+
6
+ # Cross-engine agreement for the Moxml::XSLT contract. The corpus
7
+ # sticks to XSLT 1.0 shapes (the common denominator — nokogiri/
8
+ # libxslt speaks 1.0 only; leptris additionally covers 2.0/3.0 in its
9
+ # own suite). apply_to_string output is engine-serialized: the
10
+ # comparison strips a leading declaration when the engines disagree
11
+ # on emitting one.
12
+ SUPPORTED = %i[nokogiri leptris].select do |name|
13
+ Moxml::Adapter::AVAILABLE_ADAPTERS.include?(name)
14
+ end.freeze
15
+
16
+ CORPUS = {
17
+ "value-of with count" => [
18
+ <<~XSL,
19
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
20
+ <xsl:template match="/"><out><xsl:value-of select="count(//i)"/></out></xsl:template>
21
+ </xsl:stylesheet>
22
+ XSL
23
+ "<r><i>1</i><i>2</i></r>",
24
+ /<out>2<\/out>/,
25
+ ],
26
+ "for-each with sort" => [
27
+ <<~XSL,
28
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
29
+ <xsl:template match="/">
30
+ <out><xsl:for-each select="//i"><xsl:sort select="." order="descending"/><v><xsl:value-of select="."/></v></xsl:for-each></out>
31
+ </xsl:template>
32
+ </xsl:stylesheet>
33
+ XSL
34
+ "<r><i>3</i><i>1</i><i>2</i></r>",
35
+ /<v>3<\/v>\s*<v>2<\/v>\s*<v>1<\/v>/,
36
+ ],
37
+ "copy-of" => [
38
+ <<~XSL,
39
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
40
+ <xsl:template match="/"><out><xsl:copy-of select="//keep/*"/></out></xsl:template>
41
+ </xsl:stylesheet>
42
+ XSL
43
+ %(<r><keep><a x="1">t</a></keep></r>),
44
+ /<a x="1">t<\/a>/,
45
+ ],
46
+ "named template" => [
47
+ <<~XSL,
48
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
49
+ <xsl:template match="/"><out><xsl:call-template name="greet"/></out></xsl:template>
50
+ <xsl:template name="greet">hi</xsl:template>
51
+ </xsl:stylesheet>
52
+ XSL
53
+ "<r/>",
54
+ /<out>hi<\/out>/,
55
+ ],
56
+ "attribute value template" => [
57
+ <<~XSL,
58
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
59
+ <xsl:template match="/"><out n="{count(//i)}"/></xsl:template>
60
+ </xsl:stylesheet>
61
+ XSL
62
+ "<r><i/><i/></r>",
63
+ /<out\s+n="2"\s*\/?>/,
64
+ ],
65
+ "namespaced output" => [
66
+ <<~XSL,
67
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:o="urn:o" version="1.0">
68
+ <xsl:template match="/"><o:out><xsl:value-of select="//t"/></o:out></xsl:template>
69
+ </xsl:stylesheet>
70
+ XSL
71
+ "<r><t>v</t></r>",
72
+ /<[^>]*:out[^>]*>v<\/[^>]*:out>/,
73
+ ],
74
+ }.freeze
75
+
76
+ RSpec.describe "Moxml::XSLT cross-engine agreement" do
77
+ SUPPORTED.each do |adapter_name|
78
+ context "with the #{adapter_name} adapter" do
79
+ let(:ctx) { Moxml.new(adapter_name) }
80
+
81
+ def result_body(output)
82
+ output.sub(/\A<\?xml[^>]*\?>\s*/, "")
83
+ end
84
+
85
+ CORPUS.each do |label, (sheet_xml, input, expected_re)|
86
+ it "transforms #{label}" do
87
+ sheet = ctx.xslt(sheet_xml)
88
+ doc = ctx.parse(input)
89
+ expect(result_body(sheet.apply_to_string(doc))).to match(expected_re)
90
+ expect(sheet.apply_to(doc).to_xml).to match(expected_re)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ it "agrees across engines on the corpus" do
97
+ return unless SUPPORTED.size == 2
98
+
99
+ first, second = SUPPORTED.map { |name| Moxml.new(name) }
100
+ CORPUS.each do |label, (sheet_xml, input, _)|
101
+ a = first.xslt(sheet_xml).apply_to_string(first.parse(input))
102
+ b = second.xslt(sheet_xml).apply_to_string(second.parse(input))
103
+ expect(Moxml::C14n.equivalent?(a, b))
104
+ .to(be(true), "#{label}: engines disagree\n a=#{a.inspect}\n b=#{b.inspect}")
105
+ end
106
+ end
107
+ end
@@ -509,7 +509,16 @@ RSpec.describe Moxml::Adapter::Leptris do
509
509
  # The binding retains a constant one-document wrapper set of
510
510
  # its own; moxml must not retain beyond a couple of dropped
511
511
  # documents' worth (was: all 20 pinned under the strong map).
512
- expect(count.call - before).to be < 2 * 51
512
+ if Moxml::Context::WEAK_WRAPPERS
513
+ expect(count.call - before).to be < 2 * 51
514
+ else
515
+ # MRI < 3.4 strong registry (moxml#303): entries are bounded
516
+ # by the to_binding valve instead of weakly released — no
517
+ # unbounded pinning, but dropped documents stay until the
518
+ # clear. Assert the bound, not weak-release semantics.
519
+ cap = Moxml::Adapter::Leptris::BINDING_OF_CAP
520
+ expect(count.call - before).to be <= cap
521
+ end
513
522
  end
514
523
 
515
524
  it "keeps wrapper identity while a document is alive" do
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # The Moxml::XSLT contract: compile once, apply many. Only engines
6
+ # with an XSLT processor implement it (nokogiri/libxslt; leptris
7
+ # covering XSLT 1.0-3.0). leptris does not thread top-level params
8
+ # yet (leptris-ruby#360) — non-empty params raise Moxml::XsltError.
9
+ SHEET_COUNT = <<~XSL
10
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
11
+ <xsl:template match="/"><out><xsl:value-of select="count(//i)"/></out></xsl:template>
12
+ </xsl:stylesheet>
13
+ XSL
14
+
15
+ RSpec.describe "Moxml::XSLT" do
16
+ let(:ctx) { Moxml.new(adapter_name) }
17
+ let(:doc) { ctx.parse("<r><i>1</i><i>2</i></r>") }
18
+
19
+ context "with the nokogiri adapter" do
20
+ let(:adapter_name) { :nokogiri }
21
+
22
+ it "compiles and applies to a document" do
23
+ sheet = ctx.xslt(SHEET_COUNT)
24
+ expect(sheet.apply_to(doc).root.name).to eq("out")
25
+ expect(sheet.apply_to(doc).root.text).to eq("2")
26
+ end
27
+
28
+ it "returns engine-serialized output as a string" do
29
+ sheet = ctx.xslt(SHEET_COUNT)
30
+ expect(sheet.apply_to_string(doc)).to include("<out>2</out>")
31
+ end
32
+
33
+ it "threads top-level params" do
34
+ sheet = ctx.xslt(<<~XSL)
35
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
36
+ <xsl:param name="n" select="'0'"/>
37
+ <xsl:template match="/"><out><xsl:value-of select="$n"/></out></xsl:template>
38
+ </xsl:stylesheet>
39
+ XSL
40
+ expect(sheet.apply_to_string(doc, params: { "n" => "7" })).to include("7")
41
+ end
42
+
43
+ it "raises XsltError on a malformed stylesheet" do
44
+ expect { ctx.xslt("<not-a-stylesheet/>") }
45
+ .to raise_error(Moxml::XsltError, /compile failed/)
46
+ end
47
+
48
+ it "raises XsltError on a broken transform" do
49
+ sheet = ctx.xslt(<<~XSL)
50
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
51
+ <xsl:template match="/"><out><xsl:value-of select="$undefined"/></out></xsl:template>
52
+ </xsl:stylesheet>
53
+ XSL
54
+ expect { sheet.apply_to(doc) }.to raise_error(Moxml::XsltError)
55
+ end
56
+
57
+ it "reports support" do
58
+ expect(Moxml::XSLT.supported?(ctx)).to be(true)
59
+ end
60
+ end
61
+
62
+ context "with the leptris adapter" do
63
+ let(:adapter_name) { :leptris }
64
+
65
+ it "compiles and applies to a document" do
66
+ sheet = ctx.xslt(SHEET_COUNT)
67
+ expect(sheet.apply_to(doc).root.name).to eq("out")
68
+ expect(sheet.apply_to(doc).root.text).to eq("2")
69
+ end
70
+
71
+ it "preserves fragments through the string face" do
72
+ sheet = ctx.xslt(<<~XSL)
73
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
74
+ <xsl:template match="/">plain text result</xsl:template>
75
+ </xsl:stylesheet>
76
+ XSL
77
+ expect(sheet.apply_to_string(doc)).to include("plain text result")
78
+ end
79
+
80
+ it "raises XsltError on non-empty params (leptris-ruby#360)" do
81
+ sheet = ctx.xslt(SHEET_COUNT)
82
+ expect { sheet.apply_to(doc, params: { "n" => "7" }) }
83
+ .to raise_error(Moxml::XsltError, /params.*#360/)
84
+ end
85
+
86
+ it "reports support" do
87
+ expect(Moxml::XSLT.supported?(ctx)).to be(true)
88
+ end
89
+ end
90
+
91
+ context "with an adapter without an engine" do
92
+ let(:ctx) { Moxml.new(:rexml) }
93
+
94
+ it "raises NotImplementedError at compile" do
95
+ expect { ctx.xslt(SHEET_COUNT) }
96
+ .to raise_error(Moxml::NotImplementedError, /nokogiri, leptris/)
97
+ end
98
+
99
+ it "answers supported? false" do
100
+ expect(Moxml::XSLT.supported?(ctx)).to be(false)
101
+ end
102
+ end
103
+ 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.97
4
+ version: 0.5.98
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -293,6 +293,8 @@ files:
293
293
  - lib/moxml/xpath/parser.rb
294
294
  - lib/moxml/xpath/ruby/generator.rb
295
295
  - lib/moxml/xpath/ruby/node.rb
296
+ - lib/moxml/xslt.rb
297
+ - lib/moxml/xslt/stylesheet.rb
296
298
  - moxml.gemspec
297
299
  - old-specs/moxml/adapter/customized_libxml/.gitkeep
298
300
  - reference-docs/w3c-xmldsig-bestpractices.md
@@ -304,6 +306,7 @@ files:
304
306
  - spec/consistency/adapter_parity_spec.rb
305
307
  - spec/consistency/eol_normalization_spec.rb
306
308
  - spec/consistency/round_trip_spec.rb
309
+ - spec/consistency/xslt_spec.rb
307
310
  - spec/examples/README.md
308
311
  - spec/examples/attribute_examples_spec.rb
309
312
  - spec/examples/basic_usage_spec.rb
@@ -555,6 +558,7 @@ files:
555
558
  - spec/moxml/xpath/ruby/node_spec.rb
556
559
  - spec/moxml/xpath_capabilities_spec.rb
557
560
  - spec/moxml/xpath_spec.rb
561
+ - spec/moxml/xslt/stylesheet_spec.rb
558
562
  - spec/performance/README.md
559
563
  - spec/performance/benchmark_spec.rb
560
564
  - spec/performance/memory_usage_spec.rb