tidy_json 0.2.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e53774339fb56a01ae47f0dd087159e9a1d2fa070dccda937d5a7de399e07ba9
4
- data.tar.gz: 9f6736f1c5ef84b2d4e89b507b7fa417b3ab713b01226c55c86afc54d07699ab
3
+ metadata.gz: 53537db0fe360e1e7d7fe1ba75cb91418c6efa9ee9f108c2f8a2e3020bffed39
4
+ data.tar.gz: f665d2faff39441554d62de9cf91a6af9462d146501f1ba979d0b5ea21785e9d
5
5
  SHA512:
6
- metadata.gz: f35fe6a646a7e47c5926e04a10222cd483f45d177d3808d3de51040404b6d9ab4fdee940ceb077dba5e74b55ca171699331896485091e6c0c1a0f7c9f1ecb422
7
- data.tar.gz: d599497adc2efb538341d2b4ad09b9b7b9f47ffd30ee056f9c47ceeda42953a6a23eb8266aa87c7ed220424a4a68ec323c864f10e7a78618bd1688dd4e4d627e
6
+ metadata.gz: 01d9f6899b775b39c1cad8acbdf9bdbf563f5a61bd74ca5e395c47d0acb0cc8f963328d5a617c2366aeccd70d5b2f2d423c59485e828055e8743b945e45afd52
7
+ data.tar.gz: ae1c4ce4ee8b0b123b3627c694bfb9a2acec49b53387d830ce6acd2215347305552e6fbe2248197991f47ab0be07cd2888c824bb71fee3f56a86678dd05a063c
data/.yardopts CHANGED
@@ -1 +1,3 @@
1
- --private lib/*.rb lib/tidy_json/version.rb - README.md LICENSE
1
+ --exclude lib/tidy_json/dedication.rb
2
+ --private lib/**/*.rb
3
+ --files README.md,LICENSE
data/Gemfile CHANGED
@@ -7,3 +7,10 @@ gemspec
7
7
  group :test do
8
8
  gem 'rake'
9
9
  end
10
+
11
+ group :development do
12
+ install_if -> { ENV['COVERAGE'] } do
13
+ gem 'simplecov'
14
+ gem 'simplecov-cobertura'
15
+ end
16
+ end
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019-2020 Robert Di Pardo
3
+ Copyright (c) 2019-2021 Robert Di Pardo
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # tidy_json
2
2
 
3
- [![Build Status][travis_build_status_badge]][travis_build_status] [![cci_build_status_badge]][cci_build_status] [![Gem Version][gem_version_badge]][gem_version]
3
+ ![Gem Version][gem_version_badge] ![Downloads][gem_downloads] [![Travis CI][travis_build_status_badge]][travis_build_status] [![codecov][codecov_badge]][codecov_status]
4
4
 
5
5
  A mixin providing (recursive) JSON serialization and pretty printing.
6
6
 
@@ -19,6 +19,13 @@ gem 'tidy_json'
19
19
  # ...
20
20
  ```
21
21
 
22
+ ### Formatting Options
23
+
24
+ As of version [0.3.0][], most of the same options accepted by [`JSON.generate`][]
25
+ can be passed to `#write_json`, `#to_tidy_json`, or `TidyJson.tidy`.
26
+
27
+ See [the docs][] for a current list of options and their default values.
28
+
22
29
  ### Example
23
30
 
