Module: XML::Mixup

Extended by:
Mixup
Included in:
Mixup
Defined in:
lib/xml/mixup/version.rb,
lib/xml/mixup.rb

Constant Summary collapse

VERSION =
"0.2.0"

Instance Method Summary collapse

Instance Method Details

#flatten_attr(obj, args = []) ⇒ String

Returns a string suitable for an XML attribute.

Parameters:

  • obj (Object)

    the object to be flattened

  • args (Array, nil) (defaults to: [])

    callback arguments

Returns:

  • (String)

    the attribute in question.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/xml/mixup.rb', line 66

def flatten_attr obj, args = []
  return if obj.nil?
  args ||= []
  # early bailout for most likely condition
  if ATOMS.any? { |x| obj.is_a? x }
    obj.to_s
  elsif obj.is_a? Hash
    tmp = obj.sort.map do |kv|
      v = flatten_attr kv.last, args
      v.nil? ? nil : "#{kv.first.to_s}: #{v}"
    end.compact
    tmp.empty? ? nil : tmp.join(' ')
  elsif obj.respond_to? :call
    flatten_attr obj.call(*args), args
  elsif [Array, Set].any? { |c| obj.is_a? c }
    tmp = obj.to_a.map { |x| flatten_attr x, args }.reject do |x|
      x.nil? || x == ''
    end
    tmp.empty? ? nil : tmp.join(' ')
  else
    obj.to_s
  end
end

#markup(spec: nil, doc: nil, args: [], **nodes) ⇒ Nokogiri::XML::Node

Generates an XML tree from a given specification.

require 'xml-mixup'

class Anything
  include XML::Mixup
end

# note you can now also just call XML::Mixup.markup

something = Anything.new

# generate a structure
node = something.markup spec: [
  { '#pi'   => 'xml-stylesheet', type: 'text/xsl', href: '/transform' },
  { '#dtd'  => :html },
  { '#html' => [
    { '#head' => [
      { '#title' => 'look ma, title' },
      { '#elem'  => :base, href: 'http://the.base/url' },
    ] },
    { '#body' => [
      { '#h1' => 'Illustrious Heading' },
      { '#p'  => :lolwut },
    ] },
  ], xmlns: 'http://www.w3.org/1999/xhtml' }
]

# `node` will correspond to the last thing generated. In this
# case, it will be a text node containing 'lolwut'.

doc = node.document
puts doc.to_xml

