canon 0.3.4 → 0.3.5
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/lib/canon/html/data_model.rb +48 -145
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +116 -282
- data/lib/canon/xml/sax_builder.rb +2 -1
- data/lib/canon/xml/tree_builder.rb +106 -0
- data/lib/canon/xml/whitespace_policy.rb +37 -5
- data/lib/canon/xml.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8912c851a2e2b0e250fcae120cda428d7705f9c6262b57ec7fe1218aaf186229
|
|
4
|
+
data.tar.gz: 98a7b6c64818395e18c1de7b8918255368b6b4bf8766ecaa567b5502c7c084b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2fee687e2469d42193c4f58a294179dc8332aca3fefca5c3ad476cdc061e40c2370d2183790b20c1e43ae83b420ae78567cf7f27114c128f21b0ab8d76be46fe
|
|
7
|
+
data.tar.gz: 61a58938d48f48ce066c93b09e734bc62a05cfccec1c7fea80ecdb4de97663e589940108f89f0d8f947737d096ca9c21b9f41edbe51569da5bcc2ad5c990fd0f
|
|
@@ -76,171 +76,74 @@ module Canon
|
|
|
76
76
|
Canon::Xml::DataModel.serialize(node)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
# Build XPath data model from Nokogiri document or
|
|
79
|
+
# Build XPath data model from a Nokogiri HTML document or
|
|
80
|
+
# fragment — a TreeBuilder walk like the XML extractors, with the
|
|
81
|
+
# HTML whitespace policy and xmlns-free attributes.
|
|
80
82
|
def self.build_from_nokogiri(nokogiri_doc)
|
|
83
|
+
builder = Canon::Xml::TreeBuilder.new
|
|
81
84
|
root = Canon::Xml::Nodes::RootNode.new
|
|
85
|
+
skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : []
|
|
82
86
|
|
|
83
87
|
if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
nokogiri_doc.children.each do |child|
|
|
89
|
-
next if child == nokogiri_doc.root
|
|
90
|
-
next if child.is_a?(Nokogiri::XML::DTD)
|
|
91
|
-
|
|
92
|
-
node = build_node_from_nokogiri(child)
|
|
93
|
-
root.add_child(node) if node
|
|
88
|
+
root.add_child(walk(builder, nokogiri_doc.root))
|
|
89
|
+
builder.add_document_children(root, nokogiri_doc.children,
|
|
90
|
+
nokogiri_doc.root, skip_types) do |child|
|
|
91
|
+
walk(builder, child)
|
|
94
92
|
end
|
|
95
93
|
else
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
next if child.is_a?(Nokogiri::XML::DTD)
|
|
100
|
-
|
|
101
|
-
node = build_node_from_nokogiri(child)
|
|
102
|
-
root.add_child(node) if node
|
|
94
|
+
builder.add_document_children(root, nokogiri_doc.children,
|
|
95
|
+
nil, skip_types) do |child|
|
|
96
|
+
walk(builder, child)
|
|
103
97
|
end
|
|
104
98
|
end
|
|
105
99
|
|
|
106
100
|
root
|
|
107
101
|
end
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
case nokogiri_node
|
|
103
|
+
def self.walk(builder, node, inherited_namespaces: nil)
|
|
104
|
+
case node
|
|
112
105
|
when Nokogiri::XML::Element
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
when Nokogiri::XML::Comment
|
|
117
|
-
build_comment_node(nokogiri_node)
|
|
118
|
-
when Nokogiri::XML::ProcessingInstruction
|
|
119
|
-
build_pi_node(nokogiri_node)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Build element node from Nokogiri element
|
|
124
|
-
def self.build_element_node(nokogiri_element)
|
|
125
|
-
element = Canon::Xml::Nodes::ElementNode.new(
|
|
126
|
-
name: nokogiri_element.name,
|
|
127
|
-
namespace_uri: nokogiri_element.namespace&.href,
|
|
128
|
-
prefix: nokogiri_element.namespace&.prefix,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
# Build namespace nodes (includes inherited namespaces)
|
|
132
|
-
build_namespace_nodes(nokogiri_element, element)
|
|
133
|
-
|
|
134
|
-
# Build attribute nodes
|
|
135
|
-
build_attribute_nodes(nokogiri_element, element)
|
|
136
|
-
|
|
137
|
-
# Build child nodes
|
|
138
|
-
nokogiri_element.children.each do |child|
|
|
139
|
-
node = build_node_from_nokogiri(child)
|
|
140
|
-
element.add_child(node) if node
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
element
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Build namespace nodes for an element
|
|
147
|
-
def self.build_namespace_nodes(nokogiri_element, element)
|
|
148
|
-
# Collect all in-scope namespaces
|
|
149
|
-
namespaces = collect_in_scope_namespaces(nokogiri_element)
|
|
150
|
-
|
|
151
|
-
namespaces.each do |prefix, uri|
|
|
152
|
-
ns_node = Canon::Xml::Nodes::NamespaceNode.new(
|
|
153
|
-
prefix: prefix,
|
|
154
|
-
uri: uri,
|
|
106
|
+
scope = builder.merge_namespace_scope(
|
|
107
|
+
inherited_namespaces,
|
|
108
|
+
node.namespace_definitions.map { |ns| [ns.prefix, ns.href] },
|
|
155
109
|
)
|
|
156
|
-
element.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
current.namespace_definitions.each do |ns|
|
|
169
|
-
prefix = ns.prefix || ""
|
|
170
|
-
# Only add if not already defined (child overrides parent)
|
|
171
|
-
unless namespaces.key?(prefix)
|
|
172
|
-
namespaces[prefix] = ns.href
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
current = current.parent
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Always include xml namespace
|
|
180
|
-
namespaces["xml"] ||= "http://www.w3.org/XML/1998/namespace"
|
|
181
|
-
|
|
182
|
-
namespaces
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Build attribute nodes for an element
|
|
186
|
-
def self.build_attribute_nodes(nokogiri_element, element)
|
|
187
|
-
nokogiri_element.attributes.each do |name, attr|
|
|
188
|
-
next if name.start_with?("xmlns")
|
|
189
|
-
|
|
190
|
-
attr_node = Canon::Xml::Nodes::AttributeNode.new(
|
|
191
|
-
name: attr.name,
|
|
192
|
-
value: attr.value,
|
|
193
|
-
namespace_uri: attr.namespace&.href,
|
|
194
|
-
prefix: attr.namespace&.prefix,
|
|
110
|
+
element = builder.element(
|
|
111
|
+
name: node.name,
|
|
112
|
+
prefix: node.namespace&.prefix,
|
|
113
|
+
namespace_uri: node.namespace&.href,
|
|
114
|
+
# HTML attributes are namespace-free; xmlns declarations are
|
|
115
|
+
# not reported as attributes.
|
|
116
|
+
attributes: node.attribute_nodes.filter_map do |attr|
|
|
117
|
+
next if attr.name.start_with?("xmlns")
|
|
118
|
+
|
|
119
|
+
[attr.name, attr.value, nil, nil]
|
|
120
|
+
end,
|
|
121
|
+
namespace_scope: scope,
|
|
195
122
|
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
# Build text node from Nokogiri text node
|
|
201
|
-
# HTML-specific: handles whitespace-sensitive elements (pre, code, textarea, script, style)
|
|
202
|
-
# and preserves whitespace between inline element siblings.
|
|
203
|
-
def self.build_text_node(nokogiri_text)
|
|
204
|
-
# Skip text nodes that are only whitespace between elements
|
|
205
|
-
# EXCEPT in whitespace-sensitive elements (pre, code, textarea, script, style)
|
|
206
|
-
# and when whitespace is between inline element siblings (semantically significant)
|
|
207
|
-
content = nokogiri_text.content
|
|
208
|
-
|
|
209
|
-
# NBSP (U+00A0) is never insignificant whitespace
|
|
210
|
-
if content.strip.empty? && nokogiri_text.parent.is_a?(Nokogiri::XML::Element) && !content.include?("\u00A0")
|
|
211
|
-
# Check if parent is whitespace-sensitive
|
|
212
|
-
parent_name = nokogiri_text.parent.name.downcase
|
|
213
|
-
whitespace_sensitive_tags = %w[pre code textarea script style]
|
|
214
|
-
|
|
215
|
-
# Check if whitespace is between inline siblings
|
|
216
|
-
unless whitespace_sensitive_tags.include?(parent_name) ||
|
|
217
|
-
Canon::Comparison::WhitespaceSensitivity.inline_whitespace_significant?(nokogiri_text)
|
|
218
|
-
return nil
|
|
123
|
+
node.children.each do |child|
|
|
124
|
+
built = walk(builder, child, inherited_namespaces: scope)
|
|
125
|
+
element.add_child(built) if built
|
|
219
126
|
end
|
|
127
|
+
element
|
|
128
|
+
when Nokogiri::XML::Text
|
|
129
|
+
builder.text(
|
|
130
|
+
node.content,
|
|
131
|
+
keep: Canon::Xml::WhitespacePolicy.keep_html_text?(
|
|
132
|
+
node.content,
|
|
133
|
+
parent_name: node.parent.is_a?(Nokogiri::XML::Element) ? node.parent.name : nil,
|
|
134
|
+
inline_significant:
|
|
135
|
+
Canon::Comparison::WhitespaceSensitivity.inline_whitespace_significant?(node),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
when Nokogiri::XML::Comment
|
|
139
|
+
builder.comment(node.content)
|
|
140
|
+
when Nokogiri::XML::ProcessingInstruction
|
|
141
|
+
builder.processing_instruction(node.name, node.content)
|
|
220
142
|
end
|
|
221
|
-
|
|
222
|
-
# Nokogiri already handles CDATA conversion and entity resolution
|
|
223
|
-
Canon::Xml::Nodes::TextNode.new(value: content)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Build comment node from Nokogiri comment
|
|
227
|
-
def self.build_comment_node(nokogiri_comment)
|
|
228
|
-
Canon::Xml::Nodes::CommentNode.new(value: nokogiri_comment.content)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Build PI node from Nokogiri PI
|
|
232
|
-
def self.build_pi_node(nokogiri_pi)
|
|
233
|
-
Canon::Xml::Nodes::ProcessingInstructionNode.new(
|
|
234
|
-
target: nokogiri_pi.name,
|
|
235
|
-
data: nokogiri_pi.content,
|
|
236
|
-
)
|
|
237
143
|
end
|
|
238
144
|
|
|
239
145
|
class << self
|
|
240
|
-
private :build_from_nokogiri, :
|
|
241
|
-
:build_element_node, :build_namespace_nodes,
|
|
242
|
-
:collect_in_scope_namespaces, :build_attribute_nodes,
|
|
243
|
-
:build_text_node, :build_comment_node, :build_pi_node
|
|
146
|
+
private :build_from_nokogiri, :walk
|
|
244
147
|
end
|
|
245
148
|
end
|
|
246
149
|
end
|
data/lib/canon/version.rb
CHANGED
data/lib/canon/xml/data_model.rb
CHANGED
|
@@ -126,149 +126,85 @@ module Canon
|
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
# ---
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
skip_types, converter)
|
|
137
|
-
children.each do |child|
|
|
138
|
-
next if child.equal?(document_element)
|
|
139
|
-
next if skip_types.any? { |type| child.is_a?(type) }
|
|
140
|
-
|
|
141
|
-
node = converter.call(child)
|
|
142
|
-
root.add_child(node) if node
|
|
143
|
-
end
|
|
144
|
-
end
|
|
129
|
+
# --- Engine extractors ---
|
|
130
|
+
#
|
|
131
|
+
# Traversal and engine mapping only. Each walk maps its engine's
|
|
132
|
+
# shapes onto TreeBuilder's interface; all construction semantics
|
|
133
|
+
# (namespace scopes, attribute normalization, whitespace policy,
|
|
134
|
+
# node kinds, document ordering) live in TreeBuilder and
|
|
135
|
+
# WhitespacePolicy — one dialect, three feeds.
|
|
145
136
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
#
|
|
149
|
-
# namespace.
|
|
150
|
-
def self.merge_namespace_scope(inherited, declaration_pairs)
|
|
151
|
-
scope = inherited ? inherited.dup : { "xml" => "http://www.w3.org/XML/1998/namespace" }
|
|
152
|
-
declaration_pairs.each do |prefix, uri|
|
|
153
|
-
scope[prefix || ""] = uri
|
|
154
|
-
end
|
|
155
|
-
scope
|
|
156
|
-
end
|
|
137
|
+
TREE_BUILDER = TreeBuilder.new
|
|
138
|
+
|
|
139
|
+
# -- Nokogiri walk: top-down, scope flows down the recursion --
|
|
157
140
|
|
|
158
141
|
def self.build_from_nokogiri(nokogiri_doc, preserve_whitespace: false)
|
|
159
142
|
root = Nodes::RootNode.new
|
|
160
|
-
|
|
161
|
-
converter = ->(child) { build_node_from_nokogiri(child, preserve_whitespace: preserve_whitespace) }
|
|
162
143
|
skip_types = defined?(Nokogiri) ? [Nokogiri::XML::DTD] : []
|
|
163
144
|
|
|
164
145
|
if nokogiri_doc.is_a?(Nokogiri::XML::Document) && nokogiri_doc.root
|
|
165
|
-
root.add_child(
|
|
166
|
-
|
|
167
|
-
add_document_children(root, nokogiri_doc.children,
|
|
168
|
-
|
|
146
|
+
root.add_child(walk_nokogiri(nokogiri_doc.root,
|
|
147
|
+
preserve_whitespace: preserve_whitespace))
|
|
148
|
+
TREE_BUILDER.add_document_children(root, nokogiri_doc.children,
|
|
149
|
+
nokogiri_doc.root, skip_types) do |child|
|
|
150
|
+
walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
|
|
151
|
+
end
|
|
169
152
|
else
|
|
170
|
-
add_document_children(root, nokogiri_doc.children,
|
|
171
|
-
|
|
153
|
+
TREE_BUILDER.add_document_children(root, nokogiri_doc.children,
|
|
154
|
+
nil, skip_types) do |child|
|
|
155
|
+
walk_nokogiri(child, preserve_whitespace: preserve_whitespace)
|
|
156
|
+
end
|
|
172
157
|
end
|
|
173
158
|
|
|
174
159
|
root
|
|
175
160
|
end
|
|
176
161
|
|
|
177
|
-
def self.
|
|
178
|
-
|
|
179
|
-
case
|
|
162
|
+
def self.walk_nokogiri(node, preserve_whitespace:,
|
|
163
|
+
inherited_namespaces: nil)
|
|
164
|
+
case node
|
|
180
165
|
when Nokogiri::XML::Element
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
166
|
+
scope = TREE_BUILDER.merge_namespace_scope(
|
|
167
|
+
inherited_namespaces,
|
|
168
|
+
node.namespace_definitions.map { |ns| [ns.prefix, ns.href] },
|
|
169
|
+
)
|
|
170
|
+
element = TREE_BUILDER.element(
|
|
171
|
+
name: node.name,
|
|
172
|
+
prefix: node.namespace&.prefix,
|
|
173
|
+
namespace_uri: node.namespace&.href,
|
|
174
|
+
attributes: node.attribute_nodes.map do |attr|
|
|
175
|
+
[attr.name, attr.value, attr.namespace&.href, attr.namespace&.prefix]
|
|
176
|
+
end,
|
|
177
|
+
namespace_scope: scope,
|
|
178
|
+
)
|
|
179
|
+
node.children.each do |child|
|
|
180
|
+
built = walk_nokogiri(child,
|
|
181
|
+
preserve_whitespace: preserve_whitespace,
|
|
182
|
+
inherited_namespaces: scope)
|
|
183
|
+
element.add_child(built) if built
|
|
184
|
+
end
|
|
185
|
+
element
|
|
184
186
|
when Nokogiri::XML::Text, Nokogiri::XML::CDATA
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
TREE_BUILDER.text(node.content,
|
|
188
|
+
keep: WhitespacePolicy.keep_dom_text?(
|
|
189
|
+
node.content,
|
|
190
|
+
preserve_whitespace: preserve_whitespace,
|
|
191
|
+
element_parent: node.parent.is_a?(Nokogiri::XML::Element),
|
|
192
|
+
),
|
|
193
|
+
original: node.to_xml)
|
|
187
194
|
when Nokogiri::XML::Comment
|
|
188
|
-
|
|
195
|
+
TREE_BUILDER.comment(node.content)
|
|
189
196
|
when Nokogiri::XML::ProcessingInstruction
|
|
190
|
-
|
|
197
|
+
TREE_BUILDER.processing_instruction(node.name, node.content)
|
|
191
198
|
end
|
|
192
199
|
end
|
|
193
200
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
scope = nokogiri_namespace_scope(nokogiri_element, inherited_namespaces)
|
|
203
|
-
scope.each do |prefix, uri|
|
|
204
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
205
|
-
prefix: prefix,
|
|
206
|
-
uri: uri,
|
|
207
|
-
))
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
build_attribute_nodes(nokogiri_element, element)
|
|
211
|
-
|
|
212
|
-
nokogiri_element.children.each do |child|
|
|
213
|
-
node = build_node_from_nokogiri(child,
|
|
214
|
-
preserve_whitespace: preserve_whitespace,
|
|
215
|
-
inherited_namespaces: scope)
|
|
216
|
-
element.add_child(node) if node
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
element
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# In-scope bindings via the shared kernel; the scope is passed down
|
|
223
|
-
# the recursion — one definition fetch per element instead of an
|
|
224
|
-
# ancestor walk per element (O(n) total, not O(n * depth)).
|
|
225
|
-
def self.nokogiri_namespace_scope(nokogiri_element, inherited)
|
|
226
|
-
merge_namespace_scope(inherited,
|
|
227
|
-
nokogiri_element.namespace_definitions.map do |ns|
|
|
228
|
-
[ns.prefix, ns.href]
|
|
229
|
-
end)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def self.build_attribute_nodes(nokogiri_element, element)
|
|
233
|
-
# attribute_nodes, not the attributes hash: the hash is keyed by
|
|
234
|
-
# local name and collapses e.g. attr / a:attr / b:attr into one
|
|
235
|
-
# entry, losing namespaced attributes (W3C C14N ex 3.3).
|
|
236
|
-
nokogiri_element.attribute_nodes.each do |attr|
|
|
237
|
-
attr_node = Nodes::AttributeNode.new(
|
|
238
|
-
name: attr.name,
|
|
239
|
-
value: attr.value,
|
|
240
|
-
namespace_uri: attr.namespace&.href,
|
|
241
|
-
prefix: attr.namespace&.prefix,
|
|
242
|
-
)
|
|
243
|
-
element.add_attribute(attr_node)
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def self.build_text_node(nokogiri_text, preserve_whitespace: false)
|
|
248
|
-
content = nokogiri_text.content
|
|
249
|
-
|
|
250
|
-
unless WhitespacePolicy.keep_dom_text?(content,
|
|
251
|
-
preserve_whitespace: preserve_whitespace,
|
|
252
|
-
element_parent: nokogiri_text.parent.is_a?(Nokogiri::XML::Element))
|
|
253
|
-
return nil
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
original = nokogiri_text.to_xml
|
|
257
|
-
Nodes::TextNode.new(value: content, original: original)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def self.build_comment_node(nokogiri_comment)
|
|
261
|
-
Nodes::CommentNode.new(value: nokogiri_comment.content)
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def self.build_pi_node(nokogiri_pi)
|
|
265
|
-
Nodes::ProcessingInstructionNode.new(
|
|
266
|
-
target: nokogiri_pi.name,
|
|
267
|
-
data: nokogiri_pi.content,
|
|
268
|
-
)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
# --- Moxml path ---
|
|
201
|
+
# -- moxml record stream: post-order frames adopt children --
|
|
202
|
+
# One flattened record stream (moxml#132/#138) instead of a
|
|
203
|
+
# wrapper walk, so document conversion costs no Moxml::Node
|
|
204
|
+
# allocation per node. Own namespace declarations ride an
|
|
205
|
+
# identity-keyed stash; assign_moxml_namespace_scopes expands
|
|
206
|
+
# them to in-scope scopes afterwards (post-order arrival means
|
|
207
|
+
# scopes cannot flow down during the stream).
|
|
272
208
|
|
|
273
209
|
def self.from_moxml_xml(xml_string, preserve_whitespace:)
|
|
274
210
|
doc = Canon::XmlParsing.moxml_context.parse(xml_string, readonly: true)
|
|
@@ -301,35 +237,26 @@ inherited_namespaces: nil)
|
|
|
301
237
|
|
|
302
238
|
def self.build_from_moxml(moxml_doc, preserve_whitespace: false)
|
|
303
239
|
root = Nodes::RootNode.new
|
|
304
|
-
|
|
305
|
-
converter = ->(child) { build_moxml_node(child, preserve_whitespace: preserve_whitespace) }
|
|
306
240
|
skip_types = [Moxml::Doctype]
|
|
307
241
|
|
|
308
242
|
if moxml_doc.is_a?(Moxml::Document) && moxml_doc.root
|
|
309
243
|
element = build_moxml_subtree_from_records(moxml_doc.root,
|
|
310
244
|
preserve_whitespace: preserve_whitespace)
|
|
311
245
|
root.add_child(element) if element
|
|
312
|
-
add_document_children(root, moxml_doc.children,
|
|
313
|
-
|
|
246
|
+
TREE_BUILDER.add_document_children(root, moxml_doc.children,
|
|
247
|
+
moxml_doc.root, skip_types) do |child|
|
|
248
|
+
walk_moxml(child, preserve_whitespace: preserve_whitespace)
|
|
249
|
+
end
|
|
314
250
|
else
|
|
315
|
-
add_document_children(root, moxml_doc.children,
|
|
316
|
-
|
|
251
|
+
TREE_BUILDER.add_document_children(root, moxml_doc.children,
|
|
252
|
+
nil, skip_types) do |child|
|
|
253
|
+
walk_moxml(child, preserve_whitespace: preserve_whitespace)
|
|
254
|
+
end
|
|
317
255
|
end
|
|
318
256
|
|
|
319
257
|
root
|
|
320
258
|
end
|
|
321
259
|
|
|
322
|
-
# Document path: build the root subtree from materialize records —
|
|
323
|
-
# one flattened record stream (moxml#132/#138) instead of a wrapper
|
|
324
|
-
# walk, so conversion costs no Moxml::Node allocation per node.
|
|
325
|
-
#
|
|
326
|
-
# Records arrive post-order (children before parents): completed
|
|
327
|
-
# nodes stack up with their depth, and an element record adopts
|
|
328
|
-
# every completed node deeper than itself. Each element's OWN
|
|
329
|
-
# namespace declarations ride an identity-keyed stash; afterwards
|
|
330
|
-
# assign_moxml_namespace_scopes expands them to in-scope scopes
|
|
331
|
-
# with the same first-wins/xml-prebound semantics as the Nokogiri
|
|
332
|
-
# collector.
|
|
333
260
|
def self.build_moxml_subtree_from_records(moxml_element,
|
|
334
261
|
preserve_whitespace: false)
|
|
335
262
|
frames = []
|
|
@@ -339,8 +266,8 @@ preserve_whitespace: false)
|
|
|
339
266
|
# The record contract is root-subtree-only since moxml 0.5.11
|
|
340
267
|
# (moxml#140). Older 0.5.x releases — still allowed by canon's
|
|
341
268
|
# gemspec floor — leaked epilog document-level records at depth
|
|
342
|
-
# 0, which the document-children enumeration
|
|
343
|
-
#
|
|
269
|
+
# 0, which the document-children enumeration also covers; one
|
|
270
|
+
# integer compare per record keeps those working.
|
|
344
271
|
next if record[:depth].zero? && record[:kind] != :element
|
|
345
272
|
|
|
346
273
|
node = case record[:kind]
|
|
@@ -348,14 +275,16 @@ preserve_whitespace: false)
|
|
|
348
275
|
build_moxml_element_from_record(record, frames,
|
|
349
276
|
own_namespaces)
|
|
350
277
|
when :text, :cdata
|
|
351
|
-
|
|
278
|
+
content = record[:text].to_s
|
|
279
|
+
TREE_BUILDER.text(content,
|
|
280
|
+
keep: WhitespacePolicy.keep_dom_text?(
|
|
281
|
+
content, preserve_whitespace: preserve_whitespace
|
|
282
|
+
))
|
|
352
283
|
when :comment
|
|
353
|
-
|
|
284
|
+
TREE_BUILDER.comment(record[:text])
|
|
354
285
|
when :processing_instruction
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
data: record[:text] || "",
|
|
358
|
-
)
|
|
286
|
+
TREE_BUILDER.processing_instruction(record[:qname],
|
|
287
|
+
record[:text] || "")
|
|
359
288
|
end
|
|
360
289
|
|
|
361
290
|
frames.push([record[:depth], node]) if node
|
|
@@ -364,34 +293,18 @@ preserve_whitespace: false)
|
|
|
364
293
|
top = frames.last
|
|
365
294
|
return nil unless top
|
|
366
295
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
{ "xml" => "http://www.w3.org/XML/1998/namespace" },
|
|
370
|
-
own_namespaces)
|
|
371
|
-
element
|
|
296
|
+
assign_moxml_namespace_scopes(top[1], nil, own_namespaces)
|
|
297
|
+
top[1]
|
|
372
298
|
end
|
|
373
299
|
|
|
374
300
|
def self.build_moxml_element_from_record(record, frames, own_namespaces)
|
|
375
|
-
element =
|
|
301
|
+
element = TREE_BUILDER.element(
|
|
376
302
|
name: record[:qname],
|
|
377
|
-
namespace_uri: record[:namespace_uri],
|
|
378
303
|
prefix: record[:prefix],
|
|
304
|
+
namespace_uri: record[:namespace_uri],
|
|
305
|
+
attributes: record[:attributes],
|
|
379
306
|
)
|
|
380
307
|
|
|
381
|
-
seen = {}
|
|
382
|
-
record[:attributes].each do |name, value, namespace_uri, prefix|
|
|
383
|
-
key = [name, namespace_uri]
|
|
384
|
-
next if seen.key?(key)
|
|
385
|
-
|
|
386
|
-
seen[key] = true
|
|
387
|
-
element.add_attribute(Nodes::AttributeNode.new(
|
|
388
|
-
name: name,
|
|
389
|
-
value: value,
|
|
390
|
-
namespace_uri: namespace_uri,
|
|
391
|
-
prefix: prefix,
|
|
392
|
-
))
|
|
393
|
-
end
|
|
394
|
-
|
|
395
308
|
# Adopt completed children: they pop in reverse order; reversing
|
|
396
309
|
# once is O(n) (unshift per child would be O(n^2) on wide trees).
|
|
397
310
|
children = []
|
|
@@ -402,133 +315,54 @@ preserve_whitespace: false)
|
|
|
402
315
|
element
|
|
403
316
|
end
|
|
404
317
|
|
|
405
|
-
# Text records inside a document subtree always have an element
|
|
406
|
-
# parent (only the document element sits at depth 0), so the
|
|
407
|
-
# whitespace-only skip rule matches the wrapper path exactly.
|
|
408
|
-
def self.build_moxml_text_from_record(record, preserve_whitespace)
|
|
409
|
-
content = record[:text].to_s
|
|
410
|
-
return nil unless WhitespacePolicy.keep_dom_text?(content, preserve_whitespace: preserve_whitespace)
|
|
411
|
-
|
|
412
|
-
Nodes::TextNode.new(value: content, original: content)
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
# Expand each element's own declarations to in-scope bindings via
|
|
416
|
-
# the shared kernel. Scope flows down the canon tree — no engine
|
|
417
|
-
# calls.
|
|
418
318
|
def self.assign_moxml_namespace_scopes(element, inherited, own_namespaces)
|
|
419
|
-
scope = merge_namespace_scope(inherited,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
423
|
-
prefix: prefix,
|
|
424
|
-
uri: uri,
|
|
425
|
-
))
|
|
426
|
-
end
|
|
319
|
+
scope = TREE_BUILDER.merge_namespace_scope(inherited,
|
|
320
|
+
own_namespaces[element].to_a)
|
|
321
|
+
TREE_BUILDER.attach_namespace_scope(element, scope)
|
|
427
322
|
|
|
428
323
|
element.children.each do |child|
|
|
429
324
|
assign_moxml_namespace_scopes(child, scope, own_namespaces) if child.is_a?(Nodes::ElementNode)
|
|
430
325
|
end
|
|
431
326
|
end
|
|
432
327
|
|
|
433
|
-
|
|
328
|
+
# -- moxml wrapper walk: fragments and user-supplied nodes --
|
|
329
|
+
|
|
330
|
+
def self.walk_moxml(node, preserve_whitespace: false,
|
|
434
331
|
inherited_namespaces: nil)
|
|
435
332
|
case node
|
|
436
333
|
when Moxml::Element
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
334
|
+
scope = TREE_BUILDER.merge_namespace_scope(
|
|
335
|
+
inherited_namespaces,
|
|
336
|
+
node.namespace_definitions.map { |ns| [ns.prefix, ns.uri] },
|
|
337
|
+
)
|
|
338
|
+
element = TREE_BUILDER.element(
|
|
339
|
+
name: node.name,
|
|
340
|
+
prefix: node.namespace&.prefix,
|
|
341
|
+
namespace_uri: node.namespace&.uri,
|
|
342
|
+
attributes: node.attributes.map do |attr|
|
|
343
|
+
[attr.name, attr.value, attr.namespace&.uri, attr.namespace&.prefix]
|
|
344
|
+
end,
|
|
345
|
+
namespace_scope: scope,
|
|
346
|
+
)
|
|
347
|
+
node.children.each do |child|
|
|
348
|
+
built = walk_moxml(child,
|
|
349
|
+
preserve_whitespace: preserve_whitespace,
|
|
350
|
+
inherited_namespaces: scope)
|
|
351
|
+
element.add_child(built) if built
|
|
352
|
+
end
|
|
353
|
+
element
|
|
440
354
|
when Moxml::Text, Moxml::Cdata
|
|
441
|
-
|
|
355
|
+
TREE_BUILDER.text(node.content,
|
|
356
|
+
keep: WhitespacePolicy.keep_dom_text?(
|
|
357
|
+
node.content,
|
|
358
|
+
preserve_whitespace: preserve_whitespace,
|
|
359
|
+
element_parent: node.parent.is_a?(Moxml::Element),
|
|
360
|
+
))
|
|
442
361
|
when Moxml::Comment
|
|
443
|
-
|
|
362
|
+
TREE_BUILDER.comment(node.content)
|
|
444
363
|
when Moxml::ProcessingInstruction
|
|
445
|
-
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
def self.build_moxml_element_node(moxml_element,
|
|
450
|
-
preserve_whitespace: false,
|
|
451
|
-
inherited_namespaces: nil)
|
|
452
|
-
ns = moxml_element.namespace
|
|
453
|
-
element = Nodes::ElementNode.new(
|
|
454
|
-
name: moxml_element.name,
|
|
455
|
-
namespace_uri: ns&.uri,
|
|
456
|
-
prefix: ns&.prefix,
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
scope = moxml_namespace_scope(moxml_element, inherited_namespaces)
|
|
460
|
-
scope.each do |prefix, uri|
|
|
461
|
-
element.add_namespace(Nodes::NamespaceNode.new(
|
|
462
|
-
prefix: prefix,
|
|
463
|
-
uri: uri,
|
|
464
|
-
))
|
|
364
|
+
TREE_BUILDER.processing_instruction(node.target, node.content)
|
|
465
365
|
end
|
|
466
|
-
|
|
467
|
-
build_moxml_attribute_nodes(moxml_element, element)
|
|
468
|
-
|
|
469
|
-
moxml_element.children.each do |child|
|
|
470
|
-
node = build_moxml_node(child,
|
|
471
|
-
preserve_whitespace: preserve_whitespace,
|
|
472
|
-
inherited_namespaces: scope)
|
|
473
|
-
element.add_child(node) if node
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
element
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
# In-scope namespace bindings: the element's own declarations
|
|
480
|
-
# shadow inherited ones, xml is prebound. The scope is passed down
|
|
481
|
-
# the recursion so each element pays one declaration fetch instead
|
|
482
|
-
# of an ancestor walk through the adapter layer (the Nokogiri
|
|
483
|
-
# collector walks ancestors because those calls are cheap there).
|
|
484
|
-
def self.moxml_namespace_scope(moxml_element, inherited)
|
|
485
|
-
merge_namespace_scope(inherited,
|
|
486
|
-
moxml_element.namespace_definitions.map do |decl|
|
|
487
|
-
[decl.prefix, decl.uri]
|
|
488
|
-
end)
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
def self.build_moxml_attribute_nodes(moxml_element, element)
|
|
492
|
-
seen = {}
|
|
493
|
-
moxml_element.attributes.each do |attr|
|
|
494
|
-
# Duplicate attribute names are invalid XML; engines expose
|
|
495
|
-
# them differently (libxml2 lists repeats, libleptris dedups
|
|
496
|
-
# last-wins). Canon follows the Nokogiri presentation contract:
|
|
497
|
-
# first occurrence wins, keyed by name + namespace.
|
|
498
|
-
key = [attr.name, attr.namespace&.uri]
|
|
499
|
-
next if seen.key?(key)
|
|
500
|
-
|
|
501
|
-
seen[key] = true
|
|
502
|
-
element.add_attribute(Nodes::AttributeNode.new(
|
|
503
|
-
name: attr.name,
|
|
504
|
-
value: attr.value,
|
|
505
|
-
namespace_uri: attr.namespace&.uri,
|
|
506
|
-
prefix: attr.namespace&.prefix,
|
|
507
|
-
))
|
|
508
|
-
end
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
def self.build_moxml_text_node(moxml_text, preserve_whitespace: false)
|
|
512
|
-
content = moxml_text.content
|
|
513
|
-
|
|
514
|
-
unless WhitespacePolicy.keep_dom_text?(content,
|
|
515
|
-
preserve_whitespace: preserve_whitespace,
|
|
516
|
-
element_parent: moxml_text.parent.is_a?(Moxml::Element))
|
|
517
|
-
return nil
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
Nodes::TextNode.new(value: content, original: content)
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
def self.build_moxml_comment_node(moxml_comment)
|
|
524
|
-
Nodes::CommentNode.new(value: moxml_comment.content)
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
def self.build_moxml_pi_node(moxml_pi)
|
|
528
|
-
Nodes::ProcessingInstructionNode.new(
|
|
529
|
-
target: moxml_pi.target,
|
|
530
|
-
data: moxml_pi.content,
|
|
531
|
-
)
|
|
532
366
|
end
|
|
533
367
|
end
|
|
534
368
|
end
|
|
@@ -200,7 +200,8 @@ strip_doctype: false)
|
|
|
200
200
|
|
|
201
201
|
# Skip whitespace-only text nodes unless preserving, per the SAX
|
|
202
202
|
# policy (CR-bearing content always survives for C14N;
|
|
203
|
-
# non-ASCII whitespace is meaningful content
|
|
203
|
+
# non-ASCII whitespace is meaningful content; document-level
|
|
204
|
+
# whitespace drops with every policy).
|
|
204
205
|
return unless WhitespacePolicy.keep_sax_text?(decoded_string,
|
|
205
206
|
preserve_whitespace: @preserve_whitespace,
|
|
206
207
|
element_parent: parent.node_type == :element)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
module Xml
|
|
5
|
+
# The one place canon tree nodes are constructed from parsed XML.
|
|
6
|
+
#
|
|
7
|
+
# Owns construction semantics: namespace scopes, attribute
|
|
8
|
+
# normalization, node kinds, and document-level ordering. Keep/strip
|
|
9
|
+
# decisions come from WhitespacePolicy (one home for all three
|
|
10
|
+
# policies); engine walks — the Nokogiri and moxml extractors in
|
|
11
|
+
# Xml::DataModel, the HTML walk in Html::DataModel — only map their
|
|
12
|
+
# engine's shapes onto this interface.
|
|
13
|
+
#
|
|
14
|
+
# Attribute normalization: duplicate (name, namespace) pairs are
|
|
15
|
+
# invalid XML; engines expose them differently (libxml2 lists
|
|
16
|
+
# repeats, libleptris deduplicates), so the builder resolves them
|
|
17
|
+
# once — first occurrence wins.
|
|
18
|
+
class TreeBuilder
|
|
19
|
+
NO_ATTRIBUTES = [].freeze
|
|
20
|
+
XML_NAMESPACE_PREFIX = "xml"
|
|
21
|
+
XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"
|
|
22
|
+
|
|
23
|
+
# In-scope namespace bindings: the element's own declarations
|
|
24
|
+
# shadow inherited ones; xml is prebound at the base. Declaration
|
|
25
|
+
# pairs are [prefix-or-nil, uri]; nil and "" both mean the default
|
|
26
|
+
# namespace.
|
|
27
|
+
def merge_namespace_scope(inherited, declaration_pairs)
|
|
28
|
+
scope = inherited ? inherited.dup : { XML_NAMESPACE_PREFIX => XML_NAMESPACE_URI }
|
|
29
|
+
declaration_pairs.each do |prefix, uri|
|
|
30
|
+
scope[prefix || ""] = uri
|
|
31
|
+
end
|
|
32
|
+
scope
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Attach an in-scope scope to an element as namespace nodes.
|
|
36
|
+
def attach_namespace_scope(element, scope)
|
|
37
|
+
scope.each do |prefix, uri|
|
|
38
|
+
element.add_namespace(Nodes::NamespaceNode.new(
|
|
39
|
+
prefix: prefix,
|
|
40
|
+
uri: uri,
|
|
41
|
+
))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Build an element. `attributes` are [name, value, namespace_uri,
|
|
46
|
+
# prefix] pairs; `namespace_scope` is a merged scope (or nil for
|
|
47
|
+
# no namespace nodes).
|
|
48
|
+
def element(name:, prefix: nil, namespace_uri: nil,
|
|
49
|
+
attributes: NO_ATTRIBUTES, namespace_scope: nil)
|
|
50
|
+
element = Nodes::ElementNode.new(
|
|
51
|
+
name: name,
|
|
52
|
+
namespace_uri: namespace_uri,
|
|
53
|
+
prefix: prefix,
|
|
54
|
+
)
|
|
55
|
+
attach_namespace_scope(element, namespace_scope) if namespace_scope
|
|
56
|
+
|
|
57
|
+
seen = {}
|
|
58
|
+
attributes.each do |attr_name, value, attr_namespace_uri, attr_prefix|
|
|
59
|
+
key = [attr_name, attr_namespace_uri]
|
|
60
|
+
next if seen.key?(key)
|
|
61
|
+
|
|
62
|
+
seen[key] = true
|
|
63
|
+
element.add_attribute(Nodes::AttributeNode.new(
|
|
64
|
+
name: attr_name,
|
|
65
|
+
value: value,
|
|
66
|
+
namespace_uri: attr_namespace_uri,
|
|
67
|
+
prefix: attr_prefix,
|
|
68
|
+
))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
element
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Build a text node; `keep` comes from WhitespacePolicy (the
|
|
75
|
+
# caller knows the policy and parent context).
|
|
76
|
+
def text(content, keep:, original: content)
|
|
77
|
+
return nil unless keep
|
|
78
|
+
|
|
79
|
+
Nodes::TextNode.new(value: content, original: original)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def comment(content)
|
|
83
|
+
Nodes::CommentNode.new(value: content)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def processing_instruction(target, data)
|
|
87
|
+
Nodes::ProcessingInstructionNode.new(target: target, data: data)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Attach document-level children (prolog/epilog PIs, comments,
|
|
91
|
+
# document-level text) to the canon root, in document order,
|
|
92
|
+
# skipping the document element and the given types. Yields each
|
|
93
|
+
# child to the caller's converter; nil results are dropped.
|
|
94
|
+
def add_document_children(root, children, document_element,
|
|
95
|
+
skip_types = [])
|
|
96
|
+
children.each do |child|
|
|
97
|
+
next if child.equal?(document_element)
|
|
98
|
+
next if skip_types.any? { |type| child.is_a?(type) }
|
|
99
|
+
|
|
100
|
+
node = yield child
|
|
101
|
+
root.add_child(node) if node
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -4,8 +4,15 @@ module Canon
|
|
|
4
4
|
module Xml
|
|
5
5
|
# Parse-time whitespace policy: whether a character-data node
|
|
6
6
|
# survives conversion. One home for the keep/strip rules so the
|
|
7
|
-
# DOM/SAX differences are visible here instead of implied by
|
|
7
|
+
# DOM/SAX/HTML differences are visible here instead of implied by
|
|
8
8
|
# copy-paste across conversion sites.
|
|
9
|
+
#
|
|
10
|
+
# Document-level character data (outside the root element) can only
|
|
11
|
+
# be whitespace per the XML grammar, and the XPath data model —
|
|
12
|
+
# which canon's tree and C14N follow — has no root text children at
|
|
13
|
+
# all. Every policy therefore drops whitespace-only document-level
|
|
14
|
+
# text; engines that report it (libxml2 does not, libleptris 1.9.38+
|
|
15
|
+
# does) stay byte-compatible through here.
|
|
9
16
|
module WhitespacePolicy
|
|
10
17
|
module_function
|
|
11
18
|
|
|
@@ -15,9 +22,14 @@ module Canon
|
|
|
15
22
|
#
|
|
16
23
|
# NOTE: CR-only nodes are dropped on this path; the SAX rule keeps
|
|
17
24
|
# them (character references must survive for C14N).
|
|
18
|
-
def keep_dom_text?(content, preserve_whitespace:,
|
|
25
|
+
def keep_dom_text?(content, preserve_whitespace:,
|
|
26
|
+
element_parent: true)
|
|
27
|
+
# The XPath data model has no root text children — drop
|
|
28
|
+
# whitespace-only document-level text even when preserving
|
|
29
|
+
# (engines that report it stay byte-compatible with libxml2).
|
|
30
|
+
return false if !element_parent && content.strip.empty?
|
|
31
|
+
|
|
19
32
|
return true if preserve_whitespace
|
|
20
|
-
return true unless element_parent
|
|
21
33
|
|
|
22
34
|
!content.strip.empty?
|
|
23
35
|
end
|
|
@@ -25,13 +37,33 @@ module Canon
|
|
|
25
37
|
# SAX rule: same shape, plus CR-bearing content is always kept
|
|
26
38
|
# (
 must survive parsing for C14N) — only runs of pure ASCII
|
|
27
39
|
# whitespace (space, tab, CR, LF) are dropped when not preserving.
|
|
28
|
-
def keep_sax_text?(content, preserve_whitespace:,
|
|
40
|
+
def keep_sax_text?(content, preserve_whitespace:,
|
|
41
|
+
element_parent: true)
|
|
42
|
+
return false if !element_parent && content.strip.empty?
|
|
43
|
+
|
|
29
44
|
return true if preserve_whitespace
|
|
30
|
-
return true unless element_parent
|
|
31
45
|
return true if content.include?("\r")
|
|
32
46
|
|
|
33
47
|
!content.gsub(/[ \t\r\n]/, "").empty?
|
|
34
48
|
end
|
|
49
|
+
|
|
50
|
+
# HTML conversion rule: whitespace-only text is dropped except in
|
|
51
|
+
# whitespace-sensitive elements (pre/code/textarea/script/style),
|
|
52
|
+
# between inline siblings (semantically significant), and when it
|
|
53
|
+
# carries NBSP (U+00A0 — never insignificant; strip is ASCII-only
|
|
54
|
+
# so it is checked explicitly).
|
|
55
|
+
HTML_WHITESPACE_SENSITIVE_TAGS = %w[pre code textarea script style].freeze
|
|
56
|
+
|
|
57
|
+
def keep_html_text?(content, parent_name:, inline_significant: false)
|
|
58
|
+
return true unless content.strip.empty?
|
|
59
|
+
return true if content.include?(" ")
|
|
60
|
+
|
|
61
|
+
parent_name = parent_name.to_s.downcase
|
|
62
|
+
return true if HTML_WHITESPACE_SENSITIVE_TAGS.include?(parent_name)
|
|
63
|
+
return true if inline_significant
|
|
64
|
+
|
|
65
|
+
false
|
|
66
|
+
end
|
|
35
67
|
end
|
|
36
68
|
end
|
|
37
69
|
end
|
data/lib/canon/xml.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Canon
|
|
|
27
27
|
autoload :Processor, "canon/xml/processor"
|
|
28
28
|
autoload :Sax, "canon/xml/sax"
|
|
29
29
|
autoload :SaxBuilder, "canon/xml/sax_builder"
|
|
30
|
+
autoload :TreeBuilder, "canon/xml/tree_builder"
|
|
30
31
|
autoload :WhitespaceNormalizer, "canon/xml/whitespace_normalizer"
|
|
31
32
|
autoload :WhitespacePolicy, "canon/xml/whitespace_policy"
|
|
32
33
|
autoload :XmlBaseHandler, "canon/xml/xml_base_handler"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: canon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -400,6 +400,7 @@ files:
|
|
|
400
400
|
- lib/canon/xml/sax/moxml_driver.rb
|
|
401
401
|
- lib/canon/xml/sax/nokogiri_driver.rb
|
|
402
402
|
- lib/canon/xml/sax_builder.rb
|
|
403
|
+
- lib/canon/xml/tree_builder.rb
|
|
403
404
|
- lib/canon/xml/whitespace_normalizer.rb
|
|
404
405
|
- lib/canon/xml/whitespace_policy.rb
|
|
405
406
|
- lib/canon/xml/xml_base_handler.rb
|