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 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
@@ -0,0 +1,15 @@
1
+ syntax: glob
2
+ /.bundle/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ Gemfile.lock
11
+ .\#*
12
+ \#*\#
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in mixup.gemspec
6
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,5 @@
1
+ module XML
2
+ module Mixup
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
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
@@ -0,0 +1,3 @@
1
+ require 'xml/mixup'
2
+
3
+ # See {xml/mixup}
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: []