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 +4 -4
- data/docs/_guides/advanced-features.adoc +34 -0
- data/lib/moxml/adapter/base.rb +36 -0
- data/lib/moxml/adapter/leptris.rb +56 -2
- data/lib/moxml/adapter/nokogiri.rb +29 -0
- data/lib/moxml/context.rb +31 -1
- data/lib/moxml/error.rb +3 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xslt/stylesheet.rb +93 -0
- data/lib/moxml/xslt.rb +51 -0
- data/lib/moxml.rb +2 -0
- data/spec/consistency/xslt_spec.rb +107 -0
- data/spec/moxml/adapter/leptris_spec.rb +10 -1
- data/spec/moxml/xslt/stylesheet_spec.rb +103 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c17ad7b7e8250a5f143af509bfe551cb5c29deb1a2ae7f5cc64f275d8d460df
|
|
4
|
+
data.tar.gz: 9c6302666df0ca9a3761b2c072dc098ff3bc49a419b9cba404161e7d352bc70b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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
data/lib/moxml/version.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|