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", " ").gsub("\r", " ").
gsub(%r{"}, '"') # " 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
|