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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/Rakefile +16 -0
- data/lib/pretty_tree/adapter/array_tree.rb +50 -0
- data/lib/pretty_tree/adapter.rb +21 -0
- data/lib/pretty_tree/box.rb +26 -0
- data/lib/pretty_tree/formatter/default.rb +19 -0
- data/lib/pretty_tree/formatter.rb +9 -0
- data/lib/pretty_tree/layout.rb +257 -0
- data/lib/pretty_tree/preview.rb +52 -0
- data/lib/pretty_tree/renderer.rb +34 -0
- data/lib/pretty_tree/version.rb +5 -0
- data/lib/pretty_tree.rb +28 -0
- metadata +63 -0
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,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
|
data/lib/pretty_tree.rb
ADDED
|
@@ -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: []
|