Add a `CommandKit::Printing::Trees` module
Add a module which adds a print_tree method that prints Arrays of Arrays or Hashes of Hashes/Arrays in a vertical tree. It must support ANSI formatting.
Examples
ANSI
my-app/
├─ node_modules/
├─ public/
│ ├─ favicon.ico
│ ├─ index.html
│ ├─ robots.txt
├─ src/
│ ├─ index.css
│ ├─ index.js
├─ .gitignore
├─ package.json
├─ README.md
@postmodern I'd like to contribute this feature. I have a working recursive method for printing this tree output. It gets involved though with the different formats that can be provided. I think it would benefit from having an expected input. Can you please provide some samples of "Arrays of Arrays or Hashes of Hashes/Arrays" that you would expect to work with this method? That way I can get an idea of how best to implement support.
@javierjulio for Arrays of Arrays, I foresee two formats:
Arrays of Arrays
Arrays of Arrays (Format A)
[
"One",
"Two",
"Three",
[
"Foo",
"Bar"
]
]
Which would be formatted as:
* One
* Two
* Three
* Foo
* Bar
Arrays of Arrays (Format B)
[
"One",
"Two",
[
"Three",
[
"Foo",
"Bar"
]
]
]
Which would be formatted as:
* One
* Two
* Three
* Foo
* Bar
Arrays of Hashes / Hashes of Hashes
Supporting Hashes would be a bit more tricky, as the keys could represent the parent list item and the value(s) for the key would represent the sub-list items.
Array of Hashes (Example)
[
"One",
"Two",
{
"Three" => [
"Foo",
"Bar"
]
}
]
Hash of Hashes (Example)
{
"One" => [],
"Two" => [],
"Three" => [
"Foo",
"Bar"
]
}
Imo, supporting Arrays of Arrays would be easier to implement and more closely mirrors the item of displaying lists and sub-lists.
@postmodern thank you. Taking that in along with the original content into consideration, to handle both a root or multi top level items, I'd recommend using array pairs of name and children. It's a little verbose but seems to be common and would keep things predictable/consistent when generating the expected output.
So take the following data samples:
tree1 = [
["root", [
["branch1", [
["leaf1", []],
["leaf2", []]
]],
["branch2", [
["leaf3", []],
["subbranch", [
["leaf4", []]
]]
]]
]]
]
tree2 = [
[ "A", [] ],
[ "B", [
[ "B1", [] ],
[ "B2", [
[ "B2a", [] ],
[ "B2b", [] ]
] ]
] ],
[ "C", [
[ "C1", [] ],
[ "C2", [
[ "C2a", [] ],
[ "C2b", [
[ "C2b1", [] ]
] ]
] ]
]
]]
They would end up printing:
# Tree 1
└─ root
├─ branch1
│ ├─ leaf1
│ └─ leaf2
└─ branch2
├─ leaf3
└─ subbranch
└─ leaf4
# Tree 2
├─ A
├─ B
│ ├─ B1
│ └─ B2
│ ├─ B2a
│ └─ B2b
└─ C
├─ C1
└─ C2
├─ C2a
└─ C2b
└─ C2b1
Would that be acceptable? I can start a PR with these changes.
@javierjulio those representations of trees look a bit too complex. One shouldn't have to define two arrays for just a single line of the tree-list output. Ideally a single line without any children should be a single String. If a line has children, then we should consider either making it an Array or having the Array follow the String element, like in my examples posted above.
That can be done, it's just tricky because it has an implicit tree structure since any time there's an array element following a non-array element, the latter has to be treated as the parent so that has to be tracked. I have something working but have concerns around the root handling.
What do you expect for the output and handling of the root element? Should it be enforced that the top level of the array follow the format below? Meaning top level its expected to be either one element (string) or two (string, array) but no more.
[
'Root',
[ 'A', 'B', ... ]
]
@javierjulio I'm still not sure whether to support ['Has-no-children', 'Parent1', ['Child', ...], 'Has-no-children', ...] style where the Array of children follows the parent String or [ 'Has-no-children', ['Parent1', ['Child', ...]], 'Has-no-children', ...] style where the children are grouped with the parent String into a two element Array. I just want to avoid having to explicitly specify ..., 'Parent', [], ... or [..., ['Parent', []], ...] just to print a line with no children, as that seems excessive. I would prefer to specify a single line with no children as just a String.
If we decide to go with Arrays that follow the parent String, then maybe Array#each_cons(2) could be used? If we decide to group the children elements with the parent String into a two element Array, then maybe we could use splatting to unpack the String or String + Array of children:
list.each do |element|
value, *children = element
if children.empty?
# print a single line with no children
else
# print a single line and then print the children
end
end
I think if you go with the latter it would be best to stick with the array approach outlined in https://github.com/postmodern/command_kit.rb/issues/67#issuecomment-2908621885 which makes the pattern clear and consistent in handling as everything is a node.
For the two cases you outlined in https://github.com/postmodern/command_kit.rb/issues/67#issuecomment-2914801754 the handling gets weird but for the former it's not as bad for the implicit case I think. This is what I left off with as a POC for what you requested there.
def print_tree(node, prefix = "", is_last = true)
return if node.nil?
if node.is_a?(Array)
i = 0
while i < node.size
item = node[i]
child = node[i + 1]
has_children = child.is_a?(Array)
connector = (i + 1 == node.size) ? "└─ " : "├─ "
puts "#{prefix}#{connector}#{item}"
if has_children
new_prefix = (connector == "└─ ") ? prefix + " " : prefix + "│ "
print_tree(child, new_prefix)
i += 1 # Skip over the child array
end
i += 1
end
else
puts "#{prefix}#{is_last ? '└─ ' : '├─ '}#{node}"
end
end
tree_data = [
'Root',
[
'A',
'B',
'C',
[
'D',
'E',
[
1,
2,
[
'A',
'B',
'C'
],
4,
5
]
],
'F',
'G',
],
]
puts tree_data[0]
print_tree(tree_data[1])
The output for that is:
Root
├─ A
├─ B
├─ C
│ ├─ D
│ ├─ E
│ │ ├─ 1
│ │ ├─ 2
│ │ │ ├─ A
│ │ │ ├─ B
│ │ │ └─ C
│ │ ├─ 4
│ │ └─ 5
├─ F
└─ G
@javierjulio very cool! I would recommend using if/else instead of ternary operators.
value = if foo then value1
else value2
end
if foo
puts ...
else
puts ...
end
I also wonder if Array#each_cons(2) could be used instead of a while loop with indices?
[1,2,3,4,5].each_cons(2) { |i,j| p [i, j] }
# [1, 2]
# [2, 3]
# [3, 4]
# [4, 5]
.with_index could also be added if you needed the index of the current node:
%w[A B C D E].each_cons(2).with_index do |(node,next_node),index|
puts "node=#{node}, next_node=#{next_node}, index=#{index}"
end
# node=A, next_node=B, index=0
# node=B, next_node=C, index=1
# node=C, next_node=D, index=2
# node=D, next_node=E, index=3
Would still need to handle the two edge-cases of the Array being empty or only containing one element.
You may be better off implementing this yourself with an each_cons(2).with_index do |(node,next_node),index| block approach if preferred. I'm not sure there is much benefit over using a while loop. You should be able to repurpose what's there to use it. It would trade off edge cases where with the former you'd have to render twice for the last item and then the index will be off by 1 twice to compare to array size (e.g. index + 2 == array.size) to determine if is last. Another issue is if there isn't enough elements, e.g. only 1 element, then each_cons does nothing.