tidy_json 0.2.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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