pretty_tree 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8901c02377ffae134b6db07c23e714032a9673464c7cbb866e4696f83aedd623
4
+ data.tar.gz: 1b4442d03bc01ffd6719f5ca509e985e3b1ea6b517c4043fb7abc62324b8284e
5
+ SHA512:
6
+ metadata.gz: c9f0c54c4e0d93e65c2e3aadc15d352d8b51751c891c1be760fa59c633fea0c9d64b900b47a7544b2fe58ea5aadf8dd83d2433cd946131d7892d375ecac70492
7
+ data.tar.gz: 9708437a157ede479cc7ffe932bb4723124be884a24f3481b3d0aa55f780b18eb4016258d5dbfc96a3c2b0ec061d26080c8e726cf480f2103e2c9c97088986b9
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-09-30
11
+
12
+ ### Added
13
+
14
+ - `PrettyTree.render` and `PrettyTree.print` draw a tree as an ASCII diagram.
15
+ - Layouts for nodes with one, two, three or more children, with `nil` marking an
16
+ empty slot.
17
+ - `Adapter` interface for rendering any node type, with a default `ArrayTree`
18
+ adapter for nested arrays.
19
+ - `Formatter` interface for labels, with a default formatter that uses `#inspect`
20
+ and truncates long labels.
21
+ - `PrettyTree.preview` prints example trees.
22
+ - `PrettyTree::Error` is raised for invalid nested-array trees.
23
+
24
+ [Unreleased]: https://github.com/riccardo-giomi/pretty_tree/compare/v0.1.0...HEAD
25
+ [0.1.0]: https://github.com/riccardo-giomi/pretty_tree/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Riccardo Giomi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # PrettyTree
2
+
3
+ Pretty-print tree-shaped data as readable ASCII diagrams — for `#inspect` output, log lines, or debugging.
4
+
5
+ ```ascii
6
+ 1
7
+ __________|__________
8
+ / | \
9
+ 2 6 10
10
+ / | \ / | \ / | \
11
+ / | \ / | \ / | \
12
+ 3 4 5 7 8 9 11 12 13
13
+ ```
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bundle add pretty_tree
19
+ ```
20
+
21
+ Or, without Bundler:
22
+
23
+ ```bash
24
+ gem install pretty_tree
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ Out of the box, PrettyTree understands nested arrays, where each node is
30
+ `[value, *children]` and `nil` marks an empty child slot:
31
+
32
+ ```ruby
33
+ require "pretty_tree"
34
+
35
+ tree = ["a", ["b", nil, nil], ["c", ["d", nil, nil], nil]]
36
+
37
+ PrettyTree.print(tree)
38
+ # =>
39
+ # "a"
40
+ # / \
41
+ # / \
42
+ #"b" "c"
43
+ # /
44
+ # /
45
+ # "d"
46
+ ```
47
+
48
+ `PrettyTree.render(tree)` returns the diagram as a `String` instead of printing
49
+ it, for use in `#inspect` or logging:
50
+
51
+ ```ruby
52
+ def inspect
53
+ PrettyTree.render(self)
54
+ end
55
+ ```
56
+
57
+ ### Preview
58
+
59
+ `PrettyTree.preview` will print a collection of example trees as a preview, call it from `irb` or a script:
60
+
61
+ ```ruby
62
+ require "pretty_tree"
63
+
64
+ PrettyTree.preview
65
+ ```
66
+
67
+ ### Custom node types
68
+
69
+ Any tree shape can be rendered by providing an `Adapter` — an object that knows how to read a node's value and children:
70
+
71
+ ```ruby
72
+ class MyAdapter < PrettyTree::Adapter
73
+ def value(node) = node.name
74
+ def children(node) = node.kids
75
+ end
76
+
77
+ PrettyTree.render(my_tree, adapter: MyAdapter.new)
78
+ ```
79
+
80
+ An adapter must follow this contract:
81
+
82
+ - `value(node)` returns the node's value: any object your `Formatter` can turn
83
+ into a label.
84
+ - `children(node)` returns an object that responds to `#each` (an `Array`, a
85
+ `Set`, an `Enumerator`, any `Enumerable`), and never `nil`. It yields the
86
+ node's children in order, from left to right. Use `nil` for an empty slot: a
87
+ slot keeps its position, so `[left, nil]` and `[nil, right]` are different
88
+ trees. No children, or only `nil`s, makes the node a leaf.
89
+ - The adapter is never called with `nil`: an empty slot is drawn as empty space
90
+ without asking the adapter about it.
91
+ - Anything else is up to the adapter. PrettyTree does not look at the children
92
+ themselves, only at what `children` returns, and raises `PrettyTree::Error`
93
+ if that does not respond to `#each`.
94
+
95
+ A `Formatter` controls how a value is turned into a label (defaults to
96
+ `#inspect`). `label` receives the value and a `max_width:` keyword, which it can
97
+ use to shorten long labels, and returns the label; whatever it returns is
98
+ converted with `to_s`:
99
+
100
+ ```ruby
101
+ class MyFormatter < PrettyTree::Formatter
102
+ def label(value, max_width: nil) = value.to_s.upcase
103
+ end
104
+
105
+ PrettyTree.render(my_tree, formatter: MyFormatter.new)
106
+ ```
107
+
108
+ The structure must be a tree: PrettyTree does not detect cycles, so an adapter
109
+ whose `children` leads back to an ancestor raises `SystemStackError`.
110
+
111
+ ## Development
112
+
113
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
114
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
115
+ prompt.
116
+
117
+ To install this gem onto your local machine, run `bundle exec rake install`. To
118
+ release a new version, update the version number in `version.rb`, then run
119
+ `bundle exec rake release`.
120
+
121
+ `rake preview` will call `PrettyTree.preview`.
122
+
123
+ ## Contributing
124
+
125
+ Bug reports and pull requests are welcome on GitHub at
126
+ https://github.com/riccardo-giomi/pretty_tree.
127
+
128
+ ## License
129
+
130
+ The gem is available as open source under the terms of the
131
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ desc "Print example trees"
11
+ task :preview do
12
+ require_relative "lib/pretty_tree"
13
+ PrettyTree.preview
14
+ end
15
+
16
+ task default: %i[test standard]
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../adapter"
4
+
5
+ # Adapter for trees represented as nested arrays.
6
+ #
7
+ # Example, binary tree:
8
+ #
9
+ # c
10
+ # / \
11
+ # b d
12
+ # / / \
13
+ # a e f
14
+ #
15
+ # ["c",
16
+ # ["b",
17
+ # ["a", nil, nil]
18
+ # ],
19
+ # ["d",
20
+ # ["e", nil, nil],
21
+ # ["f", nil, nil]
22
+ # ]
23
+ # ]
24
+ #
25
+ module PrettyTree
26
+ class Adapter
27
+ class ArrayTree < PrettyTree::Adapter
28
+ def validate(node)
29
+ unless node.is_a?(Array) && !node.empty?
30
+ got = node.is_a?(Array) ? "[]" : node.class.name
31
+ raise PrettyTree::Error, "invalid tree node, expected non-empty Array, got #{got}"
32
+ end
33
+ end
34
+
35
+ def value(node)
36
+ validate(node)
37
+ node.first
38
+ end
39
+
40
+ def children(node)
41
+ validate(node)
42
+ children = node[1...]
43
+
44
+ children.each { |node| validate(node) unless node.nil? }
45
+
46
+ children
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reads a tree node's value and children.
4
+ #
5
+ # Adapters are never called with nil: the renderer treats a nil node as an empty
6
+ # slot and does not ask the adapter about it.
7
+ #
8
+ # #value(node) - anything the Formatter can turn into a label.
9
+ # #children(node) - responds to #each, never nil. Yields the child nodes in
10
+ # order, with nil marking an empty slot.
11
+ module PrettyTree
12
+ class Adapter
13
+ def value(node)
14
+ raise NotImplementedError, "#{self.class} must implement #value"
15
+ end
16
+
17
+ def children(node)
18
+ raise NotImplementedError, "#{self.class} must implement #children"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A rectangular block of text: some lines, all the same width, plus an `anchor
4
+ # column`. It represents a rendered sub-tree.
5
+ #
6
+ # The anchor column is the column (within that block) where the node's label is
7
+ # centered.
8
+ #
9
+ module PrettyTree
10
+ # lines: Array<String>, each exactly :width characters
11
+ # anchor: 0-indexed column
12
+ Box = Struct.new(:lines, :width, :anchor, :empty) do
13
+ def self.leaf(label, anchor: label.size / 2, empty: false)
14
+ new(lines: [label], width: label.size, anchor:, empty:)
15
+ end
16
+
17
+ # An empty box can still have a label if the layout renders it as empty
18
+ # spaces for spacing.
19
+ def self.empty(label, anchor: label.size / 2)
20
+ leaf(label, anchor:, empty: true)
21
+ end
22
+
23
+ def height = lines.size
24
+ def empty? = !!empty
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Defines how a node label is formatted in the string ouput.
4
+ #
5
+ # This is the default behaviour:
6
+ # - for labels call #inspect on the value, then truncate to min_width.
7
+ module PrettyTree
8
+ class Formatter
9
+ class Default < PrettyTree::Formatter
10
+ # Format as inspect, then truncate with "..." if max_width allows.
11
+ def label(value, max_width: nil)
12
+ text = value.inspect
13
+ return text if max_width.nil? || text.length <= max_width
14
+ return text[0, max_width] if max_width <= 3
15
+ "#{text[0, max_width - 3]}..."
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrettyTree
4
+ class Formatter
5
+ def label(value, max_width: nil)
6
+ raise NotImplementedError, "#{self.class} must implement #label"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "box"
4
+
5
+ module PrettyTree
6
+ module Layout
7
+ GAP = 1
8
+
9
+ def self.empty_box(parent_arity:)
10
+ layout_class_for(parent_arity).empty_box
11
+ end
12
+
13
+ def self.node_box(label, child_boxes, parent_arity:, position:)
14
+ return layout_class_for(parent_arity).leaf_box(label, position:) if child_boxes.all?(&:empty?)
15
+
16
+ label = Base.pad_label(label)
17
+
18
+ layout = layout_for(child_boxes)
19
+ lines, width, offsets = layout.merge
20
+
21
+ anchor = layout.label_anchor(offsets)
22
+ lines, width, offsets, anchor = layout.accommodate_parent_label(lines, width, offsets, anchor, label.length)
23
+
24
+ label_line = layout.render_label_line(label, width, anchor)
25
+ connector_lines = layout.render_connector_lines(label, anchor, offsets, width)
26
+
27
+ new_lines = [label_line] + connector_lines + lines
28
+ Box.new(lines: new_lines, width:, anchor:, empty: false)
29
+ end
30
+
31
+ def self.layout_class_for(size)
32
+ case size
33
+ when 1
34
+ Unary
35
+ when 2
36
+ Binary
37
+ when 3
38
+ Ternary
39
+ else
40
+ Generic
41
+ end
42
+ end
43
+
44
+ def self.layout_for(boxes)
45
+ layout_class_for(boxes.size).new(boxes)
46
+ end
47
+
48
+ class Base
49
+ MIN_LEAF_SIZE = 3 # Required to allow space for connector symbols
50
+
51
+ def self.pad_label(label) = label.to_s.center(MIN_LEAF_SIZE)
52
+
53
+ def self.leaf_box(label, position:) = Box.leaf(pad_label(label))
54
+ def self.empty_box = Box.empty(pad_label(nil))
55
+
56
+ def initialize(boxes)
57
+ @boxes = boxes
58
+ end
59
+
60
+ def merge
61
+ height = @boxes.map(&:height).max
62
+
63
+ lines = Array.new(height) { "" }
64
+ offsets = []
65
+ @boxes.each_with_index do |box, i|
66
+ offsets << lines.first.length + box.anchor
67
+
68
+ padded_lines(box, height).each_with_index do |line, j|
69
+ lines[j] += line
70
+ # Do not gap the last box
71
+ lines[j] += " " * GAP unless i == @boxes.size - 1
72
+ end
73
+ end
74
+
75
+ [lines, lines.first.length, offsets]
76
+ end
77
+
78
+ def padded_lines(box, height)
79
+ return box.lines if box.height == height
80
+
81
+ padding = Array.new(height - box.height) { " " * box.width }
82
+ box.lines + padding
83
+ end
84
+
85
+ def render_label_line(label, width, anchor)
86
+ label_start = anchor - label.length / 2
87
+ line = " " * width
88
+ line[label_start, label.length] = label
89
+ line
90
+ end
91
+
92
+ # Ensures that the box created by merging the children subtrees is not
93
+ # smaller than the parent label, including the fact that the label does not
94
+ # start from the beginning of its line.
95
+ def accommodate_parent_label(lines, width, offsets, anchor, label_length)
96
+ label_left = label_length / 2
97
+ label_right = label_length - label_left
98
+
99
+ left_pad = [label_left - anchor, 0].max
100
+ anchor += left_pad
101
+ right_pad = [anchor + label_right - (width + left_pad), 0].max
102
+
103
+ return [lines, width, offsets, anchor] if left_pad.zero? && right_pad.zero?
104
+
105
+ lines = lines.map { |line| (" " * left_pad) + line + (" " * right_pad) }
106
+ [lines, width + left_pad + right_pad, offsets.map { |o| o + left_pad }, anchor]
107
+ end
108
+
109
+ def render_left_branch(lines, parent_label, parent_anchor, offsets)
110
+ anchor = offsets.first
111
+ first_parent_label_char = parent_anchor - parent_label.length / 2
112
+ labels_distance = first_parent_label_char - (anchor + 2)
113
+ anchors_distance = parent_anchor - (anchor + 2)
114
+
115
+ if labels_distance > 1
116
+ lines[0][anchor + 2, anchors_distance + 1] = "_" * anchors_distance + "|"
117
+ else
118
+ lines[0][anchor + 2] = "/"
119
+ end
120
+ lines[1][anchor + 1] = "/"
121
+
122
+ lines
123
+ end
124
+
125
+ def render_middle_branch(lines, anchor)
126
+ lines[0][anchor] = "|"
127
+ lines[1][anchor] = "|"
128
+
129
+ lines
130
+ end
131
+
132
+ def render_right_branch(lines, parent_label, parent_anchor, offsets)
133
+ anchor = offsets.last
134
+ last_parent_label_char = parent_anchor + parent_label.length / 2
135
+ anchors_distance = anchor - 2 - parent_anchor
136
+ labels_distance = (anchor - 2) - last_parent_label_char
137
+ if labels_distance > 1
138
+ lines[0][parent_anchor, anchors_distance + 1] = "|" + "_" * anchors_distance
139
+ else
140
+ lines[0][anchor - 2] = "\\"
141
+ end
142
+ lines[1][anchor - 1] = "\\"
143
+
144
+ lines
145
+ end
146
+ end
147
+
148
+ class Unary < Base
149
+ def label_anchor(offsets)
150
+ offsets.first
151
+ end
152
+
153
+ def render_connector_lines(_parent_label, _parent_anchor, offsets, width)
154
+ lines = Array.new(2) { " " * width }
155
+
156
+ lines = render_middle_branch(lines, offsets.first) unless @boxes.first.empty?
157
+
158
+ lines
159
+ end
160
+ end
161
+
162
+ class Binary < Base
163
+ CONNECTOR_SPAN = 3
164
+ MIN_ANCHORS_DISTANCE = 2 * CONNECTOR_SPAN
165
+
166
+ # Binary leaves require more space for connector symbols
167
+ def self.leaf_box(label, position:)
168
+ padded = pad_label(label)
169
+ return Box.leaf(padded + " ", anchor: padded.size / 2) if position == 0
170
+
171
+ Box.leaf(" " + padded, anchor: padded.size / 2 + 1)
172
+ end
173
+
174
+ def self.empty_box = Box.empty("")
175
+
176
+ def label_anchor(offsets)
177
+ return offsets.last - CONNECTOR_SPAN if @boxes.first.empty?
178
+ return offsets.first + CONNECTOR_SPAN if @boxes.last.empty?
179
+
180
+ offsets.first + (offsets.last - offsets.first) / 2
181
+ end
182
+
183
+ def merge
184
+ lines, width, offsets = super
185
+ return [lines, width, offsets] if @boxes.any?(&:empty?)
186
+
187
+ spread = MIN_ANCHORS_DISTANCE - (offsets.last - offsets.first)
188
+ return [lines, width, offsets] unless spread.positive?
189
+
190
+ lines = lines.map { |line| line.dup.insert(@boxes.first.width, " " * spread) }
191
+ [lines, width + spread, [offsets.first, offsets.last + spread]]
192
+ end
193
+
194
+ def render_connector_lines(parent_label, parent_anchor, offsets, width)
195
+ lines = Array.new(2) { " " * width }
196
+
197
+ lines = render_left_branch(lines, parent_label, parent_anchor, offsets) unless @boxes.first.empty?
198
+ lines = render_right_branch(lines, parent_label, parent_anchor, offsets) unless @boxes.last.empty?
199
+
200
+ lines
201
+ end
202
+ end
203
+
204
+ class Ternary < Base
205
+ def label_anchor(offsets)
206
+ offsets[1]
207
+ end
208
+
209
+ def render_connector_lines(parent_label, parent_anchor, offsets, width)
210
+ lines = Array.new(2) { " " * width }
211
+
212
+ lines = render_left_branch(lines, parent_label, parent_anchor, offsets) unless @boxes.first.empty?
213
+ lines = render_middle_branch(lines, offsets[1]) unless @boxes[1].empty?
214
+ lines = render_right_branch(lines, parent_label, parent_anchor, offsets) unless @boxes.last.empty?
215
+
216
+ lines
217
+ end
218
+ end
219
+
220
+ class Generic < Base
221
+ def initialize(boxes)
222
+ super(boxes.reject(&:empty?))
223
+ end
224
+
225
+ def label_anchor(offsets)
226
+ middle_or_middle_right_index = offsets.size / 2
227
+ middle_or_middle_right_offset = offsets[middle_or_middle_right_index]
228
+
229
+ return middle_or_middle_right_offset if offsets.size.odd?
230
+
231
+ middle_left_offset = offsets[middle_or_middle_right_index - 1]
232
+
233
+ middle_left_offset + ((middle_or_middle_right_offset - middle_left_offset) / 2)
234
+ end
235
+
236
+ def render_connector_lines(parent_label, parent_anchor, offsets, width)
237
+ lines = Array.new(2) { " " * width }
238
+
239
+ return render_middle_branch(lines, offsets.first) if offsets.size == 1
240
+
241
+ # Single line from (first adjusted anchor + 1) to (last adjusted anchor - 1)
242
+ from = offsets.first + 1
243
+ to = offsets.last
244
+ lines[0][from...to] = "_" * (to - from)
245
+ # With a single | breaking the line under the parent anchor
246
+ lines[0][parent_anchor] = "|"
247
+
248
+ # Second line is just a "|" over each adjusted anchor
249
+ offsets.each do |offset|
250
+ lines[1][offset] = "|"
251
+ end
252
+
253
+ lines
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrettyTree
4
+ class Preview
5
+ def initialize(...)
6
+ @renderer = Renderer.new(...)
7
+ end
8
+
9
+ def render(tree)
10
+ puts
11
+ puts tree.inspect
12
+ puts
13
+ puts @renderer.call(tree)
14
+ puts
15
+ end
16
+
17
+ def run
18
+ render(nil)
19
+ render(["a"])
20
+ render(["morethan10chars"])
21
+ render([1, [2, nil, nil]])
22
+ render([1, [2, [3, nil, nil]]])
23
+ render(["a", ["b", nil, nil], nil])
24
+ render(["a", nil, ["c", nil, nil]])
25
+ render(["a", ["b", nil, nil], ["c", nil, nil]])
26
+ render([1, [2, [3, [4]]], [5, [6]]])
27
+ render(["a", ["b", nil, nil], ["c", nil, nil], ["d", nil, nil]])
28
+ render([1, ["a", ["b", nil, nil], ["c", nil, nil], ["d", nil, nil]], nil])
29
+ render([1, nil, ["a", ["b", nil, nil], ["c", nil, nil], ["d", nil, nil]]])
30
+ render([1, nil, ["a", ["b", nil, nil], ["c", nil, nil], ["d", nil, nil]], nil])
31
+ render([123456789012, [2, nil, nil], [6, nil, nil], [7, nil, nil]])
32
+ render([1, ["a", ["b", nil, nil], ["c", nil, nil]], ["d", ["e", nil, nil], ["f", nil, nil]]])
33
+ render([1, ["a", ["b", nil, nil], ["c", nil, nil], ["d", nil, nil]], ["e", ["f", nil, nil], ["g", nil, nil], ["h", nil, nil]]])
34
+ render([1, [2, [3, nil, nil], [4, nil, nil], [5, nil, nil]], [6, [7, nil, nil], [8, nil, nil], [9, nil, nil]], [10, [11, nil, nil], [12, nil, nil], [13, nil, nil]]])
35
+ render([1, [2, nil, nil], [6, [7, nil, nil], [8, nil, nil], [9, nil, nil]], [10, [11, nil, nil], [12, nil, nil], [13, nil, nil]]])
36
+ render([1337, ["aaaa", ["bbbb", nil, nil], ["cccc", nil, nil], ["dddd", nil, nil]], ["eeee", ["ffff", nil, nil], ["gggg", nil, nil], ["hhhh", nil, nil]]])
37
+ render(["a" * 1, ["b" * 10, nil, nil], ["c" * 10, nil, nil]])
38
+ render(["a" * 10, ["b" * 10, nil, nil], ["c" * 10, nil, nil]])
39
+ render(["a" * 1, ["b" * 10, ["c" * 10, nil, nil], ["d" * 10, nil, nil]], ["e" * 10, ["f" * 10, nil, nil], ["g" * 10, nil, nil]]])
40
+ render(["a" * 10, ["b" * 10, ["c" * 10, nil, nil], ["d" * 10, nil, nil]], ["e" * 10, ["f" * 10, nil, nil], ["g" * 10, nil, nil]]])
41
+ render(["root", ["leftlongsubtree", ["a", nil, nil], ["b", nil, nil]], ["r", nil, nil]])
42
+ render(["root", ["l", nil, nil], ["rightlongsubtree", ["a", nil, nil], ["b", nil, nil]]])
43
+ render(["root", ["leftlongsubtree", ["a", nil, nil], ["b", nil, nil]], ["m", nil, nil], ["rightlongsubtree", ["c", nil, nil], ["d", nil, nil]]])
44
+ render([786494, ["dvy", nil, nil, ["", nil, [true, nil, nil], ["cmv", nil]]]])
45
+ render(["root", [1, nil, nil, nil, nil]])
46
+ render(["root", [1, nil, nil, nil, nil], [2, nil, nil, nil, nil], [3, nil, nil, nil, nil], [4, nil, nil, nil, nil]])
47
+ render(["root", [1, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]], [2, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]], [3, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]], [4, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]]])
48
+ render(["root", [1, nil, nil, nil, nil, nil], [2, nil, nil, nil, nil, nil], [3, nil, nil, nil, nil, nil], [4, nil, nil, nil, nil, nil], [5, nil, nil, nil, nil, nil]])
49
+ render(["root", [1, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]], [2, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]], [3, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]], [4, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]], [5, ["a", ["A"]], ["b", ["B"]], ["c", ["C"]]]])
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "layout"
4
+
5
+ module PrettyTree
6
+ class Renderer
7
+ LABEL_WIDTH = 12
8
+
9
+ def initialize(adapter:, formatter:, max_width: LABEL_WIDTH)
10
+ @adapter = adapter
11
+ @formatter = formatter
12
+ @max_width = max_width
13
+ end
14
+
15
+ def call(tree) = box_for(tree).lines.join("\n")
16
+
17
+ def box_for(node, parent_arity: 1, position: 0)
18
+ return Layout.empty_box(parent_arity:) if node.nil?
19
+
20
+ label = @formatter.label(@adapter.value(node), max_width: @max_width)
21
+ children = @adapter.children(node)
22
+
23
+ raise Error, "children must respond to #each" unless children.respond_to?(:each)
24
+
25
+ # Visit all children, merge getting back from the recursion
26
+ children = children.to_a
27
+ child_boxes = children.each_with_index.map do |node, i|
28
+ box_for(node, parent_arity: children.size, position: i)
29
+ end
30
+
31
+ Layout.node_box(label, child_boxes, parent_arity:, position:)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrettyTree
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pretty_tree/version"
4
+
5
+ require_relative "pretty_tree/adapter"
6
+ require_relative "pretty_tree/adapter/array_tree"
7
+ require_relative "pretty_tree/formatter"
8
+ require_relative "pretty_tree/formatter/default"
9
+
10
+ require_relative "pretty_tree/renderer"
11
+
12
+ module PrettyTree
13
+ class Error < StandardError; end
14
+
15
+ def self.render(tree, adapter: Adapter::ArrayTree.new, formatter: Formatter::Default.new)
16
+ Renderer.new(adapter:, formatter:).call(tree)
17
+ end
18
+
19
+ def self.print(tree, adapter: Adapter::ArrayTree.new, formatter: Formatter::Default.new)
20
+ puts render(tree, adapter:, formatter:)
21
+ end
22
+
23
+ # Prints some example trees as a preview of the output.
24
+ def self.preview(adapter: Adapter::ArrayTree.new, formatter: Formatter::Default.new)
25
+ require_relative "pretty_tree/preview"
26
+ Preview.new(adapter:, formatter:).run
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pretty_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Riccardo Giomi
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ PrettyTree renders tree-shaped data as readable ASCII diagrams.
14
+
15
+ Useful in #inspect output, log lines, or anywhere you want to see a tree's structure at a glance instead of a nested inspect dump.
16
+
17
+ Works out of the box with nested-array trees, and extends to any node type via a small Adapter/Formatter interface.
18
+ email:
19
+ - riccardo.giomi.contact@gmail.com
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - CHANGELOG.md
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - lib/pretty_tree.rb
29
+ - lib/pretty_tree/adapter.rb
30
+ - lib/pretty_tree/adapter/array_tree.rb
31
+ - lib/pretty_tree/box.rb
32
+ - lib/pretty_tree/formatter.rb
33
+ - lib/pretty_tree/formatter/default.rb
34
+ - lib/pretty_tree/layout.rb
35
+ - lib/pretty_tree/preview.rb
36
+ - lib/pretty_tree/renderer.rb
37
+ - lib/pretty_tree/version.rb
38
+ homepage: https://github.com/riccardo-giomi/pretty_tree
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://github.com/riccardo-giomi/pretty_tree
43
+ source_code_uri: https://github.com/riccardo-giomi/pretty_tree
44
+ changelog_uri: https://github.com/riccardo-giomi/pretty_tree/blob/master/CHANGELOG.md
45
+ rubygems_mfa_required: 'true'
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.3'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 4.0.22
61
+ specification_version: 4
62
+ summary: Renders a tree as a string, prettily.
63
+ test_files: []