xml-mixup 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []