File: xmlbase.rb

package info (click to toggle)
ruby-builder 3.2.4-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 368 kB
  • sloc: ruby: 1,945; makefile: 4
file content (199 lines) | stat: -rw-r--r-- 5,811 bytes parent folder | download | duplicates (2)
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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
# frozen_string_literal: true

require 'builder/blankslate'

module Builder

  # Generic error for builder
  class IllegalBlockError < RuntimeError; end

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

    class << self
      attr_accessor :cache_method_calls
    end

    # Create an XML markup builder.
    #
    # out      :: Object receiving the markup.  +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.
    # encoding :: When <tt>encoding</tt> and $KCODE are set to 'utf-8'
    #             characters aren't converted to character entities in
    #             the output stream.
    def initialize(indent=0, initial=0, encoding='utf-8')
      @indent = indent
      @level  = initial
      @encoding = encoding.downcase
    end

    def explicit_nil_handling?
      @explicit_nil_handling
    end

    # Create a tag named +sym+.  Other than the first argument which
    # is the tag name, the arguments are the same as the tags
    # implemented via <tt>method_missing</tt>.
    def tag!(sym, *args, &block)
      text = nil
      attrs = nil
      sym = "#{sym}:#{args.shift}" if args.first.kind_of?(::Symbol)
      sym = sym.to_sym unless sym.class == ::Symbol
      args.each do |arg|
        case arg
        when ::Hash
          attrs ||= {}
          attrs.merge!(arg)
        when nil
          attrs ||= {}
          attrs.merge!({:nil => true}) if explicit_nil_handling?
        else
          text ||= ''.dup
          text << arg.to_s
        end
      end
      if block
        unless text.nil?
          ::Kernel::raise ::ArgumentError,
            "XmlMarkup cannot mix a text argument with a block"
        end
        _indent
        _start_tag(sym, attrs)
        _newline
        begin
          _nested_structures(block)
        ensure
          _indent
          _end_tag(sym)
          _newline
        end
      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

    # 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 that isn't cached.
    def method_missing(sym, *args, &block)
      cache_method_call(sym) if ::Builder::XmlBase.cache_method_calls
      tag!(sym, *args, &block)
    end

    # Append text to the output target.  Escape any markup.  May be
    # used within the markup brackets as:
    #
    #   builder.p { |b| b.br; b.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

    require 'builder/xchar'
    if ::String.method_defined?(:encode)
      def _escape(text)
        result = XChar.encode(text)
        begin
          encoding = ::Encoding::find(@encoding)
          raise Exception if encoding.dummy?
          result.encode(encoding)
        rescue
          # if the encoding can't be supported, use numeric character references
          result.
            gsub(/[^\u0000-\u007F]/) {|c| "&##{c.ord};"}.
            force_encoding('ascii')
        end
      end
    else
      def _escape(text)
        if (text.method(:to_xs).arity == 0)
          text.to_xs
        else
          text.to_xs((@encoding != 'utf-8' or $KCODE != 'UTF8'))
        end
      end
    end

    def _escape_attribute(text)
      _escape(text).gsub("\n", "&#10;").gsub("\r", "&#13;").
        gsub(%r{"}, '&quot;') # " WART
    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

    # If XmlBase.cache_method_calls = true, we dynamicly create the method
    # missed as an instance method on the XMLBase object. Because XML
    # documents are usually very repetative in nature, the next node will
    # be handled by the new method instead of method_missing. As
    # method_missing is very slow, this speeds up document generation
    # significantly.
    def cache_method_call(sym)
      class << self; self; end.class_eval do
        unless method_defined?(sym)
          define_method(sym) do |*args, &block|
            tag!(sym, *args, &block)
          end
        end
      end
    end
  end

  XmlBase.cache_method_calls = true

end