class TidyJson::Formatter

A purpose-built JSON formatter.

@api private

Attributes

format[R]

@return [Hash] the JSON format options specified by this Formatter

instance.

Public Class Methods

new(opts = {}) click to toggle source

Returns a new instance of Formatter.

@param opts [Hash] Formatting options. @option opts [[2,4,6,8,10,12]] :indent (2) The number of spaces to indent

each object member.

@option opts [[1..8]] :space_before (0) The number of spaces to put after

property names.

@option opts [[1..8]] :space (1) The number of spaces to put before

property values.

@option opts [String] :object_nl (ā€œnā€) A string of whitespace to delimit

object members.

@option opts [String] :array_nl (ā€œnā€) A string of whitespace to delimit

array elements.

@option opts [Numeric] :max_nesting (100) The maximum level of data

structure nesting in the generated JSON. Disable depth checking by
passing +max_nesting: 0+.

@option opts [Boolean] :escape_slash (false) Whether or not a forward

slash (/) should be escaped.

@option opts [Boolean] :ascii_only (false) Whether or not only ASCII

characters should be generated.

@option opts [Boolean] :allow_nan (false) Whether or not to allow NaN,

+Infinity+ and +-Infinity+. If +false+, an exception is thrown if one
of these values is encountered.

@option opts [Boolean] :sort (false) Whether or not object members should

be sorted by property name.

@see github.com/flori/json/blob/b8c1c640cd375f2e2ccca1b18bf943f80ad04816/lib/json/pure/generator.rb#L111 JSON::Pure::Generator

# File lib/tidy_json/formatter.rb, line 40
def initialize(opts = {})
  # The number of times to reduce the left indent of a nested array's
  # opening bracket
  @left_bracket_offset = 0

  # True if printing a nested array
  @need_offset = false

  valid_indent = (2..12).step(2).include?(opts[:indent])
  valid_space_before = (1..8).include?(opts[:space_before])
  valid_space_after = (1..8).include?(opts[:space])
  # don't test for the more explicit :integer? method because it's defined
  # for floating point numbers also
  valid_depth = opts[:max_nesting] >= 0 \
                if opts[:max_nesting].respond_to?(:times)
  valid_newline = ->(str) { str.respond_to?(:strip) && str.strip.empty? }
  @format = {
    indent: "\s" * (valid_indent ? opts[:indent] : 2),
    space_before: "\s" * (valid_space_before ? opts[:space_before] : 0),
    space: "\s" * (valid_space_after ? opts[:space] : 1),
    object_nl: (valid_newline.call(opts[:object_nl]) ? opts[:object_nl] : "\n"),
    array_nl: (valid_newline.call(opts[:array_nl]) ? opts[:array_nl] : "\n"),
    max_nesting: valid_depth ? opts[:max_nesting] : 100,
    escape_slash: opts[:escape_slash] || false,
    ascii_only: opts[:ascii_only] || false,
    allow_nan: opts[:allow_nan] || false,
    sorted: opts[:sort] || false
  }
end

Public Instance Methods

format_node(node, obj) click to toggle source

Returns the given node as pretty-printed JSON.

@param node [#to_s] A visible attribute of obj. @param obj [{Object => to_s}, <#to_s>] The enumerable object

containing +node+.

@return [String] A formatted string representation of node.

# File lib/tidy_json/formatter.rb, line 78
def format_node(node, obj)
  str = ''
  indent = @format[:indent]

  is_last = (obj.length <= 1) ||
            (obj.length > 1 &&
              (obj.instance_of?(Array) &&
                !(node === obj.first) &&
                  (obj.size.pred == obj.rindex(node))))

  if node.instance_of?(Array)
    str << '['
    str << "\n" unless node.empty?

    # format array elements
    node.each do |elem|
      if elem.instance_of?(Hash)
        str << "#{indent * 2}{"
        str << "\n" unless elem.empty?

        elem.each_with_index do |inner_h, h_idx|
          str << "#{indent * 3}\"#{inner_h.first}\": "
          str << node_to_str(inner_h.last, 4)
          str << ',' unless h_idx == elem.to_a.length.pred
          str << "\n"
        end

        str << (indent * 2).to_s unless elem.empty?
        str << '}'

      # element a scalar, or a nested array
      else
        is_nested_array = elem.instance_of?(Array) &&
                          elem.any? { |e| e.instance_of?(Array) }
        if is_nested_array
          @left_bracket_offset = \
            elem.take_while { |e| e.instance_of?(Array) }.size
        end

        str << (indent * 2) << node_to_str(elem)
      end

      str << ",\n" unless node.index(elem) == node.length.pred
    end

    str << "\n#{indent}" unless node.empty?
    str << ']'
    str << ",\n" unless is_last

  elsif node.instance_of?(Hash)
    str << '{'
    str << "\n" unless node.empty?

    # format elements as key-value pairs
    node.each_with_index do |h, idx|
      # format values which are hashes themselves
      if h.last.instance_of?(Hash)
        key = if h.first.eql? ''
                "#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
              else
                "#{indent * 2}\"#{h.first}\": "
              end

        str << key << '{'
        str << "\n" unless h.last.empty?

        h.last.each_with_index do |inner_h, inner_h_idx|
          str << "#{indent * 3}\"#{inner_h.first}\": "
          str << node_to_str(inner_h.last, 4)
          str << ",\n" unless inner_h_idx == h.last.to_a.length.pred
        end

        str << "\n#{indent * 2}" unless h.last.empty?
        str << '}'

      # format scalar values
      else
        str << "#{indent * 2}\"#{h.first}\": " << node_to_str(h.last)
      end

      str << ",\n" unless idx == node.to_a.length.pred
    end

    str << "\n#{indent}" unless node.empty?
    str << '}'
    str << ',' unless is_last
    str << "\n"

  # scalars
  else
    str << node_to_str(node)
    str << ',' unless is_last
    str << "\n"
  end

  trim str.gsub(/(#{indent})+[\n\r]+/, '')
          .gsub(/\}\,+/, '},')
          .gsub(/\]\,+/, '],')
end
node_to_str(node, tabs = 0) click to toggle source

Returns a JSON-appropriate string representation of node.

@param node [#to_s] A visible attribute of a Ruby object. @param tabs [Integer] Tab width at which to start printing this node. @return [String] A formatted string representation of node.

# File lib/tidy_json/formatter.rb, line 185
def node_to_str(node, tabs = 0)
  graft = ''
  tabs += 2 if tabs.zero?

  if @need_offset
    tabs -= 1
    @left_bracket_offset -= 1
  end

  indent = @format[:indent] * (tabs / 2)

  if node.nil? then graft << 'null'

  elsif node.instance_of?(Hash)

    format_node(node, node).scan(/.*$/) do |n|
      graft << "\n" << indent << n
    end

  elsif node.instance_of?(Array)
    @need_offset = @left_bracket_offset.positive?

    format_node(node, {}).scan(/.*$/) do |n|
      graft << "\n" << indent << n
    end

  elsif !node.instance_of?(String) then graft << node.to_s

  else graft << "\"#{node.gsub(/\"/, '\\"')}\""
  end

  graft.strip
end
trim(node) click to toggle source

Removes any trailing comma from serialized object members.

@param node [String] A serialized object member. @return [String] A copy of node without a trailing comma.

# File lib/tidy_json/formatter.rb, line 225
def trim(node)
  if (extra_comma = /(?<trail>,\s*[\]\}]\s*)$/.match(node))
    node.sub(extra_comma[:trail],
             extra_comma[:trail]
             .slice(1, node.length.pred)
             .sub(/^\s/, "\n"))
  else node
  end
end