# Please see http://github.com/techcrunch/json_printer # class JsonPrinter attr_reader :buf, :indent # ==== Arguments # obj:: # The object to be rendered into JSON. This object and all of its # associated objects must be either nil, true, false, a String, a Symbol, # a Numeric, an Array, or a Hash. # # ==== Returns # :: # The pretty-printed JSON ecoding of the given obj. This string # can be parsed by any compliant JSON parser without modification. # # ==== Examples # See JsonPrinter docs. # def self.render(obj) new(obj).buf end def self.render_html(obj) render(obj).gsub(' ', ' ').gsub("\n", "
") end private # Execute the JSON rendering of obj, storing the result in the # buf. # def initialize(obj) @buf = "" @indent = "" render(obj) end # Increase the indentation level. # def indent_out @indent << " " end # Decrease the indendation level. # def indent_in @indent.slice!(-1, 1) end # Append the given str to the buf. # def print(str) @buf << str end # Recursive rendering method. Primitive values, like nil, true, false, # numbers, symbols, and strings are converted to JSON and appended to the # buffer. Enumerables are treated specially to generate pretty whitespace. # def render(obj) # We can't use a case statement here becuase "when Hash" doesn't work for # ActiveSupport::OrderedHash - respond_to?(:values) is a more reliable # indicator of hash-like behavior. if NilClass === obj print("null") elsif TrueClass === obj print("true") elsif FalseClass === obj print("false") elsif String === obj print(escape_json_string(obj)) elsif Symbol === obj print("\"#{obj}\"") elsif Numeric === obj print(obj.to_s) elsif Time === obj print(obj.to_s) elsif obj.respond_to?(:keys) print("{") indent_out last_key = obj.keys.last obj.each do |(key, val)| render(key) case val when Hash, Array indent_out print(":\n#{indent}") render(val) indent_in else print(": ") render(val) end print(",\n#{indent}") unless key == last_key end indent_in print("}") elsif Array === obj print("[") indent_out last_index = obj.size - 1 obj.each_with_index do |elem, index| render(elem) print(",\n#{indent}") unless index == last_index end indent_in print("]") else raise "unrenderable object: #{obj.inspect}" end end # Special JSON character escape cases. ESCAPED_CHARS = { "\010" => '\b', "\f" => '\f', "\n" => '\n', "\r" => '\r', "\t" => '\t', '"' => '\"', '\\' => '\\\\', '>' => '\u003E', '<' => '\u003C', '&' => '\u0026'} # String#to_json extracted from ActiveSupport, using interpolation for speed. # def escape_json_string(str) begin "\"#{ str.gsub(/[\010\f\n\r\t"\\><&]/) { |s| ESCAPED_CHARS[s] }. gsub(/([\xC0-\xDF][\x80-\xBF]| [\xE0-\xEF][\x80-\xBF]{2}| [\xF0-\xF7][\x80-\xBF]{3})+/nx) do |s| s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/, '\\\\u\&') end }\"" rescue Encoding::CompatibilityError rawbytes = str.dup.force_encoding 'ASCII-8BIT' escape_json_string(rawbytes) end end end