24
31
  ```ruby
@@ -27,22 +34,27 @@ require 'tidy_json'
27
34
  class Jsonable
28
35
  attr_reader :a, :b
29
36
  def initialize
30
- @a = { a: 'uno', f: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', { 'ichi': "\u{4e00}", 'ni': "\u{4e8c}", 'san': "\u{4e09}", 'yon': "\u{56db}" }]], b: 'dos' }
31
- @b = { z: { iv: 4, ii: 'duos', iii: 3, i: 'one' }, b: ['two', 3, '<abbr title="four">IV</abbr>'], a: 1, f: %w[x y z] }
37
+ @a = { a: 'uno', f: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', { 'ichi': "\u{4e00}", 'ni': "\u{4e8c}", 'san': "\u{4e09}", 'yon': "\u{56db}" }]], c: {}, b: 'dos', e: [[]] }
38
+ @b = { z: { iv: 4, ii: 'dos', iii: 3, i: 'uno' }, b: ['deux', 3, '<abbr title="four">IV</abbr>'], a: 1, g: [{ none: [] }], f: %w[x y z] }
32
39
  end
33
40
  end
34
41
 
35
42
  my_jsonable = Jsonable.new
43
+ # => #<Jsonable:0x000055790c93e768 @a={:a=>"uno", :f=>["I", "II", "III", ["i.", "ii.", "iii.", {:ichi=>"一", :ni=>"二", :san=>"三", :yon=>"四"}]], :c=>{}, :b=>"dos", :e=>[[]]}, @b={:z=>{:iv=>4, :ii=>"dos", :iii=>3, :i=>"uno"}, :b=>["deux", 3, "<abbr title=\"four\">IV</abbr>"], :a=>1, :g=>[{:none=>[]}], :f=>["x", "y", "z"]}>
36
44
 
37
45
  JSON.parse my_jsonable.stringify
38
- # => {"class"=>"Jsonable", "a"=>{"a"=>"uno", "f"=>["I", "II", "III", ["i.", "ii.", "iii.", {"ichi"=>"", "ni"=>"", "san"=>"", "yon"=>""}]], "b"=>"dos"}, "b"=>{"z"=>{"iv"=>4, "ii"=>"duos", "iii"=>3, "i"=>"one"}, "b"=>["two", 3, "<abbr title=\"four\">IV</abbr>"], "a"=>1, "f"=>["x", "y", "z"]}}
46
+ # => "{\"class\":\"Jsonable\",\"a\":{\"a\":\"uno\",\"f\":[\"I\",\"II\",\"III\",[\"i.\",\"ii.\",\"iii.\",{\"ichi\":\"一\",\"ni\":\"二\",\"san\":\"三\",\"yon\":\"四\"}]],\"c\":{},\"b\":\"dos\",\"e\":[[]]},\"b\":{\"z\":{\"iv\":4,\"ii\":\"dos\",\"iii\":3,\"i\":\"uno\"},\"b\":[\"deux\",3,\"<abbr title=\\\"four\\\">IV</abbr>\"],\"a\":1,\"g\":[{\"none\":[]}],\"f\":[\"x\",\"y\",\"z\"]}}"
39
47
 
40
- puts my_jsonable.to_tidy_json(indent: 4, sort: true)
48
+ puts my_jsonable.to_tidy_json(indent: 4, sort: true, space_before: 2, ascii_only: true)
41
49
  # {
42
- # "a": {
43
- # "a": "uno",
44
- # "b": "dos",
45
- # "f": [
50
+ # "a" : {
51
+ # "a" : "uno",
52
+ # "b" : "dos",
53
+ # "c" : {},
54
+ # "e" : [
55
+ # []
56
+ # ],
57
+ # "f" : [
46
58
  # "I",
47
59
  # "II",
48
60
  # "III",
@@ -51,54 +63,88 @@ puts my_jsonable.to_tidy_json(indent: 4, sort: true)
51
63
  # "ii.",
52
64
  # "iii.",
53
65
  # {
54
- # "ichi": "",
55
- # "ni": "",
56
- # "san": "",
57
- # "yon": ""
66
+ # "ichi" : "\u4e00",
67
+ # "ni" : "\u4e8c",
68
+ # "san" : "\u4e09",
69
+ # "yon" : "\u56db"
58
70
  # }
59
71
  # ]
60
72
  # ]
61
73
  # },
62
- # "b": {
63
- # "a": 1,
64
- # "b": [
65
- # "two",
74
+ # "b" : {
75
+ # "a" : 1,
76
+ # "b" : [
77
+ # "deux",
66
78
  # 3,
67
79
  # "<abbr title=\"four\">IV</abbr>"
68
80
  # ],
69
- # "f": [
81
+ # "f" : [
70
82
  # "x",
71
83
  # "y",
72
84
  # "z"
73
85
  # ],
74
- # "z": {
75
- # "i": "one",
76
- # "ii": "duos",
77
- # "iii": 3,
78
- # "iv": 4
86
+ # "g" : [
87
+ # {
88
+ # "none" : []
89
+ # }
90
+ # ],
91
+ # "z" : {
92
+ # "i" : "uno",
93
+ # "ii" : "dos",
94
+ # "iii" : 3,
95
+ # "iv" : 4
79
96
  # }
80
97
  # },
81
- # "class": "Jsonable"
98
+ # "class" : "Jsonable"
82
99
  # }
83
100
  # => nil
84
101
  ```
