aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_view/vendor/builder/xmlbase.rb
blob: 7202bb2ead8403dd5b1390048e08778e3c56a46c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/usr/bin/env ruby

require 'builder/blankslate'

module Builder #:nodoc:

  # Generic error for builder
  class IllegalBlockError < RuntimeError #:nodoc:
  end

  # XmlBase is a base class for building XML builders.  See
  # Builder::XmlMarkup and Builder::XmlEvents for examples.
  class XmlBase < BlankSlate #:nodoc:

    # Create an XML markup builder.
    #
    # out::     Object receiving the markup.1  +out+ must respond to
    #           <tt><<</tt>.
    # indent::  Number of spaces used for indentation (0 implies no
    #           indentation and no line breaks).
    # initial:: Level of initial indentation.
    #
    def initialize(indent=0, initial=0)
      @indent = indent
      @level  = initial
    end
    
    # Create a tag named +sym+.  Other than the first argument which
    # is the tag name, the arguements are the same as the tags
    # implemented via <tt>method_missing</tt>.
    def tag!(sym, *args, &block)
      self.__send__(sym, *args, &block)
    end

    # Create XML markup based on the name of the method.  This method
    # is never invoked directly, but is called for each markup method
    # in the markup block.
    def method_missing(sym, *args, &block)
      text = nil
      attrs = nil
      sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
      args.each do |arg|
	case arg
	when Hash
	  attrs ||= {}
	  attrs.merge!(arg)
	else
	  text ||= ''
	  text << arg.to_s
	end
      end
      if block
	unless text.nil?
	  raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
	end
	_capture_outer_self(block) if @self.nil?
	_indent
	_start_tag(sym, attrs)
	_newline
	_nested_structures(block)
	_indent
	_end_tag(sym)
	_newline
      elsif text.nil?
	_indent
	_start_tag(sym, attrs, true)
	_newline
      else
	_indent
	_start_tag(sym, attrs)
	text! text
	_end_tag(sym)
	_newline
      end
      @target
    end

    # Append text to the output target.  Escape any markup.  May be
    # used within the markup brackets as:
    #
    #   builder.p { br; text! "HI" }   #=>  <p><br/>HI</p>
    def text!(text)
      _text(_escape(text))
    end
    
    # Append text to the output target without escaping any markup.
    # May be used within the markup brackets as:
    #
    #   builder.p { |x| x << "<br/>HI" }   #=>  <p><br/>HI</p>
    #
    # This is useful when using non-builder enabled software that
    # generates strings.  Just insert the string directly into the
    # builder without changing the inserted markup.
    #
    # It is also useful for stacking builder objects.  Builders only
    # use <tt><<</tt> to append to the target, so by supporting this
    # method/operation builders can use other builders as their
    # targets.
    def <<(text)
      _text(text)
    end
    
    # For some reason, nil? is sent to the XmlMarkup object.  If nil?
    # is not defined and method_missing is invoked, some strange kind
    # of recursion happens.  Since nil? won't ever be an XML tag, it
    # is pretty safe to define it here. (Note: this is an example of
    # cargo cult programming,
    # cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
    def nil?
      false
    end

    private
    
    def _escape(text)
      text.
	gsub(%r{&}, '&amp;').
	gsub(%r{<}, '&lt;').
	gsub(%r{>}, '&gt;')
    end

    def _capture_outer_self(block)
      @self = eval("self", block)
    end
    
    def _newline
      return if @indent == 0
      text! "\n"
    end
    
    def _indent
      return if @indent == 0 || @level == 0
      text!(" " * (@level * @indent))
    end
    
    def _nested_structures(block)
      @level += 1
      block.call(self)
    ensure
      @level -= 1
    end
  end
end