xmlbase.rb 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env ruby
  2. require 'builder/blankslate'
  3. module Builder
  4. # Generic error for builder
  5. class IllegalBlockError < RuntimeError; end
  6. # XmlBase is a base class for building XML builders. See
  7. # Builder::XmlMarkup and Builder::XmlEvents for examples.
  8. class XmlBase < BlankSlate
  9. # Create an XML markup builder.
  10. #
  11. # out:: Object receiving the markup. +out+ must respond to
  12. # <tt><<</tt>.
  13. # indent:: Number of spaces used for indentation (0 implies no
  14. # indentation and no line breaks).
  15. # initial:: Level of initial indentation.
  16. # encoding:: When <tt>encoding</tt> and $KCODE are set to 'utf-8'
  17. # characters aren't converted to character entities in
  18. # the output stream.
  19. def initialize(indent=0, initial=0, encoding='utf-8')
  20. @indent = indent
  21. @level = initial
  22. @encoding = encoding.downcase
  23. end
  24. # Create a tag named +sym+. Other than the first argument which
  25. # is the tag name, the arguments are the same as the tags
  26. # implemented via <tt>method_missing</tt>.
  27. def tag!(sym, *args, &block)
  28. method_missing(sym.to_sym, *args, &block)
  29. end
  30. # Create XML markup based on the name of the method. This method
  31. # is never invoked directly, but is called for each markup method
  32. # in the markup block.
  33. def method_missing(sym, *args, &block)
  34. text = nil
  35. attrs = nil
  36. sym = "#{sym}:#{args.shift}" if args.first.kind_of?(::Symbol)
  37. args.each do |arg|
  38. case arg
  39. when ::Hash
  40. attrs ||= {}
  41. attrs.merge!(arg)
  42. else
  43. text ||= ''
  44. text << arg.to_s
  45. end
  46. end
  47. if block
  48. unless text.nil?
  49. ::Kernel::raise ::ArgumentError,
  50. "XmlMarkup cannot mix a text argument with a block"
  51. end
  52. _indent
  53. _start_tag(sym, attrs)
  54. _newline
  55. begin
  56. _nested_structures(block)
  57. ensure
  58. _indent
  59. _end_tag(sym)
  60. _newline
  61. end
  62. elsif text.nil?
  63. _indent
  64. _start_tag(sym, attrs, true)
  65. _newline
  66. else
  67. _indent
  68. _start_tag(sym, attrs)
  69. text! text
  70. _end_tag(sym)
  71. _newline
  72. end
  73. @target
  74. end
  75. # Append text to the output target. Escape any markup. May be
  76. # used within the markup brackets as:
  77. #
  78. # builder.p { |b| b.br; b.text! "HI" } #=> <p><br/>HI</p>
  79. def text!(text)
  80. _text(_escape(text))
  81. end
  82. # Append text to the output target without escaping any markup.
  83. # May be used within the markup brackets as:
  84. #
  85. # builder.p { |x| x << "<br/>HI" } #=> <p><br/>HI</p>
  86. #
  87. # This is useful when using non-builder enabled software that
  88. # generates strings. Just insert the string directly into the
  89. # builder without changing the inserted markup.
  90. #
  91. # It is also useful for stacking builder objects. Builders only
  92. # use <tt><<</tt> to append to the target, so by supporting this
  93. # method/operation builders can use other builders as their
  94. # targets.
  95. def <<(text)
  96. _text(text)
  97. end
  98. # For some reason, nil? is sent to the XmlMarkup object. If nil?
  99. # is not defined and method_missing is invoked, some strange kind
  100. # of recursion happens. Since nil? won't ever be an XML tag, it
  101. # is pretty safe to define it here. (Note: this is an example of
  102. # cargo cult programming,
  103. # cf. http://fishbowl.pastiche.org/2004/10/13/cargo_cult_programming).
  104. def nil?
  105. false
  106. end
  107. private
  108. require 'builder/xchar'
  109. if ::String.method_defined?(:encode)
  110. def _escape(text)
  111. result = XChar.encode(text)
  112. begin
  113. result.encode(@encoding)
  114. rescue
  115. # if the encoding can't be supported, use numeric character references
  116. result.
  117. gsub(/[^\u0000-\u007F]/) {|c| "&##{c.ord};"}.
  118. force_encoding('ascii')
  119. end
  120. end
  121. else
  122. def _escape(text)
  123. text.to_xs((@encoding != 'utf-8' or $KCODE != 'UTF8'))
  124. end
  125. end
  126. def _escape_quote(text)
  127. _escape(text).gsub(%r{"}, '&quot;') # " WART
  128. end
  129. def _newline
  130. return if @indent == 0
  131. text! "\n"
  132. end
  133. def _indent
  134. return if @indent == 0 || @level == 0
  135. text!(" " * (@level * @indent))
  136. end
  137. def _nested_structures(block)
  138. @level += 1
  139. block.call(self)
  140. ensure
  141. @level -= 1
  142. end
  143. end
  144. end