Parameters:

  • spec (Hash, Array, Nokogiri::XML::Node, Proc, #to_s) (defaults to: nil)

    An XML tree specification. May be composed of multiple hashes and arrays. See the spec spec.

  • doc (Nokogiri::XML::Document, nil) (defaults to: nil)

    an optional XML document instance; will be created if none given.

  • args (#to_a) (defaults to: [])

    Any arguments to be passed to any callbacks anywhere in the spec. Assumed to be an array.

  • parent (Nokogiri::XML::Node)

    The node under which the evaluation result of the spec is to be attached. This is the default adjacent node, which in turn defaults to the document if it or no other adjacent node is given. Conflicts with other adjacent nodes.

  • before (Nokogiri::XML::Node)

    This represents a sibling node which the spec is to be inserted before. Conflicts with other adjacent nodes.

  • after (Nokogiri::XML::Node)

    This represents a sibling node which the spec is to be inserted after. Conflicts with other adjacent nodes.

  • replace (Nokogiri::XML::Node)

    This represents a sibling node which the spec is intended to replace. Conflicts with other adjacent nodes.

Returns:

  • (Nokogiri::XML::Node)

    the last node generated, in document order. Will return a Nokogiri::XML::Document when called without arguments.



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/xml/mixup.rb', line 166

def markup spec: nil, doc: nil, args: [], **nodes
  # handle adjacent node declaration
  adj = nil
  ADJACENT.keys.each do |k|
    if nodes[k]
      if adj
        raise "Cannot bind to #{k}: #{adj} is already present"
      end
      unless nodes[k].is_a? Nokogiri::XML::Node
        raise "#{k} must be an XML node"
      end
      adj = k
    end
  end

  # generate doc/parent
  if adj
    doc ||= nodes[adj].document
    unless adj.to_sym == :parent
      unless (nodes[:parent] = nodes[adj].parent)
        raise "#{adj} node must have a parent node!"
      end
    end
  else
    doc ||= Nokogiri::XML::Document.new
    nodes[adj = :parent] ||= doc
  end

  node = nodes[adj]

  # dispatch based on spec type
  if spec and not (spec.respond_to? :empty? and spec.empty?)
    spec = spec.to_a if spec.is_a? Nokogiri::XML::NodeSet
    if spec.is_a? Array
      par = adj == :parent ? nodes[:parent] : doc.fragment
      out = spec.map do |x|
        markup(spec: x, parent: par, pseudo: nodes[:parent], doc: doc,
               args: nodes[:args])
      end

      # only run this if there is something to run
      if out.length > 0
        # this is already attached if the adjacent node is the parent
        ADJACENT[adj].call(par, nodes[adj]) unless adj == :parent
        node = out.last
      end
      # node otherwise defaults to adjacent

    elsif spec.respond_to? :call
      # handle proc/lambda/whatever
      node = markup(spec: spec.call(*args), args: args,
                    doc: doc, adj => nodes[adj])
    elsif spec.is_a? Hash
      # maybe element, maybe something else

      # find the nil key which should contain a viable node name
      # (and maybe children)
      name     = nil
      children = []
      if x = spec[nil]
        if x.respond_to? :to_a
          x = x.to_a
          name = x.shift
          children = x
        else
          name = x
        end
      elsif (compact = spec.select { |k, _| k.respond_to?(:to_a) or
                 k.is_a?(Nokogiri::XML::Node)}) and !compact.empty?
        # compact syntax eliminates the `nil` key
        raise %q{Spec can't have duplicate compact keys} if compact.count > 1
        children, name = compact.first
        children = children.is_a?(Hash) ||
          children.is_a?(Nokogiri::XML::Node) ? [children] : children.to_a
      elsif (special = spec.select { |k, _| k.respond_to? :to_s and
                 k.to_s.start_with? ?# }) and !special.empty?
        # these are special keys
        raise %q{Spec can't have multiple special keys} if special.count > 1
        name, children = special.first

        if %w{# #elem #element #tag}.include? name
          # then the name is in the `children` slot
          raise "Value of #{name} shorthand formulation" +
            "must be a valid element name" unless children.respond_to? :to_s
          name = children
          # set children to empty array
          children = []
        elsif not RESERVED.any? name
          # then the name is encoded into the key and we have to
          # remove the octothorpe
          name = name.delete_prefix ?#
          # note we assign because the object is input and may be frozen
        end

        # don't forget to reset the child nodes
        children = children.is_a?(Hash) || !children.respond_to?(:to_a) ?
          [children] : children.to_a
      end

      # note the name can be nil because it can be inferred

      # now we pull out "attributes" which are the rest of the keys;
      # these should be amenable to being turned into symbols
      attr = spec.select { |k, _|
        k and k.respond_to? :to_sym and not k.to_s.start_with? '#'
      }.transform_keys(&:to_sym)

      # now we dispatch based on the name
      if name == '#comment'
        # first up, comments
        node = doc.create_comment flatten_attr(children, args).to_s

        # attach it
        ADJACENT[adj].call node, nodes[adj]

      elsif name == '#pi' or name == '#processing-instruction'
        # now processing instructions
        if children.empty?
          raise "Processing instruction must have at least a target"
        end
        target  = children[0]
        content = ''
        if (c = children[1..children.length]) and c.length > 0
          #warn c.inspect
          content = flatten_attr(c, args).to_s
        else
          content = attr.sort.map { |pair|
            v = flatten_attr(pair[1], args) or next
            "#{pair[0].to_s}=\"#{v}\""
          }.compact.join(' ')
        end

        node = Nokogiri::XML::ProcessingInstruction.new(doc, target, content)

        #warn node.inspect, content

        # attach it
        ADJACENT[adj].call node, nodes[adj]

      elsif %w[#dtd #doctype].include? name
        # now doctype declarations
        if children.empty?
          raise "DTD node must have a root element declaration"
        end

        # assign as if these are args
        root, pub, sys = children
        # supplant with attributes if present
        pub ||= attr[:public] if attr[:public]
        sys ||= attr[:system] if attr[:system]

        # XXX for some reason this is an *internal* subset?
        # anyway these may not be strings and upstream is not forgiving
        node = doc.create_internal_subset(root.to_s,
                                          pub.nil? ? pub : pub.to_s,
                                          sys.nil? ? sys : sys.to_s)


        # at any rate it doesn't have to be explicitly attached

        # attach it to the document
        #doc.add_child node

        # attach it (?)
        #ADJACENT[adj].call node, nodes[adj]
      elsif name == '#cdata'
        # let's not forget cdata sections
        node = doc.create_cdata flatten_attr(children, args)
        # attach it
        ADJACENT[adj].call node, nodes[adj]

      else
        # finally, an element

        raise "Element name inference NOT IMPLEMENTED: #{spec}" unless name

        # first check the name
        prefix = local = nil
        if name and (md = /^(?:([^:]+):)?(.+)/.match(name.to_s))
          # XXX match actual qname/ncname here
          prefix, local = md.captures
        end

        # next pull apart the namespaces and ordinary attributes
        ns = {}
        at = {}
        attr.each do |k, v|
          k = k.to_s
          v = flatten_attr(v, args) or next
          if (md = /^xmlns(?::(.*))?$/i.match(k))
            ns[md[1]] = v
          else
            at[k] = v
          end
        end

        # now go over the attributes and set any missing namespaces to nil
        at.keys.each do |k|
          p, _ = /^(?:([^:]+):)?(.+)$/.match(k).captures
          ns[p] ||= nil
        end

        # also do the tag prefix but only if there is a local name
        ns[prefix] ||= nil if local

        # unconditionally remove ns['xml'], we never want it in there
        ns.delete 'xml'

        # pseudo is a stand-in for non-parent adjacent nodes
        pseudo = nodes[:pseudo] || nodes[:parent]

        # now get the final namespace mapping
        ns.keys.each do |k|
          pk = k ? "xmlns:#{k}" : "xmlns"
          if pseudo.namespaces.has_key? pk
            ns[k] ||= pseudo.namespaces[pk]
          end
        end
        # delete nil => nil
        if ns.has_key? nil and ns[nil].nil?
          ns.delete(nil)
        end

        # there should be no nil namespace declarations now
        if ns.has_value? nil
          raise "INTERNAL ERROR: nil namespace declaration: #{ns}"
        end

        # generate the node
        node = element name, doc: doc, ns: ns, attr: at, args: args

        # attach it
        ADJACENT[adj].call node, nodes[adj]

        # don't forget the children!
        if children.length > 0
          #warn node.inspect, children.inspect
          node = markup(spec: children, doc: doc, parent: node, args: args)
        end
      end
    else
      if spec.is_a? Nokogiri::XML::Node
        # existing node
        node = spec.dup 1
      else
        # text node
        node = doc.create_text_node spec.to_s
      end

      # attach it
      ADJACENT[adj].call node, nodes[adj]
    end
  end

  # return the node
  node
end

#xhtml_stub(doc: nil, base: nil, ns: {}, prefix: {}, vocab: nil, lang: nil, title: nil, link: [], meta: [], style: [], script: [], extra: [], head: {}, body: {}, attr: {}, content: [], transform: nil, dtd: true, xmlns: true, args: []) ⇒ Nokogiri::XML::Node

Note:

*This method is still under development.* I am still trying to figure out how I want it to behave. Some parts may not work as advertised.

Generates an XHTML stub, with optional RDFa attributes. All parameters are optional.

Parameters:

  • doc (Nokogiri::XML::Document, nil) (defaults to: nil)

    an optional document.

  • base (#to_s) (defaults to: nil)

    the contents of <base href=“”/>.

  • prefix (Hash) (defaults to: {})

    the contents of the root node’s prefix= and xmlns:* attributes.

  • vocab (#to_s) (defaults to: nil)

    the contents of the root node’s vocab=.

  • lang (#to_s) (defaults to: nil)

    the contents of lang= and when applicable, xml:lang.

  • title (#to_s, #to_a, Hash) (defaults to: nil)

    the contents of the <title> tag. When given as an array-like object, all elements after the first one will be flattened to a single string and inserted into the property= attribute. When given as a Hash, it will be coerced into a snippet of spec that produces the appropriate tag.

  • link (#to_a, Hash) (defaults to: [])

    A spec describing one or more <link/> elements.

  • meta (#to_a, Hash) (defaults to: [])

    A spec describing one or more <meta/> elements.

  • style (#to_a, Hash) (defaults to: [])

    A spec describing one or more <style/> elements.

  • script (#to_a, Hash) (defaults to: [])

    A spec describing one or more <script/> elements.

  • extra (#to_a, Hash) (defaults to: [])

    A spec describing any extra elements inside <head> that aren’t in the previous categories.

  • attr (Hash) (defaults to: {})

    A spec containing attributes for the <body>.

  • content (Hash, Array, Nokogiri::XML::Node, ...) (defaults to: [])

    A spec which will be attached underneath the <body>.

  • head (Hash, Array) (defaults to: {})

    A Hash spec which overrides the entire <head> element, or otherwise an array of its children.

  • body (Hash) (defaults to: {})

    A spec which overrides the entire <body>.

  • transform (#to_s) (defaults to: nil)

    An optional XSLT transform.

  • dtd (true, false, nil, #to_a) (defaults to: true)

    Whether or not to attach a <!DOCTYPE html> declaration. Can be given as an array-like thing containing two stringlike things which serve as public and system identifiers. Defaults to true.

  • xmlns (true, false, nil, Hash) (defaults to: true)

    Whether or not to include XML namespace declarations, including the XHTML declaration. When given as a Hash, it will set _only the hash contents_ as namespaces. Defaults to true.

  • args (#to_a) (defaults to: [])

    Arguments for any callbacks in the spec.

Returns:

  • (Nokogiri::XML::Node)

    the last node generated, in document order.



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/xml/mixup.rb', line 490

def xhtml_stub doc: nil, base: nil, ns: {}, prefix: {}, vocab: nil,
    lang: nil, title: nil, link: [], meta: [], style: [], script: [],
    extra: [], head: {}, body: {}, attr: {}, content: [],
    transform: nil, dtd: true, xmlns: true, args: []

  vocab ||= prefix[nil]
  prefix = prefix.except nil if prefix

  spec = []

  # add xslt stylesheet
  if transform
    spec << (transform.is_a?(Nokogiri::XML::ProcessingInstruction) ?
      transform.dup : transform.is_a?(Hash) ? transform :
      { '#pi' => 'xml-stylesheet', type: 'text/xsl', href: transform })
  end

  # add doctype declaration
  if dtd
    ps = dtd.respond_to?(:to_a) ? dtd.to_a : []
    spec << { nil => %w{#dtd html} + ps }
  end

  # normalize title

  if title.nil? or (title.respond_to?(:empty?) and title.empty?)
    title = { '#title' => '' } # add an empty string for closing tag
  elsif title.is_a? Hash
    # nothing
  elsif title.respond_to? :to_a
    title = title.to_a.dup
    text  = title.shift
    props = title
    title = { '#title' => text }
    title[:property] = props unless props.empty?
  else
    title = { '#title' => title }
  end

  # normalize base

  if base and not base.is_a? Hash
    base = { nil => :base, href: base }
  end

  # TODO normalize link, meta, style, script elements

  # construct document tree

  head ||= {}
  if head.is_a? Hash and head.empty?
    head[nil] = [:head, title, base, link, meta, style, script, extra]
  elsif head.is_a? Array
    # children of unmarked head element
    head = { nil => [:head] + head }
  end

  body ||= {}
  if body.is_a? Hash and body.empty?
    body[nil] = [:body, content]
    body = attr.merge(body) if attr and attr.is_a?(Hash)
  end

  root = { nil => [:html, head, body] }
  root[:vocab] = vocab if vocab
  root[:lang]  = lang  if lang

  # deal with namespaces
  if xmlns
    root['xmlns'] = 'http://www.w3.org/1999/xhtml'

    # namespaced language attribute
    root['xml:lang'] = lang if lang

    if !prefix and xmlns.is_a? Hash
      x = prefix.transform_keys { |k| "xmlns:#{k}" }
      root = x.merge(root)
    end
  end

  # deal with prefixes distinct from namespaces
  prefix ||= {}
  if prefix.respond_to? :to_h
    prefix = prefix.to_h
    unless prefix.empty?
      # this will get automatically smushed into the right shape
      root[:prefix] = prefix

      if xmlns
        x = prefix.except(nil).transform_keys { |k| "xmlns:#{k}" }
        root = x.merge(root)
      end
    end
  end

  # add the document structure to the spec
  spec << root

  # as usual this will return the last innermost node
  markup spec: spec, doc: doc, args: args
end

#xml_doc(version = nil) ⇒ Nokogiri::XML::Document

Generate a handy blank document.

Parameters:

  • version (Numeric, nil) (defaults to: nil)

Returns:

  • (Nokogiri::XML::Document)

    a Nokogiri XML document.



96
97
98
# File 'lib/xml/mixup.rb', line 96

def xml_doc version = nil
  Nokogiri::XML::Document.new version
end