xml-mixup 0.1.0
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 +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/README.md +191 -0
- data/Rakefile +6 -0
- data/lib/xml/mixup/version.rb +5 -0
- data/lib/xml/mixup.rb +505 -0
- data/lib/xml-mixup.rb +3 -0
- data/xml-mixup.gemspec +38 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b5d422e3cab772172b48772aa775c71c7362f3459c4315b28a48180832160dca
|
4
|
+
data.tar.gz: 12d50f74beb943f993fbd52b89913e9696c9240893fb14bcca81a434bc569d0c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1896aa7cec980baf9779ca87aaad386de815879406f24ca04cc92d9a5b00b995e4a785edbd7d625b4954435f3b248b69f997f429eb55d76d1dbef4538f38f0da
|
7
|
+
data.tar.gz: 1648c19729fcd0a56504c046d11e4c2def65c2cc287ee371fbb24e837c8fe499991ad763c51ec9abaa060d3e34718c8db69d74bb16c3299c6da8c5266532eee0
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
# XML::Mixup: A mixin for XML markup
|
2
|
+
|
3
|
+
```ruby
|
4
|
+
require 'xml-mixup'
|
5
|
+
|
6
|
+
class Anything
|
7
|
+
include XML::Mixup
|
8
|
+
end
|
9
|
+
|
10
|
+
something = Anything.new
|
11
|
+
|
12
|
+
# generate a structure
|
13
|
+
node = something.markup spec: [
|
14
|
+
{ '#pi' => 'xml-stylesheet', type: 'text/xsl', href: '/transform' },
|
15
|
+
{ '#dtd' => :html },
|
16
|
+
{ '#html' => [
|
17
|
+
{ '#head' => [
|
18
|
+
{ '#title' => 'look ma, title' },
|
19
|
+
{ '#elem' => :base, href: 'http://the.base/url' },
|
20
|
+
] },
|
21
|
+
{ '#body' => [
|
22
|
+
{ '#h1' => 'Illustrious Heading' },
|
23
|
+
{ '#p' => :lolwut },
|
24
|
+
] },
|
25
|
+
], xmlns: 'http://www.w3.org/1999/xhtml' }
|
26
|
+
]
|
27
|
+
|
28
|
+
# `node` will correspond to the last thing generated. In this
|
29
|
+
# case, it will be a text node containing 'lolwut'.
|
30
|
+
|
31
|
+
doc = node.document
|
32
|
+
puts doc.to_xml
|
33
|
+
```
|
34
|
+
|
35
|
+
## Yet another XML markup generator?
|
36
|
+
|
37
|
+
Some time ago, [I](https://doriantaylor.com/) wrote a Perl module called
|
38
|
+
[Role::Markup::XML](https://metacpan.org/pod/Role::Markup::XML). I did this
|
39
|
+
because I had a lot of XML to generate, and was dissatisfied with what
|
40
|
+
was currently on offer. Now I have a lot of XML to generate using
|
41
|
+
Ruby, and found a lot of the same things:
|
42
|
+
|
43
|
+
### Structure is generated by procedure calls
|
44
|
+
|
45
|
+
Granted it's a lot nicer to do this sort of thing in Ruby, but at the
|
46
|
+
end of the day, the thing generating the XML is a nested list of
|
47
|
+
method calls---_not_ a declarative data structure.
|
48
|
+
|
49
|
+
### Document has to be generated all in one shot
|
50
|
+
|
51
|
+
It's not super-easy to generate a piece of the target document and
|
52
|
+
then go back and generate some more (although
|
53
|
+
{Nokogiri::XML::Builder.with} is a nice start). This plus the last
|
54
|
+
point leads to all sorts of cockamamy constructs which are almost as
|
55
|
+
life-sucking as writing raw DOM routines.
|
56
|
+
|
57
|
+
### Hard to do surgery on existing documents
|
58
|
+
|
59
|
+
This comes up a lot: you have an existing document and you want to add
|
60
|
+
even just a single node to it---say, in between two nodes just for
|
61
|
+
fun. Good luck with that.
|
62
|
+
|
63
|
+
### Enter `XML::Mixup`
|
64
|
+
|
65
|
+
* __The input consists of ordinary Ruby data objects__ so you can
|
66
|
+
build them up ahead of time, in bulk, transform them, etc.,
|
67
|
+
* __Sprinkle pre-built XML subtrees anywhere into the spec__ so you
|
68
|
+
can memoize repeating elements, or otherwise compile a document
|
69
|
+
incrementally,
|
70
|
+
* __Attach new generated content anywhere:__ underneath a parent node,
|
71
|
+
or before, after, or _instead of_ a node at the sibling level.
|
72
|
+
|
73
|
+
## The spec format
|
74
|
+
|
75
|
+
At the heart of this module is a single method called `markup`, which,
|
76
|
+
among other things, takes a `:spec`. The spec can be any composite
|
77
|
+
|
78
|
+
### Hashes
|
79
|
+
|
80
|
+
The principal construct in `XML::Mixup` is the {Hash}. You can
|
81
|
+
generate pretty much any node with it:
|
82
|
+
|
83
|
+
#### Elements
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
{ '#tag' => 'foo' } # => <foo/>
|
87
|
+
|
88
|
+
# or
|
89
|
+
{ '#elem' => 'foo' } # => <foo/>
|
90
|
+
|
91
|
+
# or, with the element name as a symbol
|
92
|
+
{ '#element' => :foo } # => <foo/>
|
93
|
+
|
94
|
+
# or, with nil as a key
|
95
|
+
{ nil => :foo } # => <foo/>
|
96
|
+
|
97
|
+
# or, with attributes
|
98
|
+
{ nil => :foo, bar: :hi } # => <foo bar="hi"/>
|
99
|
+
|
100
|
+
# or, with namespaces
|
101
|
+
{ nil => :foo, xmlns: 'urn:x-bar' } # => <foo xmlns="urn:x-bar"/>
|
102
|
+
|
103
|
+
# or, with more namespaces
|
104
|
+
{ nil => :foo, xmlns: 'urn:x-bar', 'xmlns:hurr' => 'urn:x-durr' }
|
105
|
+
# => <foo xmlns="urn:x-bar" xmlns:hurr="urn:x-durr"/>
|
106
|
+
|
107
|
+
# or, with content
|
108
|
+
{ nil => [:foo, :hi] } # => <foo>hi</foo>
|
109
|
+
|
110
|
+
# or, shove your child nodes into an otherwise content-less key
|
111
|
+
{ [:hi] => :foo, bar: :hurr } # => <foo bar="hurr">hi</foo>
|
112
|
+
```
|
113
|
+
|
114
|
+
Attributes are sorted lexically. Composite attribute values get
|
115
|
+
flattened like this:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
{ nil => :foo, array: [:a :b], hash: { e: :f, c: :d } }
|
119
|
+
# => <foo array="a b" hash="c: d e: f"/>
|
120
|
+
```
|
121
|
+
|
122
|
+
#### Processing instructions
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
{ '#pi' => 'xml-stylesheet', type: 'text/xsl', href: '/transform' }
|
126
|
+
# => <?xml-stylesheet type="text/xsl" href="/transform"?>
|
127
|
+
|
128
|
+
# or, if you like typing
|
129
|
+
{ '#processing-instruction' => :hurr } # => <?hurr?>
|
130
|
+
```
|
131
|
+
|
132
|
+
#### `DOCTYPE` declarations
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
{ '#dtd' => :html } # => <!DOCTYPE html>
|
136
|
+
|
137
|
+
# or (note either :public or :system can be nil)
|
138
|
+
{ '#dtd' => [:html, :public, :system] }
|
139
|
+
# => <!DOCTYPE html PUBLIC "public" SYSTEM "system">
|
140
|
+
|
141
|
+
# or, same thing
|
142
|
+
{ '#doctype' => :html, public: :public, system: :system }
|
143
|
+
```
|
144
|
+
|
145
|
+
#### Comments and `CDATA` sections
|
146
|
+
|
147
|
+
Comments and `CDATA` are flattened into string literals:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
{ '#comment' => :whatever } # => <!-- whatever -->
|
151
|
+
|
152
|
+
{ '#cdata' => '<what-everrr>' } # => <![CDATA[<what-everrr>]]>
|
153
|
+
```
|
154
|
+
|
155
|
+
Pretty straight forward?
|
156
|
+
|
157
|
+
### Arrays
|
158
|
+
|
159
|
+
Parts of a spec that are arrays (or really anything that can be turned
|
160
|
+
into one) are attached at the same level of the document in the
|
161
|
+
sequence given, as you might expect.
|
162
|
+
|
163
|
+
### `Nokogiri::XML::Node` objects
|
164
|
+
|
165
|
+
These are automatically cloned, but otherwise passed in as-is.
|
166
|
+
|
167
|
+
### `Proc`s, lambdas etc.
|
168
|
+
|
169
|
+
These are executed with any supplied `:args`, and then `markup` is run
|
170
|
+
again over the result. (Take care not to supply a `Proc` that produces
|
171
|
+
another `Proc`.)
|
172
|
+
|
173
|
+
### Everything else
|
174
|
+
|
175
|
+
Turned into a text node.
|
176
|
+
|
177
|
+
## Installation
|
178
|
+
|
179
|
+
Come on, you know how to do this:
|
180
|
+
|
181
|
+
$ gem install xml-mixup
|
182
|
+
|
183
|
+
## Contributing
|
184
|
+
|
185
|
+
Bug reports and pull requests are welcome at
|
186
|
+
https://github.com/doriantaylor/rb-xml-mixup.
|
187
|
+
|
188
|
+
## License
|
189
|
+
|
190
|
+
This software is provided under
|
191
|
+
the [Apache License, 2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
data/Rakefile
ADDED
data/lib/xml/mixup.rb
ADDED
@@ -0,0 +1,505 @@
|
|
1
|
+
require 'xml/mixup/version'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module XML::Mixup
|
5
|
+
|
6
|
+
#
|
7
|
+
|
8
|
+
# these are node attachment protocols
|
9
|
+
private
|
10
|
+
|
11
|
+
ADJACENT = {
|
12
|
+
parent: lambda do |node, parent|
|
13
|
+
if parent.node_type == 9 and node.node_type == 1
|
14
|
+
parent.root = node
|
15
|
+
elsif node.node_type == 11
|
16
|
+
node.children.each do |child|
|
17
|
+
parent.add_child(child)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
parent.add_child(node)
|
21
|
+
end
|
22
|
+
end,
|
23
|
+
before: lambda do |node, sibling|
|
24
|
+
sibling.add_previous_sibling node
|
25
|
+
end,
|
26
|
+
after: lambda { |node, sibling| sibling.add_next_sibling node },
|
27
|
+
replace: lambda { |node, target| target.replace node },
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
RESERVED = %w{comment cdata doctype dtd elem element
|
31
|
+
pi processing-instruction tag}.map {|x| "##{x}"}.to_set.freeze
|
32
|
+
|
33
|
+
public
|
34
|
+
|
35
|
+
# Generate a handy blank document.
|
36
|
+
#
|
37
|
+
# @param version [Numeric, nil]
|
38
|
+
#
|
39
|
+
# @return [Nokogiri::XML::Document] a Nokogiri XML document.
|
40
|
+
|
41
|
+
def xml_doc version = nil
|
42
|
+
Nokogiri::XML::Document.new version
|
43
|
+
end
|
44
|
+
|
45
|
+
# Generates an XML tree from a given specification.
|
46
|
+
#
|
47
|
+
# require 'xml-mixup'
|
48
|
+
#
|
49
|
+
# class Anything
|
50
|
+
# include XML::Mixup
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# something = Anything.new
|
54
|
+
#
|
55
|
+
# # generate a structure
|
56
|
+
# node = something.markup spec: [
|
57
|
+
# { '#pi' => 'xml-stylesheet', type: 'text/xsl', href: '/transform' },
|
58
|
+
# { '#dtd' => :html },
|
59
|
+
# { '#html' => [
|
60
|
+
# { '#head' => [
|
61
|
+
# { '#title' => 'look ma, title' },
|
62
|
+
# { '#elem' => :base, href: 'http://the.base/url' },
|
63
|
+
# ] },
|
64
|
+
# { '#body' => [
|
65
|
+
# { '#h1' => 'Illustrious Heading' },
|
66
|
+
# { '#p' => :lolwut },
|
67
|
+
# ] },
|
68
|
+
# ], xmlns: 'http://www.w3.org/1999/xhtml' }
|
69
|
+
# ]
|
70
|
+
#
|
71
|
+
# # `node` will correspond to the last thing generated. In this
|
72
|
+
# # case, it will be a text node containing 'lolwut'.
|
73
|
+
#
|
74
|
+
# doc = node.document
|
75
|
+
# puts doc.to_xml
|
76
|
+
#
|
77
|
+
# @param spec [Hash, Array, Nokogiri::XML::Node, Proc, #to_s] An XML
|
78
|
+
# tree specification. May be composed of multiple hashes and
|
79
|
+
# arrays. See the spec spec.
|
80
|
+
#
|
81
|
+
# @param doc [Nokogiri::XML::Document, nil] an optional XML document
|
82
|
+
# instance; will be supplied if none given.
|
83
|
+
#
|
84
|
+
# @param args [#to_a] Any arguments to be passed to any callbacks
|
85
|
+
# anywhere in the spec. Assumed to be an array.
|
86
|
+
#
|
87
|
+
# @param parent [Nokogiri::XML::Node] The node under which the
|
88
|
+
# evaluation result of the spec is to be attached. This is the
|
89
|
+
# default adjacent node, which in turn defaults to the document if
|
90
|
+
# it or no other adjacent node is given. Conflicts with other
|
91
|
+
# adjacent nodes.
|
92
|
+
#
|
93
|
+
# @param before [Nokogiri::XML::Node] This represents a _sibling_
|
94
|
+
# node which the spec is to be inserted _before_. Conflicts with
|
95
|
+
# other adjacent nodes.
|
96
|
+
#
|
97
|
+
# @param after [Nokogiri::XML::Node] This represents a _sibling_
|
98
|
+
# node which the spec is to be inserted _after_. Conflicts with
|
99
|
+
# other adjacent nodes.
|
100
|
+
#
|
101
|
+
# @param replace [Nokogiri::XML::Node] This represents a _sibling_
|
102
|
+
# node which the spec is intended to _replace_. Conflicts with
|
103
|
+
# other adjacent nodes.
|
104
|
+
#
|
105
|
+
# @return [Nokogiri::XML::Node] the last node generated, in document
|
106
|
+
# order. Will return a {Nokogiri::XML::Document} when called
|
107
|
+
# without arguments.
|
108
|
+
|
109
|
+
def markup spec: nil, doc: nil, args: [], **nodes
|
110
|
+
# handle adjacent node declaration
|
111
|
+
adj = nil
|
112
|
+
ADJACENT.keys do |k|
|
113
|
+
if nodes[k]
|
114
|
+
if adj
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
unless nodes[k].is_a? Nokogiri::XML::Node
|
118
|
+
raise
|
119
|
+
end
|
120
|
+
adj = k
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# generate doc/parent
|
125
|
+
if adj
|
126
|
+
doc ||= nodes[adj].document
|
127
|
+
unless adj == 'parent'
|
128
|
+
unless (nodes[:parent] = nodes[adj].parent)
|
129
|
+
raise
|
130
|
+
end
|
131
|
+
end
|
132
|
+
else
|
133
|
+
doc ||= Nokogiri::XML::Document.new
|
134
|
+
nodes[adj = :parent] ||= doc
|
135
|
+
end
|
136
|
+
|
137
|
+
node = nodes[adj]
|
138
|
+
|
139
|
+
# dispatch based on spec type
|
140
|
+
if spec and not (spec.respond_to? :empty? and spec.empty?)
|
141
|
+
if spec.is_a? Array
|
142
|
+
par = adj == :parent ? nodes[:parent] : doc.fragment
|
143
|
+
out = spec.map do |x|
|
144
|
+
markup(spec: x, parent: par, pseudo: nodes[:parent], doc: doc,
|
145
|
+
args: nodes[:args])
|
146
|
+
end
|
147
|
+
|
148
|
+
# only run this if there is something to run
|
149
|
+
if out.length > 0
|
150
|
+
# this is already attached if the adjacent node is the parent
|
151
|
+
ADJACENT[adj].call(par, nodes[adj]) unless adj == :parent
|
152
|
+
node = out.last
|
153
|
+
end
|
154
|
+
# node otherwise defaults to adjacent
|
155
|
+
|
156
|
+
elsif spec.respond_to? :call
|
157
|
+
# handle proc/lambda/whatever
|
158
|
+
node = markup(spec: spec.call(*args), args: args,
|
159
|
+
doc: doc, adj => nodes[adj])
|
160
|
+
elsif spec.is_a? Hash
|
161
|
+
# maybe element, maybe something else
|
162
|
+
|
163
|
+
# find the nil key which should contain a viable node name
|
164
|
+
# (and maybe children)
|
165
|
+
name = nil
|
166
|
+
children = []
|
167
|
+
if x = spec[nil]
|
168
|
+
if x.respond_to? :to_a
|
169
|
+
x = x.to_a
|
170
|
+
name = x.shift
|
171
|
+
children = x
|
172
|
+
else
|
173
|
+
name = x
|
174
|
+
end
|
175
|
+
elsif (compact = spec.select { |k, _|
|
176
|
+
k.respond_to?(:to_a) or k.is_a?(Nokogiri::XML::Node)}) and
|
177
|
+
not compact.empty?
|
178
|
+
# compact syntax eliminates the `nil` key
|
179
|
+
raise %q{Spec can't have duplicate compact keys} if compact.count > 1
|
180
|
+
children, name = compact.first
|
181
|
+
children = children.respond_to?(:to_a) ? children.to_a : [children]
|
182
|
+
elsif (special = spec.select { |k, _|
|
183
|
+
k.respond_to? :to_s and k.to_s.start_with? '#' }) and
|
184
|
+
not special.empty?
|
185
|
+
# these are special keys
|
186
|
+
raise %q{Spec can't have multiple special keys} if special.count > 1
|
187
|
+
name, children = special.first
|
188
|
+
|
189
|
+
if %w{# #elem #element #tag}.any? name
|
190
|
+
# then the name is in the `children` slot
|
191
|
+
raise "Value of #{name} shorthand formulation" +
|
192
|
+
"must be a valid element name" unless children.to_s
|
193
|
+
name = children
|
194
|
+
# set children to empty array
|
195
|
+
children = []
|
196
|
+
elsif not RESERVED.any? name
|
197
|
+
# then the name is encoded into the key and we have to
|
198
|
+
# remove the octothorpe
|
199
|
+
name = name[1..name.length]
|
200
|
+
end
|
201
|
+
|
202
|
+
# don't forget to reset the child nodes
|
203
|
+
children = children.respond_to?(:to_a) ? children.to_a : [children]
|
204
|
+
end
|
205
|
+
|
206
|
+
# note the name can be nil because it can be inferred
|
207
|
+
|
208
|
+
# now we pull out "attributes" which are the rest of the keys;
|
209
|
+
# these should be amenable to being turned into symbols
|
210
|
+
attr = spec.select { |k, _|
|
211
|
+
k and k.respond_to? :to_sym and not k.to_s.start_with? '#'
|
212
|
+
}.transform_keys(&:to_sym)
|
213
|
+
|
214
|
+
# now we dispatch based on the name
|
215
|
+
if name == '#comment'
|
216
|
+
# first up, comments
|
217
|
+
node = doc.create_comment flatten(children, args)
|
218
|
+
|
219
|
+
# attach it
|
220
|
+
ADJACENT[adj].call node, nodes[adj]
|
221
|
+
|
222
|
+
elsif name == '#pi' or name == '#processing-instruction'
|
223
|
+
# now processing instructions
|
224
|
+
if children.empty?
|
225
|
+
raise
|
226
|
+
end
|
227
|
+
target = children[0]
|
228
|
+
content = ''
|
229
|
+
if (c = children[1..children.length]) and c.length > 0
|
230
|
+
#warn c.inspect
|
231
|
+
content = flatten(c, args)
|
232
|
+
else
|
233
|
+
content = attr.sort.map { |pair|
|
234
|
+
"#{pair[0].to_s}=\"#{flatten(pair[1], args)}\""
|
235
|
+
}.join(' ')
|
236
|
+
end
|
237
|
+
|
238
|
+
node = Nokogiri::XML::ProcessingInstruction.new(doc, target, content)
|
239
|
+
|
240
|
+
#warn node.inspect, content
|
241
|
+
|
242
|
+
# attach it
|
243
|
+
ADJACENT[adj].call node, nodes[adj]
|
244
|
+
|
245
|
+
elsif name == '#dtd' or name == '#doctype'
|
246
|
+
# now doctype declarations
|
247
|
+
if children.empty?
|
248
|
+
raise
|
249
|
+
end
|
250
|
+
|
251
|
+
# assign as if these are args
|
252
|
+
root, pub, sys = children
|
253
|
+
# supplant with attributes if present
|
254
|
+
pub ||= attr[:public] if attr[:public]
|
255
|
+
sys ||= attr[:system] if attr[:system]
|
256
|
+
|
257
|
+
# XXX for some reason this is an *internal* subset?
|
258
|
+
node = doc.create_internal_subset(root, pub, sys)
|
259
|
+
|
260
|
+
# at any rate it doesn't have to be explicitly attached
|
261
|
+
|
262
|
+
# attach it to the document
|
263
|
+
#doc.add_child node
|
264
|
+
|
265
|
+
# attach it (?)
|
266
|
+
#ADJACENT[adj].call node, nodes[adj]
|
267
|
+
elsif name == '#cdata'
|
268
|
+
# let's not forget cdata sections
|
269
|
+
node = doc.create_cdata flatten(children, args)
|
270
|
+
# attach it
|
271
|
+
ADJACENT[adj].call node, nodes[adj]
|
272
|
+
|
273
|
+
else
|
274
|
+
# finally, an element
|
275
|
+
|
276
|
+
raise 'Element name inference NOT IMPLEMENTED' unless name
|
277
|
+
|
278
|
+
# first check the name
|
279
|
+
prefix = local = nil
|
280
|
+
if name and (md = /^(?:([^:]+):)?(.+)/.match(name.to_s))
|
281
|
+
# XXX match actual qname/ncname here
|
282
|
+
prefix, local = md.captures
|
283
|
+
end
|
284
|
+
|
285
|
+
# next pull apart the namespaces and ordinary attributes
|
286
|
+
ns = {}
|
287
|
+
at = {}
|
288
|
+
attr.each do |k, v|
|
289
|
+
v = flatten(v, args)
|
290
|
+
if (md = /^xmlns(?::(.*))?$/i.match(k.to_s))
|
291
|
+
ns[md[1]] = v
|
292
|
+
else
|
293
|
+
at[k.to_s] = v
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# now go over the attributes and set any missing namespaces to nil
|
298
|
+
at.keys.each do |k|
|
299
|
+
p, _ = /^(?:([^:]+):)?(.+)$/.match(k).captures
|
300
|
+
ns[p] ||= nil
|
301
|
+
end
|
302
|
+
# also do the tag prefix but only if there is a local name
|
303
|
+
ns[prefix] ||= nil if local
|
304
|
+
|
305
|
+
# pseudo is a stand-in for non-parent adjacent nodes
|
306
|
+
pseudo = nodes[:pseudo] || nodes[:parent]
|
307
|
+
|
308
|
+
# now get the final namespace mapping
|
309
|
+
ns.keys.each do |k|
|
310
|
+
pk = k ? "xmlns:#{k}" : "xmlns"
|
311
|
+
if pseudo.namespaces.has_key? pk
|
312
|
+
ns[k] ||= pseudo.namespaces[pk]
|
313
|
+
end
|
314
|
+
end
|
315
|
+
# delete nil => nil
|
316
|
+
if ns.has_key? nil and ns[nil].nil?
|
317
|
+
ns.delete(nil)
|
318
|
+
end
|
319
|
+
|
320
|
+
# there should be no nil namespace declarations now
|
321
|
+
if ns.has_value? nil
|
322
|
+
raise
|
323
|
+
end
|
324
|
+
|
325
|
+
# generate the node
|
326
|
+
node = element name, doc: doc, ns: ns, attr: at, args: args
|
327
|
+
|
328
|
+
# attach it
|
329
|
+
ADJACENT[adj].call node, nodes[adj]
|
330
|
+
|
331
|
+
# don't forget the children!
|
332
|
+
if children.length > 0
|
333
|
+
#warn node.inspect, children.inspect
|
334
|
+
node = markup(spec: children, doc: doc, parent: node, args: args)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
else
|
338
|
+
if spec.is_a? Nokogiri::XML::Node
|
339
|
+
# existing node
|
340
|
+
node = spec.dup
|
341
|
+
else
|
342
|
+
# text node
|
343
|
+
node = doc.create_text_node spec.to_s
|
344
|
+
end
|
345
|
+
|
346
|
+
# attach it
|
347
|
+
ADJACENT[adj].call node, nodes[adj]
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# return the node
|
352
|
+
node
|
353
|
+
end
|
354
|
+
|
355
|
+
# Generates an XHTML stub, with optional RDFa attributes. All
|
356
|
+
# parameters are optional.
|
357
|
+
#
|
358
|
+
# @param doc [Nokogiri::XML::Document, nil] an optional document.
|
359
|
+
#
|
360
|
+
# @param base [#to_s] the contents of +<base href=""/>+.
|
361
|
+
#
|
362
|
+
# @param prefix [Hash] the contents of the root node's +prefix=+
|
363
|
+
# and +xmlns:*+ attributes.
|
364
|
+
#
|
365
|
+
# @param vocab [#to_s] the contents of the root node's +vocab=+.
|
366
|
+
#
|
367
|
+
# @param lang [#to_s] the contents of +lang=+ and when applicable, +xml:lang+.
|
368
|
+
#
|
369
|
+
# @param title [#to_s, #to_a, Hash] the contents of the +<title>+
|
370
|
+
# tag. When given as an array-like object, all elements after the
|
371
|
+
# first one will be flattened to a single string and inserted into
|
372
|
+
# the +property=+ attribute. When given as a {Hash}, it will be
|
373
|
+
# coerced into a snippet of spec that produces the appropriate tag.
|
374
|
+
#
|
375
|
+
# @param link [#to_a, Hash] A spec describing one or more +<link/>+ elements.
|
376
|
+
#
|
377
|
+
# @param meta [#to_a, Hash] A spec describing one or more +<meta/>+ elements.
|
378
|
+
#
|
379
|
+
# @param style [#to_a, Hash] A spec describing one or more
|
380
|
+
# +<style/>+ elements.
|
381
|
+
#
|
382
|
+
# @param script [#to_a, Hash] A spec describing one or more
|
383
|
+
# +<script/>+ elements.
|
384
|
+
#
|
385
|
+
# @param attr [Hash] A spec containing attributes for the +<body>+.
|
386
|
+
#
|
387
|
+
# @param content [Hash, Array, Nokogiri::XML::Node, ...] A spec which
|
388
|
+
# will be attached underneath the +<body>+.
|
389
|
+
#
|
390
|
+
# @param head [Hash] A spec which overrides the entire +<head>+.
|
391
|
+
#
|
392
|
+
# @param body [Hash] A spec which overrides the entire +<body>+.
|
393
|
+
#
|
394
|
+
# @param transform [#to_s] An optional XSLT transform.
|
395
|
+
#
|
396
|
+
# @param dtd [true, false, nil, #to_a] Whether or not to attach a
|
397
|
+
# +<!DOCTYPE html>+ declaration. Can be given as an array-like
|
398
|
+
# thing containing two stringlike things which serve as public and
|
399
|
+
# system identifiers. Defaults to +true+.
|
400
|
+
#
|
401
|
+
# @param xmlns [true, false, nil, Hash] Whether or not to include
|
402
|
+
# XML namespace declarations, including the XHTML declaration. When
|
403
|
+
# given as a {Hash}, it will set _only the hash contents_ as
|
404
|
+
# namespaces. Defaults to +true+.
|
405
|
+
#
|
406
|
+
# @param args [#to_a] Arguments for any callbacks in the spec.
|
407
|
+
#
|
408
|
+
# @return [Nokogiri::XML::Node] the last node generated, in document order.
|
409
|
+
|
410
|
+
def xhtml_stub doc: nil, base: nil, ns: {}, prefix: {}, vocab: nil,
|
411
|
+
lang: nil, title: nil, link: [], meta: [], style: [], script: [],
|
412
|
+
head: {}, body: {}, attr: {}, content: [],
|
413
|
+
transform: nil, dtd: true, xmlns: true, args: []
|
414
|
+
|
415
|
+
spec = []
|
416
|
+
|
417
|
+
# add xslt stylesheet
|
418
|
+
if transform
|
419
|
+
spec << (transform.is_a? Hash ? transform :
|
420
|
+
{ nil => ['#pi', 'xml-stylesheet'],
|
421
|
+
type: 'text/xsl', href: transform.to_s })
|
422
|
+
end
|
423
|
+
|
424
|
+
# add doctype declaration
|
425
|
+
if dtd
|
426
|
+
ps = dtd.respond_to?(:to_a) ? dtd.to_a : []
|
427
|
+
spec << { nil => %w{#dtd html} + ps }
|
428
|
+
end
|
429
|
+
|
430
|
+
# construct document tree
|
431
|
+
|
432
|
+
head ||= {}
|
433
|
+
if head.empty?
|
434
|
+
head[nil] = [:head, title, base, link, meta, style, script]
|
435
|
+
end
|
436
|
+
|
437
|
+
body ||= {}
|
438
|
+
if body.empty?
|
439
|
+
|
440
|
+
body[nil] = [:body, content]
|
441
|
+
end
|
442
|
+
|
443
|
+
root = { nil => [:html, [head, body]] }
|
444
|
+
root[:vocab] = vocab if vocab
|
445
|
+
root[:lang] = lang if lang
|
446
|
+
|
447
|
+
# deal with namespaces
|
448
|
+
if xmlns
|
449
|
+
root['xmlns'] = 'http://www.w3.org/1999/xhtml'
|
450
|
+
|
451
|
+
# namespaced language attribute
|
452
|
+
root['xml:lang'] = lang if lang
|
453
|
+
end
|
454
|
+
|
455
|
+
# deal with prefixes distinct from namespaces
|
456
|
+
if prefix
|
457
|
+
end
|
458
|
+
|
459
|
+
# add the document structure to the spec
|
460
|
+
spec << root
|
461
|
+
|
462
|
+
# as usual this will return the last innermost node
|
463
|
+
markup spec: spec, doc: doc
|
464
|
+
end
|
465
|
+
|
466
|
+
private
|
467
|
+
|
468
|
+
def element tag, doc: nil, ns: {}, attr: {}, args: []
|
469
|
+
raise unless doc
|
470
|
+
prefix = local = nil
|
471
|
+
if tag.respond_to? :to_a
|
472
|
+
prefix, local = tag
|
473
|
+
tag = tag.join ':'
|
474
|
+
end
|
475
|
+
elem = doc.create_element tag.to_s
|
476
|
+
ns.sort.each do |p, u|
|
477
|
+
elem.add_namespace((p.nil? ? p : p.to_s), u.to_s)
|
478
|
+
end
|
479
|
+
attr.sort.each do |k, v|
|
480
|
+
elem[k.to_s] = flatten(v, args)
|
481
|
+
end
|
482
|
+
|
483
|
+
elem
|
484
|
+
end
|
485
|
+
|
486
|
+
ATOMS = [String, Symbol, Numeric, NilClass, FalseClass, TrueClass]
|
487
|
+
|
488
|
+
# yo dawg
|
489
|
+
|
490
|
+
def flatten obj, args
|
491
|
+
# early bailout for most likely condition
|
492
|
+
if ATOMS.any? { |x| obj.is_a? x }
|
493
|
+
obj.to_s
|
494
|
+
elsif obj.is_a? Hash
|
495
|
+
obj.sort.map { |kv| "#{kv[0].to_s}: #{flatten(kv[1], args)}" }.join(' ')
|
496
|
+
elsif obj.respond_to? :call
|
497
|
+
obj.call(*args)
|
498
|
+
elsif obj.respond_to? :map
|
499
|
+
obj.map { |x| flatten(x, args) }.join(' ')
|
500
|
+
else
|
501
|
+
obj.to_s
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
end
|
data/lib/xml-mixup.rb
ADDED
data/xml-mixup.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "xml/mixup/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "xml-mixup"
|
8
|
+
spec.version = XML::Mixup::VERSION
|
9
|
+
spec.authors = ["Dorian Taylor"]
|
10
|
+
spec.email = ["code@doriantaylor.com"]
|
11
|
+
spec.license = 'Apache-2.0'
|
12
|
+
spec.summary = %q{A mixin for (XML) markup}
|
13
|
+
spec.description = %q{XML::Mixup uses declarative data structures to incrementally generate XML.}
|
14
|
+
spec.homepage = "https://github.com/doriantaylor/rb-xml-mixup"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
|
19
|
+
# if spec.respond_to?(:metadata)
|
20
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
21
|
+
# else
|
22
|
+
# raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
# "public gem pushes."
|
24
|
+
# end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
34
|
+
spec.add_development_dependency "rake", "> 10.0"
|
35
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
36
|
+
|
37
|
+
spec.add_dependency "nokogiri", "~> 1.8.2"
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xml-mixup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dorian Taylor
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nokogiri
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.8.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.8.2
|
69
|
+
description: XML::Mixup uses declarative data structures to incrementally generate
|
70
|
+
XML.
|
71
|
+
email:
|
72
|
+
- code@doriantaylor.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- ".rspec"
|
79
|
+
- ".travis.yml"
|
80
|
+
- Gemfile
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- lib/xml-mixup.rb
|
84
|
+
- lib/xml/mixup.rb
|
85
|
+
- lib/xml/mixup/version.rb
|
86
|
+
- xml-mixup.gemspec
|
87
|
+
homepage: https://github.com/doriantaylor/rb-xml-mixup
|
88
|
+
licenses:
|
89
|
+
- Apache-2.0
|
90
|
+
metadata: {}
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubyforge_project:
|
107
|
+
rubygems_version: 2.7.6
|
108
|
+
signing_key:
|
109
|
+
specification_version: 4
|
110
|
+
summary: A mixin for (XML) markup
|
111
|
+
test_files: []
|