85
102
 
86
- ### Dependencies
103
+ ### Command Line Usage
87
104
 
88
- #### Runtime
89
- - [json](https://rubygems.org/gems/json) ~> 2.2
105
+ After [installing the gem][], pass the name of a file containing JSON to `jtidy`
106
+ (with or without a file extension). Run `jtidy -h` for a complete list of
107
+ formatting options:
90
108
 
91
- #### Building
92
- - [test-unit](https://rubygems.org/gems/test-unit) ~> 3.3
93
- - [yard](https://rubygems.org/gems/yard) ~> 0.9
109
+ ```
110
+ jtidy FILE[.json] [-d out[.json]] [-i [2,4,6,8,10,12]] [-p [1..8]] [-v [1..8]] [-o D] [-a D] [-m N] [-e] [-A] [-N] [-s] [-f] [-P]
111
+ -d, --dest out[.json] Name of output file
112
+ -i, --indent [2,4,6,8,10,12] The number of spaces to indent each object member [2]
113
+ -p, --prop-name-space [1..8] The number of spaces to put after property names [0]
114
+ -v, --value-space [1..8] The number of spaces to put before property values [1]
115
+ -o, --object-delim D A string of whitespace to delimit object members [\n]
116
+ -a, --array-delim D A string of whitespace to delimit array elements [\n]
117
+ -m, --max-nesting N The maximum level of data structure nesting in the generated JSON; 0 == "no depth checking" [100]
118
+ -e, --escape Escape /'s [false]
119
+ -A, --ascii Generate ASCII characters only [false]
120
+ -N, --nan Allow NaN, Infinity and -Infinity [false]
121
+ -s, --sort Sort property names [false]
122
+ -f, --force Overwrite source file [false]
123
+ -P, --preview Show preview of output [false]
124
+ -V, --version Show version
125
+ -h, --help Show this help message
126
+ ```
94
127
 
95
- ### License
96
- [MIT](https://github.com/rdipardo/tidy_json/blob/master/LICENSE)
128
+ ### Notice
129
+ The `jtidy` executable bundled with this gem is in no way affiliated with, nor based on,
130
+ the HTML parser and pretty printer [of the same name](https://github.com/jtidy/jtidy).
97
131
 
132
+ The JTidy source code and binaries are licensed under the terms of the Zlib-Libpng License.
133
+ More information is available [here](https://github.com/jtidy/jtidy/blob/master/LICENSE.txt).
98
134
 
99
- [travis_build_status]: https://travis-ci.com/rdipardo/tidy_json
100
- [cci_build_status]: https://circleci.com/gh/rdipardo/tidy_json/tree/master
101
- [cci_build_status_badge]: https://circleci.com/gh/rdipardo/tidy_json.svg?style=svg
102
- [travis_build_status_badge]: https://travis-ci.com/rdipardo/tidy_json.svg?branch=master
103
- [gem_version]: https://badge.fury.io/rb/tidy_json
104
- [gem_version_badge]: https://badge.fury.io/rb/tidy_json.svg
135
+ ### License
136
+ Distributed under the terms of the [MIT License][].
137
+
138
+
139
+ [travis_build_status]: https://app.travis-ci.com/github/rdipardo/tidy_json
140
+ [travis_build_status_badge]: https://app.travis-ci.com/rdipardo/tidy_json.svg?branch=master
141
+ [codecov_status]: https://codecov.io/gh/rdipardo/tidy_json/branch/master
142
+ [codecov_badge]: https://codecov.io/gh/rdipardo/tidy_json/branch/master/graph/badge.svg
143
+ [gem_version_badge]: https://img.shields.io/gem/v/tidy_json?color=%234ec820&label=gem%20version&logo=ruby&logoColor=%23e9573f
144
+ [gem_downloads]: https://img.shields.io/gem/dt/tidy_json?logo=ruby&logoColor=%23e9573f
145
+ [MIT License]: https://github.com/rdipardo/tidy_json/blob/master/LICENSE
146
+ [installing the gem]: https://github.com/rdipardo/tidy_json#installation
147
+ <!-- API spec -->
148
+ [`JSON.generate`]: https://github.com/flori/json/blob/d49c5de49e54a5ad3f6fcf587f98d63266ef9439/lib/json/pure/generator.rb#L111
149
+ [the docs]: https://rubydoc.org/github/rdipardo/tidy_json/TidyJson/Formatter#initialize-instance_method
150
+ [0.3.0]: https://github.com/rdipardo/tidy_json/releases/tag/v0.3.0
data/bin/jtidy ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'tidy_json'
6
+ require_relative 'jtidy_info'
7
+
8
+ class Jtidy # :nodoc:
9
+ OPTIONS = {
10
+ '-d out[.json]': [:dest, String, '--dest', 'Name of output file'],
11
+ '-i [2,4,6,8,10,12]': [:indent, Integer, '--indent [2,4,6,8,10,12]',
12
+ 'The number of spaces to indent each object member [2]'],
13
+ '-p [1..8]': [:space_before, Integer, '--prop-name-space [1..8]',
14
+ 'The number of spaces to put after property names [0]'],
15
+ '-v [1..8]': [:space, Integer, '--value-space [1..8]',
16
+ 'The number of spaces to put before property values [1]'],
17
+ '-o D': [:object_nl, String, '--object-delim D',
18
+ 'A string of whitespace to delimit object members [\n]'],
19
+ '-a D': [:array_nl, String, '--array-delim D',
20
+ 'A string of whitespace to delimit array elements [\n]'],
21
+ '-m N': [:max_nesting, Integer, '--max-nesting N',
22
+ 'The maximum level of data structure nesting in the generated ' \
23
+ 'JSON; 0 == "no depth checking" [100]'],
24
+ '-e': [:escape_slash, nil, '--escape', 'Escape /\'s [false]'],
25
+ '-A': [:ascii_only, nil, '--ascii', 'Generate ASCII characters only [false]'],
26
+ '-N': [:allow_nan, nil, '--nan', 'Allow NaN, Infinity and -Infinity [false]'],
27
+ '-s': [:sort, nil, '--sort', 'Sort property names [false]'],
28
+ '-f': [:force, nil, '--force', 'Overwrite source file [false]'],
29
+ # script-only options
30
+ '-P': [:preview, nil, '--preview', 'Show preview of output [false]']
31
+ }.freeze
32
+
33
+ def self.unescape(str)
34
+ str.gsub(/\\b|\\h|\\n|\\r|\\s|\\t|\\v/,
35
+ {
36
+ '\\b': "\b",
37
+ '\\h': "\h",
38
+ '\\n': "\n",
39
+ '\\r': "\r",
40
+ '\\s': "\s",
41
+ '\\t': "\t",
42
+ '\\v': "\v"
43
+ })
44
+ end
45
+
46
+ def self.show_unused(opts)
47
+ return if opts.empty?
48
+
49
+ ignored = opts.keys.map do |key|
50
+ (OPTIONS.keys.select do |k|
51
+ OPTIONS[k][0].eql? key
52
+ end.first || '')[0..1]
53
+ end
54
+ warn "Ignoring options: #{(ignored.join ', ')}"
55
+ end
56
+
57
+ def self.parse(options)
58
+ format_options = {}
59
+ OptionParser.new do |opts|
60
+ opts.banner = \
61
+ "#{File.basename __FILE__} FILE[.json] " \
62
+ "#{(OPTIONS.keys.map { |k| "[#{k}]" }).join ' '}"
63
+ OPTIONS.each_key do |k|
64
+ opt, type, long_name, desc = OPTIONS[k]
65
+ opts.on(k, long_name, type, desc) do |v|
66
+ format_options[opt] = (type == String ? unescape(v) : v)
67
+ end
68
+ end
69
+
70
+ opts.on_tail('-V', '--version', 'Show version') do
71
+ show_unused format_options
72
+ puts ::JtidyInfo.new.to_s
73
+ exit 0
74
+ end
75
+
76
+ opts.on_tail('-h', '--help', 'Show this help message') do
77
+ show_unused format_options
78
+ puts opts
79
+ exit 0
80
+ end
81
+ end.parse! options
82
+
83
+ format_options
84
+ end
85
+
86
+ private_class_method :unescape, :show_unused
87
+ end
88
+
89
+ begin
90
+ begin
91
+ OPTS = Jtidy.parse(ARGV).freeze
92
+ INPUT_FILE = ARGV[0].freeze
93
+ rescue OptionParser::InvalidOption => e
94
+ warn e.message.capitalize
95
+ raise OptionParser::InvalidArgument
96
+ end
97
+
98
+ if !(INPUT_FILE.nil? || INPUT_FILE.strip.empty?)
99
+ tidy = ''
100
+ fname = INPUT_FILE.strip.gsub('\\', '/')
101
+ ext = File.extname(fname)
102
+ input = File.join(
103
+ File.expand_path(File.dirname(fname)), File.basename(fname, ext)
104
+ ).to_s
105
+ outfile = unless OPTS[:dest].nil? || OPTS[:dest].strip.empty?
106
+ fname = OPTS[:dest].strip.gsub('\\', '/')
107
+ ext = File.extname(fname)
108
+ File.join(
109
+ File.expand_path(File.dirname(fname)), File.basename(fname, ext)
110
+ ).to_s
111
+ end
112
+
113
+ begin
114
+ File.open((input + '.json'), 'r') do |json|
115
+ begin
116
+ tidy = TidyJson.tidy(JSON.parse(json.read.strip), OPTS)
117
+ rescue JSON::JSONError => e
118
+ warn "#{__FILE__}.#{__LINE__}: #{e.message}"
119
+ end
120
+ end
121
+
122
+ if tidy.length.positive?
123
+ output = (if OPTS[:force]
124
+ outfile.nil? ? input : outfile
125
+ elsif Regexp.new("(#{outfile})", Regexp::IGNORECASE) =~ input
126
+ warn "Can't overwrite #{input}.json without '--force' option"
127
+ "#{input}-tidy"
128
+ else outfile
129
+ end) + '.json'
130
+ File.open(output, 'w') { |fd| fd.write(tidy) }
131
+ puts "\nWrote: #{output}"
132
+ puts "#{tidy[0..1024]}\n . . ." if OPTS[:preview]
133
+ end
134
+ rescue Errno::ENOENT, Errno::EACCES, IOError => e
135
+ warn "#{__FILE__}.#{__LINE__}: #{e.message}"
136
+ end
137
+
138
+ else
139
+ Jtidy.parse %w[--help]
140
+ end
141
+ rescue OptionParser::InvalidArgument, OptionParser::MissingArgument
142
+ Jtidy.parse %w[--help]
143
+ end
data/bin/jtidy_info.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TidyJson # :nodoc:
4
+ class JtidyInfo
5
+ NOTICE = [
6
+ '#',
7
+ '# jtidy is in no way affiliated with, nor based on, ',
8
+ '# the HTML parser and pretty printer of the same name.',
9
+ '#',
10
+ '# The JTidy source code and binaries are licensed under',
11
+ '# the terms of the Zlib-Libpng License.',
12
+ '#',
13
+ '# More information is available here:',
14
+ '# https://github.com/jtidy/jtidy/blob/master/LICENSE.txt',
15
+ '#'
16
+ ].join("\n").freeze
17
+
18
+ attr_reader :meta
19
+
20
+ def initialize
21
+ gem = Gem::Specification.find_by_name('tidy_json')
22
+ @meta = {
23
+ name: "# jtidy #{gem.version}",
24
+ license: "# License: #{gem.license}",
25
+ bugs: "# Bugs: #{gem.metadata['bug_tracker_uri']}",
26
+ notice: NOTICE
27
+ }
28
+ end
29
+
30
+ def to_s
31
+ (@meta.values.join "\n").freeze
32
+ end
33
+ end
34
+ end
@@ -7,8 +7,14 @@ module TidyJson # :nodoc:
7
7
  "#{'.' * 17} to the memory of #{'.' * 17}\n#{'.' * 52}\n" \
8
8
  "#{'.' * 17} MICHAEL DI PARDO #{'.' * 17}\n#{'.' * 52}\n" \
9
9
  "#{'.' * 12} Please consider supporting #{'.' * 12}\n" \
10
- "#{'.' * 13} the MS Society of Canada #{'.' * 13}\n" \
10
+ "#{'.' * 11} multiple sclerosis research: #{'.' * 11}\n" \
11
11
  "#{'.' * 52}\n" \
12
+ "#{'.' * 23} (US) #{'.' * 23}\n" \
13
+ "#{'.' * 5} https://www.nationalmssociety.org/Donate " \
14
+ "#{'.' * 5}\n" \
15
+ "#{'.' * 21} (Canada) #{'.' * 21}\n" \
12
16
  "#{'.' * 8} https://mssociety.ca/get-involved #{'.' * 9}\n" \
13
- "#{'.' * 52}\n\n"
17
+ "#{'.' * 23} (UK) #{'.' * 23}\n" \
18
+ "#{'.' * 4} https://www.mssociety.org.uk/get-involved "\
19
+ "#{'.' * 5}\n#{'.' * 52}\n\n"
14
20
  end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: false
2
+
3
+ module TidyJson
4
+ ##
5
+ # A purpose-built JSON formatter.
6
+ #
7
+ # @api private
8
+ class Formatter
9
+ # @return [Hash] the JSON format options specified by this +Formatter+
10
+ # instance.
11
+ attr_reader :format
12
+
13
+ ##
14
+ # Returns a new instance of +Formatter+.
15
+ #
16
+ # @param opts [Hash] Formatting options.
17
+ # @option opts [[2,4,6,8,10,12]] :indent (2) The number of spaces to indent
18
+ # each object member.
19
+ # @option opts [[1..8]] :space_before (0) The number of spaces to put after
20
+ # property names.
21
+ # @option opts [[1..8]] :space (1) The number of spaces to put before
22
+ # property values.
23
+ # @option opts [String] :object_nl ("\n") A string of whitespace to delimit
24
+ # object members.
25
+ # @option opts [String] :array_nl ("\n") A string of whitespace to delimit
26
+ # array elements.
27
+ # @option opts [Numeric] :max_nesting (100) The maximum level of data
28
+ # structure nesting in the generated JSON. Disable depth checking by
29
+ # passing +max_nesting: 0+.
30
+ # @option opts [Boolean] :escape_slash (false) Whether or not a forward
31
+ # slash (/) should be escaped.
32
+ # @option opts [Boolean] :ascii_only (false) Whether or not only ASCII
33
+ # characters should be generated.
34
+ # @option opts [Boolean] :allow_nan (false) Whether or not to allow +NaN+,
35
+ # +Infinity+ and +-Infinity+. If +false+, an exception is thrown if one
36
+ # of these values is encountered.
37
+ # @option opts [Boolean] :sort (false) Whether or not object members should
38
+ # be sorted by property name.
39
+ # @see https://github.com/flori/json/blob/b8c1c640cd375f2e2ccca1b18bf943f80ad04816/lib/json/pure/generator.rb#L111 JSON::Pure::Generator
40
+ def initialize(opts = {})
41
+ # The number of times to reduce the left indent of a nested array's
42
+ # opening bracket
43
+ @left_bracket_offset = 0
44
+
45
+ # True if printing a nested array
46
+ @need_offset = false
47
+
48
+ valid_indent = (2..12).step(2).include?(opts[:indent])
49
+ valid_space_before = (1..8).include?(opts[:space_before])
50
+ valid_space_after = (1..8).include?(opts[:space])
51
+ # don't test for the more explicit :integer? method because it's defined
52
+ # for floating point numbers also
53
+ valid_depth = opts[:max_nesting] >= 0 \
54
+ if opts[:max_nesting].respond_to?(:times)
55
+ valid_newline = ->(str) { str.respond_to?(:strip) && str.strip.empty? }
56
+ @format = {
57
+ indent: "\s" * (valid_indent ? opts[:indent] : 2),
58
+ space_before: "\s" * (valid_space_before ? opts[:space_before] : 0),
59
+ space: "\s" * (valid_space_after ? opts[:space] : 1),
60
+ object_nl: (valid_newline.call(opts[:object_nl]) ? opts[:object_nl] : "\n"),
61
+ array_nl: (valid_newline.call(opts[:array_nl]) ? opts[:array_nl] : "\n"),
62
+ max_nesting: valid_depth ? opts[:max_nesting] : 100,
63
+ escape_slash: opts[:escape_slash] || false,
64
+ ascii_only: opts[:ascii_only] || false,
65
+ allow_nan: opts[:allow_nan] || false,
66
+ sorted: opts[:sort] || false
67
+ }
68
+ end
69
+ # ~Formatter#initialize
70
+
71
+ ##
72
+ # Returns the given +node+ as pretty-printed JSON.
73
+ #
74
+ # @param node [#to_s] A visible attribute of +obj+.
75
+ # @param obj [{Object => #to_s}, <#to_s>] The enumerable object
76
+ # containing +node+.
77
+ # @return [String] A formatted string representation of +node+.
78
+ def format_node(node, obj)
79
+ str = ''
80
+ indent = @format[:indent]
81
+
82
+ is_last = (obj.length <= 1) ||
83
+ (obj.length > 1 &&
84
+ (obj.instance_of?(Array) &&
85
+ !(node === obj.first) &&
86
+ (obj.size.pred == obj.rindex(node))))
87
+
88
+ if node.instance_of?(Array)
89
+ str << '['
90
+ str << "\n" unless node.empty?
91
+
92
+ # format array elements
93
+ node.each do |elem|
94
+ if elem.instance_of?(Hash)
95
+ str << "#{indent * 2}{"
96
+ str << "\n" unless elem.empty?
97
+
98
+ elem.each_with_index do |inner_h, h_idx|
99
+ str << "#{indent * 3}\"#{inner_h.first}\": "
100
+ str << node_to_str(inner_h.last, 4)
101
+ str << ',' unless h_idx == elem.to_a.length.pred
102
+ str << "\n"
103
+ end
104
+
105
+ str << (indent * 2).to_s unless elem.empty?
106
+ str << '}'
107
+
108
+ # element a scalar, or a nested array
109
+ else
110
+ is_nested_array = elem.instance_of?(Array) &&
111
+ elem.any? { |e| e.instance_of?(Array) }
112
+ if is_nested_array
113
+ @left_bracket_offset = \
114
+ elem.take_while { |e| e.instance_of?(Array) }.size
115
+ end
116
+
117
+ str << (indent * 2) << node_to_str(elem)
118
+ end
119
+
120
+ str << ",\n" unless node.index(elem) == node.length.pred
121
+ end
122
+
123
+ str << "\n#{indent}" unless node.empty?
124
+ str << ']'
125
+ str << ",\n" unless is_last
126
+
127
+ elsif node.instance_of?(Hash)
128
+ str << '{'
129
+ str << "\n" unless node.empty?
130
+
131
+ # format elements as key-value pairs
132
+ node.each_with_index do |h, idx|
133
+ # format values which are hashes themselves
134
+ if h.last.instance_of?(Hash)
135
+ key = if h.first.eql? ''
136
+ "#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
137
+ else
138
+ "#{indent * 2}\"#{h.first}\": "
139
+ end
140
+
141
+ str << key << '{'
142
+ str << "\n" unless h.last.empty?
143
+
144
+ h.last.each_with_index do |inner_h, inner_h_idx|
145
+ str << "#{indent * 3}\"#{inner_h.first}\": "
146
+ str << node_to_str(inner_h.last, 4)
147
+ str << ",\n" unless inner_h_idx == h.last.to_a.length.pred
148
+ end
149
+
150
+ str << "\n#{indent * 2}" unless h.last.empty?
151
+ str << '}'
152
+
153
+ # format scalar values
154
+ else
155
+ str << "#{indent * 2}\"#{h.first}\": " << node_to_str(h.last)
156
+ end
157
+
158
+ str << ",\n" unless idx == node.to_a.length.pred
159
+ end
160
+
161
+ str << "\n#{indent}" unless node.empty?
162
+ str << '}'
163
+ str << ',' unless is_last
164
+ str << "\n"
165
+
166
+ # scalars
167
+ else
168
+ str << node_to_str(node)
169
+ str << ',' unless is_last
170
+ str << "\n"
171
+ end
172
+
173
+ trim str.gsub(/(#{indent})+[\n\r]+/, '')
174
+ .gsub(/\}\,+/, '},')
175
+ .gsub(/\]\,+/, '],')
176
+ end
177
+ # ~Formatter#format_node
178
+
179
+ ##
180
+ # Returns a JSON-appropriate string representation of +node+.
181
+ #
182
+ # @param node [#to_s] A visible attribute of a Ruby object.
183
+ # @param tabs [Integer] Tab width at which to start printing this node.
184
+ # @return [String] A formatted string representation of +node+.
185
+ def node_to_str(node, tabs = 0)
186
+ graft = ''
187
+ tabs += 2 if tabs.zero?
188
+
189
+ if @need_offset
190
+ tabs -= 1
191
+ @left_bracket_offset -= 1
192
+ end
193
+
194
+ indent = @format[:indent] * (tabs / 2)
195
+
196
+ if node.nil? then graft << 'null'
197
+
198
+ elsif node.instance_of?(Hash)
199
+
200
+ format_node(node, node).scan(/.*$/) do |n|
201
+ graft << "\n" << indent << n
202
+ end
203
+
204
+ elsif node.instance_of?(Array)
205
+ @need_offset = @left_bracket_offset.positive?
206
+
207
+ format_node(node, {}).scan(/.*$/) do |n|
208
+ graft << "\n" << indent << n
209
+ end
210
+
211
+ elsif !node.instance_of?(String) then graft << node.to_s
212
+
213
+ else graft << "\"#{node.gsub(/\"/, '\\"')}\""
214
+ end
215
+
216
+ graft.strip
217
+ end
218
+ # ~Formatter#node_to_str
219
+
220
+ ##
221
+ # Removes any trailing comma from serialized object members.
222
+ #
223
+ # @param node [String] A serialized object member.
224
+ # @return [String] A copy of +node+ without a trailing comma.
225
+ def trim(node)
226
+ if (extra_comma = /(?<trail>,\s*[\]\}]\s*)$/.match(node))
227
+ node.sub(extra_comma[:trail],
228
+ extra_comma[:trail]
229
+ .slice(1, node.length.pred)
230
+ .sub(/^\s/, "\n"))
231
+ else node
232
+ end
233
+ end
234
+ # ~Formatter#trim
235
+ end
236
+
237
+ private_constant :Formatter
238
+ end