giblish 0.8.2 → 2.0.0.pre.alpha1

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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/unit_tests.yml +30 -0
  3. data/.gitignore +7 -3
  4. data/.ruby-version +1 -1
  5. data/Changelog.adoc +59 -0
  6. data/README.adoc +261 -0
  7. data/docs/concepts/text_search.adoc +213 -0
  8. data/docs/concepts/text_search_im/cgi-search_request.puml +35 -0
  9. data/docs/concepts/text_search_im/cgi-search_request.svg +397 -0
  10. data/docs/concepts/text_search_im/search_request.puml +40 -0
  11. data/docs/concepts/text_search_im/search_request.svg +408 -0
  12. data/docs/howtos/trigger_generation.adoc +180 -0
  13. data/docs/{setup_server_assets → howtos/trigger_generation_im}/Render Documents.png +0 -0
  14. data/docs/{setup_server_assets → howtos/trigger_generation_im}/View Documents.png +0 -0
  15. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_hooks.graphml +0 -0
  16. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_hooks.svg +0 -0
  17. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_jenkins.graphml +0 -0
  18. data/docs/{setup_server_assets → howtos/trigger_generation_im}/deploy_with_jenkins.svg +0 -0
  19. data/docs/howtos/trigger_generation_im/docgen_github.puml +51 -0
  20. data/docs/{setup_server_assets → howtos/trigger_generation_im}/giblish_deployment.graphml +0 -0
  21. data/docs/howtos/trigger_generation_im/post-receive-example.sh +50 -0
  22. data/docs/reference/box_flow_spec.adoc +22 -0
  23. data/docs/reference/search_spec.adoc +185 -0
  24. data/giblish.gemspec +54 -32
  25. data/lib/giblish/adocsrc_providers.rb +23 -0
  26. data/lib/giblish/application.rb +214 -41
  27. data/lib/giblish/cmdline.rb +273 -259
  28. data/lib/giblish/config_utils.rb +41 -0
  29. data/lib/giblish/configurator.rb +163 -0
  30. data/lib/giblish/conversion_info.rb +120 -0
  31. data/lib/giblish/docattr_providers.rb +125 -0
  32. data/lib/giblish/docid/docid.rb +181 -0
  33. data/lib/giblish/github_trigger/webhook_manager.rb +64 -0
  34. data/lib/giblish/gitrepos/checkoutmanager.rb +124 -0
  35. data/lib/giblish/{gititf.rb → gitrepos/gititf.rb} +30 -4
  36. data/lib/giblish/gitrepos/gitsummary.erb +61 -0
  37. data/lib/giblish/gitrepos/gitsummaryprovider.rb +78 -0
  38. data/lib/giblish/gitrepos/history_pb.rb +41 -0
  39. data/lib/giblish/indexbuilders/d3treegraph.rb +88 -0
  40. data/lib/giblish/indexbuilders/depgraphbuilder.rb +109 -0
  41. data/lib/giblish/indexbuilders/dotdigraphadoc.rb +174 -0
  42. data/lib/giblish/indexbuilders/standard_index.erb +10 -0
  43. data/lib/giblish/indexbuilders/subtree_indices.rb +132 -0
  44. data/lib/giblish/indexbuilders/templates/circles.html.erb +111 -0
  45. data/lib/giblish/indexbuilders/templates/flame.html.erb +61 -0
  46. data/lib/giblish/indexbuilders/templates/tree.html.erb +366 -0
  47. data/lib/giblish/indexbuilders/templates/treemap.html.erb +127 -0
  48. data/lib/giblish/indexbuilders/verbatimtree.rb +94 -0
  49. data/lib/giblish/pathtree.rb +473 -74
  50. data/lib/giblish/resourcepaths.rb +150 -0
  51. data/lib/giblish/search/expand_adoc.rb +55 -0
  52. data/lib/giblish/search/headingindexer.rb +312 -0
  53. data/lib/giblish/search/request_manager.rb +110 -0
  54. data/lib/giblish/search/searchquery.rb +68 -0
  55. data/lib/giblish/search/textsearcher.rb +349 -0
  56. data/lib/giblish/subtreeinfobuilder.rb +77 -0
  57. data/lib/giblish/treeconverter.rb +272 -0
  58. data/lib/giblish/utils.rb +142 -294
  59. data/lib/giblish/version.rb +1 -1
  60. data/lib/giblish.rb +10 -7
  61. data/scripts/hooks/post-receive.example +66 -0
  62. data/{docgen/scripts/githook_examples → scripts/hooks}/post-update.example +0 -0
  63. data/{docgen → scripts}/resources/css/adoc-colony.css +0 -0
  64. data/scripts/resources/css/giblish-serif.css +419 -0
  65. data/scripts/resources/css/giblish.css +1979 -419
  66. data/{docgen → scripts}/resources/fonts/Ubuntu-B.ttf +0 -0
  67. data/{docgen → scripts}/resources/fonts/Ubuntu-BI.ttf +0 -0
  68. data/{docgen → scripts}/resources/fonts/Ubuntu-R.ttf +0 -0
  69. data/{docgen → scripts}/resources/fonts/Ubuntu-RI.ttf +0 -0
  70. data/{docgen → scripts}/resources/fonts/mplus1p-regular-fallback.ttf +0 -0
  71. data/{docgen → scripts}/resources/images/giblish_logo.png +0 -0
  72. data/{docgen → scripts}/resources/images/giblish_logo.svg +0 -0
  73. data/{docgen → scripts}/resources/themes/giblish.yml +0 -0
  74. data/scripts/wserv_development.rb +32 -0
  75. data/web_apps/cgi_search/gibsearch.rb +43 -0
  76. data/web_apps/gh_webhook_trigger/config.ru +2 -0
  77. data/web_apps/gh_webhook_trigger/gh_webhook_trigger.rb +73 -0
  78. data/web_apps/gh_webhook_trigger/public/dummy.txt +3 -0
  79. data/web_apps/sinatra_search/config.ru +2 -0
  80. data/web_apps/sinatra_search/public/dummy.txt +3 -0
  81. data/web_apps/sinatra_search/sinatra_search.rb +34 -0
  82. data/web_apps/sinatra_search/tmp/restart.txt +0 -0
  83. metadata +188 -85
  84. data/.rubocop.yml +0 -7
  85. data/.travis.yml +0 -3
  86. data/Changelog +0 -16
  87. data/Gemfile +0 -4
  88. data/README.adoc +0 -1
  89. data/Rakefile +0 -41
  90. data/bin/console +0 -14
  91. data/bin/setup +0 -8
  92. data/data/testdocs/malformed/no_header.adoc +0 -5
  93. data/data/testdocs/toplevel.adoc +0 -19
  94. data/data/testdocs/wellformed/adorned_purpose.adoc +0 -17
  95. data/data/testdocs/wellformed/docidtest/docid_1.adoc +0 -24
  96. data/data/testdocs/wellformed/docidtest/docid_2.adoc +0 -8
  97. data/data/testdocs/wellformed/simple.adoc +0 -14
  98. data/data/testdocs/wellformed/source_highlighting/highlight_source.adoc +0 -38
  99. data/docgen/resources/css/giblish.css +0 -1979
  100. data/docgen/scripts/Jenkinsfile +0 -18
  101. data/docgen/scripts/gen_adoc_org.sh +0 -58
  102. data/docs/README.adoc +0 -387
  103. data/docs/setup_server.adoc +0 -202
  104. data/lib/giblish/buildgraph.rb +0 -216
  105. data/lib/giblish/buildindex.rb +0 -459
  106. data/lib/giblish/core.rb +0 -451
  107. data/lib/giblish/docconverter.rb +0 -308
  108. data/lib/giblish/docid.rb +0 -180
  109. data/lib/giblish/docinfo.rb +0 -75
  110. data/lib/giblish/indexheadings.rb +0 -251
  111. data/lib/giblish-search.cgi +0 -459
  112. data/scripts/hooks/post-receive +0 -57
  113. data/scripts/publish_html.sh +0 -99
@@ -1,119 +1,518 @@
1
- # This class can convert the following paths:
1
+ require "pathname"
2
+ require "set"
3
+
4
+ #
5
+ # Provides a tree structure where each node is the basename of either
6
+ # a directory or a file. The pathname of a node is the concatenation of
7
+ # all basenames from the root node to the node in question, given as a
8
+ # Pathname object.
9
+ #
10
+ # Each node must have a unique pathname within the tree it is part of.
11
+ #
12
+ # A node can contain an associated 'data' object.
13
+ #
14
+ # The following paths:
2
15
  # basedir/file_1
3
16
  # basedir/file_2
4
17
  # basedir/dir1/file_3
5
18
  # basedir/dir1/file_4
6
- # basedir2/dir2/dir3/file_5
19
+ # basedir/dir2/dir3/file_5
20
+ #
21
+ # are thus represented by the following path tree:
7
22
  #
8
- # into the following tree:
9
23
  # basedir
10
24
  # file_1
11
25
  # file_2
12
26
  # dir1
13
27
  # file_3
14
28
  # file_4
15
- # basedir2
16
29
  # dir2
17
30
  # dir3
18
31
  # file_5
32
+ #
33
+ # == Tree info
34
+ # see https://www.geeksforgeeks.org/tree-traversals-inorder-preorder-and-postorder/
35
+ #
19
36
  class PathTree
20
- attr_reader :name, :data
37
+ attr_reader :data, :name, :children, :parent, :abs_root
38
+ attr_writer :parent, :data
39
+
40
+ def initialize(path, data = nil, parent = nil)
41
+ p = clean(path)
42
+ raise ArgumentError, "Can not instantiate node with path == '.'" if p.to_s == "."
43
+ raise ArgumentError, "Trying to create a non-root node using an absolute path" if p.absolute? && !parent.nil?
44
+
45
+ head = p.descend.first
21
46
 
22
- def initialize(tail = nil, data = nil)
47
+ @name = head
23
48
  @children = []
24
- @it = nil
25
- @name = nil
26
- return unless tail
27
-
28
- tail = tail.split("/") unless tail.is_a?(Array)
29
- @name = tail.shift
30
- if tail.length.positive?
31
- @children << PathTree.new(tail, data)
32
- else
49
+ @data = nil
50
+ @parent = parent
51
+
52
+ tail = p.relative_path_from(head)
53
+ if tail.to_s == "."
33
54
  @data = data
55
+ return
34
56
  end
57
+
58
+ add_descendants(tail, data)
35
59
  end
36
60
 
37
- def add_path(tail, data = nil)
38
- tail = tail.split("/") unless tail.is_a?(Array)
39
- return if tail.empty?
61
+ # duplicate this node and all its children but keep the same data references
62
+ # as the originial nodes.
63
+ #
64
+ # parent:: the parent node of the copy, default = nil (the copy
65
+ # is a root node)
66
+ # returns:: a copy of this node and all its descendents. The copy will
67
+ # share any 'data' references with the original.
68
+ def dup(parent: nil)
69
+ d = PathTree.new(@name.dup, @data, parent)
40
70
 
41
- ch = get_child tail[0]
42
- if ch
43
- tail.shift
44
- ch.add_path tail, data
45
- else
46
- @children << PathTree.new(tail, data)
71
+ @children.each { |c| d.children << c.dup(parent: d) }
72
+ d
73
+ end
74
+
75
+ def name=(name)
76
+ name = Pathname.new(name)
77
+
78
+ if !parent.nil? && @parent.children.any? { |c| c.name == name }
79
+ raise ArgumentError, "Can not rename to #{name}. An existing node already use that name"
80
+ end
81
+
82
+ @name = name
83
+ end
84
+
85
+ # return:: a String with the path segment for this node
86
+ def segment
87
+ @name.to_s
88
+ end
89
+
90
+ # return:: a Pathname with the complete path from the root of the
91
+ # tree where this node is a member to this node (inclusive).
92
+ def pathname
93
+ return @name if @parent.nil?
94
+
95
+ (@parent.pathname / @name).cleanpath
96
+ end
97
+
98
+ # create a subtree from the given path and add it to this node
99
+ #
100
+ # return:: the leaf node for the added subtree
101
+ def add_descendants(path, data = nil)
102
+ p = clean(path)
103
+ raise ArgumentError, "Can not add absolute path as descendant!!" if p.absolute?
104
+
105
+ # invoked with 'current' name, ignore
106
+ return self if p.to_s == "."
107
+
108
+ head = p.descend.first
109
+ tail = p.relative_path_from(head)
110
+ last_segment = tail.to_s == "."
111
+
112
+ ch = get_child(head)
113
+ if ch.nil?
114
+ @children << PathTree.new(head, last_segment ? data : nil, self)
115
+ ch = @children.last
116
+ end
117
+
118
+ last_segment ? @children.last : ch.add_descendants(tail, data)
119
+ end
120
+
121
+ # adds a new path to the root of the tree where this node is a member
122
+ # and associates the given data to the leaf of that path.
123
+ def add_path(path, data = nil)
124
+ p = clean(path)
125
+ raise ArgumentError, "Trying to add already existing path: #{path}" unless node(p, from_root: true).nil?
126
+
127
+ # prune any part of the given path that already exists in this
128
+ # tree
129
+ p.ascend do |q|
130
+ n = node(q, from_root: true)
131
+ next if n.nil?
132
+
133
+ t = PathTree.new(p.relative_path_from(q).to_s, data)
134
+ n.append_tree(t)
135
+ return self
136
+ end
137
+
138
+ # no part of the given path existed within the tree
139
+ raise ArgumentError, "Trying to add path with other root is not supported"
140
+ end
141
+
142
+ # Visits depth-first by root -> left -> right
143
+ #
144
+ # level:: the number of hops from the root node
145
+ # block:: the user supplied block that is executed for every visited node
146
+ #
147
+ # the level and node are given as block parameters
148
+ #
149
+ # === Returns
150
+ # A new array containing the values returned by the block
151
+ #
152
+ # === Examples
153
+ # Get an array with name of each node together with the level of the node
154
+ # traverse_preorder{ |level, n| "#{level} #{n.segment}" }
155
+ #
156
+ def traverse_preorder(level = 0, &block)
157
+ result = Array[yield(level, self)]
158
+ @children.each do |c|
159
+ result.append(*c.traverse_preorder(level + 1, &block))
160
+ end
161
+ result
162
+ end
163
+
164
+ # Visits depth-first by left -> right -> root
165
+ #
166
+ # level:: the number of hops from the root node
167
+ # block:: the user supplied block that is executed for every visited node
168
+ #
169
+ # the level and node are given as block parameters
170
+ #
171
+ # === Returns
172
+ # A new array containing the values returned by the block
173
+ #
174
+ # === Examples
175
+ #
176
+ # Get an array of each node together with the level of the node
177
+ # traverse_postorder{ |level, n| "#{level} #{n.segment}" }
178
+ def traverse_postorder(level = 0, &block)
179
+ result = []
180
+ @children.each do |c|
181
+ result.concat(c.traverse_postorder(level + 1, &block))
47
182
  end
183
+ result << yield(level, self)
48
184
  end
49
185
 
50
- # Public: Visits each node by following each branch down from the
51
- # root, one at the time.
186
+ # Visits bredth-first left -> right for each level top-down
187
+ #
188
+ # level:: the number of hops from the root node
189
+ # block:: the user supplied block that is executed for every visited node
190
+ #
191
+ # the level and node are given as block parameters
52
192
  #
53
- # level - the number of hops from the root node
54
- # block - the user supplied block that is executed for every visited node
55
- # the level and node are given as block parameters
193
+ # === Returns
194
+ # A new array containing the values returned by the block
56
195
  #
57
- # Examples
58
- # Print the name of each node together with the level of the node
59
- # traverse_top_down{ |level, n| puts "#{level} #{n.name}" }
60
- def traverse_top_down(level = 0, &block)
196
+ # === Examples
197
+ # Get an array with the name of each node together with the level of the node
198
+ # traverse_levelorder { |level, n| "#{level} #{n.segment}" }
199
+ def traverse_levelorder(level = 0, &block)
200
+ result = []
201
+ # the node of the original call
202
+ result << yield(level, self) if level == 0
203
+
204
+ # this level
205
+ @children.each do |c|
206
+ result << yield(level + 1, c)
207
+ end
208
+
209
+ # next level
61
210
  @children.each do |c|
62
- yield(level, c)
63
- c.traverse_top_down(level + 1, &block)
211
+ result.concat(c.traverse_levelorder(level + 1, &block))
64
212
  end
213
+
214
+ result
65
215
  end
66
216
 
67
- # Public: Sort the nodes on each level in the tree in lexical order but put
217
+ # Sort the nodes on each level in the tree in lexical order but put
68
218
  # leafs before non-leafs.
69
- def sort_children
70
- @children.sort! do |a, b|
71
- if (a.leaf? && b.leaf?) || (!a.leaf? && !b.leaf?)
72
- a.name <=> b.name
73
- elsif a.leaf? && !b.leaf?
74
- -1
75
- else
76
- 1
77
- end
219
+ def sort_leaf_first!
220
+ @children.sort! { |a, b| leaf_first(a, b) }
221
+ @children.each(&:sort_leaf_first!)
222
+ self
223
+ end
224
+
225
+ # returns:: the number of nodes in the subtree with this node as
226
+ # root
227
+ def count
228
+ result = 0
229
+ traverse_preorder do |level, node|
230
+ result += 1
78
231
  end
79
- @children.each(&:sort_children)
232
+ result
80
233
  end
81
234
 
82
- # Public: is this node a leaf
83
- #
84
- # Returns: true if the node is a leaf, false otherwise
235
+ # return:: true if the node is a leaf, false otherwise
85
236
  def leaf?
86
237
  @children.length.zero?
87
238
  end
88
239
 
240
+ # return:: an array with Pathnames of each full
241
+ # path for the leaves in this tree
242
+ def leave_pathnames(prune: false)
243
+ paths = []
244
+ traverse_postorder do |l, n|
245
+ next unless n.leaf?
246
+
247
+ paths << (prune ? n.pathname.relative_path_from(pathname) : n.pathname)
248
+ end
249
+ paths
250
+ end
251
+
252
+ # return:: true if this node does not have a parent node
253
+ def root?
254
+ @parent.nil?
255
+ end
256
+
257
+ # return:: the root node of the tree where this node is a member
258
+ def root
259
+ return self if root?
260
+
261
+ @parent.root
262
+ end
263
+
264
+ # Finds the node corresponding to the given path.
265
+ #
266
+ # path:: a String or Pathname with the path to search for
267
+ # from_root:: if true start the search from the root of the tree where
268
+ # this node is a member. If false, start the search from this node's
269
+ # children.
270
+ #
271
+ # return:: the node with the given path or nil if the path
272
+ # does not exist within this pathtree
273
+ def node(path, from_root: false)
274
+ p = clean(path)
275
+ root = nil
276
+
277
+ traverse_preorder do |level, node|
278
+ q = from_root ? node.pathname : node.pathname.relative_path_from(pathname)
279
+ if q == p
280
+ root = node
281
+ break
282
+ end
283
+ end
284
+ root
285
+ end
286
+
287
+ # adds a copy of the given Pathtree as a subtree to this node. the subtree can not
288
+ # contain nodes that will end up having the same pathname as any existing
289
+ # node in the target tree. Note that 'data' attributes will not be copied. The copied
290
+ # Pathtree nodes will thus point to the same data attributes as the original.
291
+ #
292
+ # == Example
293
+ #
294
+ # 1. Add my/new/tree to /1/2 -> /1/2/my/new/tree
295
+ # 2. Add /my/new/tree to /1/2 -> ArgumentError - can not add root as subtree
296
+ # 3. Trying to add 'new/tree' to '/my' node in a tree with '/my/new/tree' raises
297
+ # ArgumentError since the pathname that would result already exists within the
298
+ # target tree.
299
+ def append_tree(root_node)
300
+ raise ArgumentError, "Trying to append a root node as subtree!" if root_node.pathname.root?
301
+
302
+ # make a copy to make sure it is a self-sustaining PathTree
303
+ c = root_node.dup
304
+
305
+ # get all leaf paths prepended with this node's name to check for
306
+ # previous existance in this tree.
307
+ p = c.leave_pathnames.collect { |p| Pathname.new(@name) / p }
308
+
309
+ # duplicate ourselves to compare paths
310
+ t = dup
311
+
312
+ # check that no path in c would collide with existing paths
313
+ common = Set.new(t.leave_pathnames) & Set.new(p)
314
+ unless common.empty?
315
+ str = common.collect { |p| p.to_s }.join(",")
316
+ raise ArgumentError, "Can not append tree due to conflicting paths: #{str}"
317
+ end
318
+
319
+ # hook the subtree into this tree
320
+ @children << c
321
+ c.parent = self
322
+ end
323
+
324
+ # Splits the node's path into
325
+ # - a 'stem', the common path to all nodes in this tree that are on the
326
+ # same level as this node or closer to the root.
327
+ # - a 'crown', the remaining path when the stem has been removed from this
328
+ # node's pathname
329
+ #
330
+ # === Example
331
+ # n.split_stem for the following tree:
332
+ #
333
+ # base
334
+ # |- dir
335
+ # |- leaf_1
336
+ # |- branch
337
+ # |- leaf_2
338
+ #
339
+ # yields
340
+ # ["base/dir", "leaf_1"] when n == leaf_1
341
+ # ["base/dir", "branch/leaf_2"] when n == leaf_2
342
+ # ["base", "dir"] when n == "dir"
343
+ # [nil, "base"] when n == "base"
344
+ #
345
+ # return:: [stem, crown]
346
+ def split_stem
347
+ r = root
348
+ s = pathname.descend do |stem|
349
+ n = r.node(stem, from_root: true)
350
+ break n if n.children.count != 1 || n == self
351
+ end
352
+
353
+ if s == self
354
+ [root? ? nil : s.parent.pathname, @name]
355
+ else
356
+ [s.pathname, pathname.relative_path_from(s.pathname)]
357
+ end
358
+ end
359
+
360
+ # return:: a Pathname containing the relative path to this node as seen from the
361
+ # given node
362
+ def relative_path_from(node)
363
+ pathname.relative_path_from(node.pathname)
364
+ end
365
+
366
+ # Builds a PathTree with its root as the given file system dir or file
367
+ #
368
+ # fs_point:: an absolute or relative path to a file or directory that
369
+ # already exists in the file system.
370
+ # prune:: if false, add the entire, absolute, path to the fs_point to
371
+ # the PathTree. If true, use only the basename of the fs_point as the
372
+ # root of the PathTree
373
+ #
374
+ # You can submit a filter predicate that determine if a specific path
375
+ # shall be part of the PathTree or not ->(Pathname) { return true/false}
376
+ #
377
+ # return:: the node corresponding to the given fs_point in the resulting
378
+ # pathtree or nil if no nodes matched the given predicate filter
379
+ #
380
+ # === Example
381
+ #
382
+ # Build a pathtree containing all files under the "mydir" directory that
383
+ # ends with '.jpg'. The resulting tree will contain the absolute path
384
+ # to 'mydir' as nodes (eg '/home/gunnar/mydir')
385
+ #
386
+ # t = PathTree.build_from_fs("./mydir",true ) { |p| p.extname == ".jpg" }
387
+ def self.build_from_fs(fs_point, prune: false)
388
+ top_node = Pathname.new(fs_point).cleanpath
389
+ raise ArgumentError, "The path '#{fs_point}' does not exist in the file system!" unless top_node.exist?
390
+
391
+ t = nil
392
+ top_node.find do |path|
393
+ p = Pathname.new(path)
394
+
395
+ if (block_given? && yield(p)) || !block_given?
396
+ t.nil? ? t = PathTree.new(p) : t.add_path(p)
397
+ end
398
+ end
399
+ return nil if t.nil?
400
+
401
+ # always return the entry node but prune the parents if
402
+ # users wishes
403
+ entry_node = t.node(top_node, from_root: true)
404
+ (prune ? entry_node.dup : entry_node)
405
+ end
406
+
407
+ # delegate method calls not implemented by PathTree to the associated 'data'
408
+ # object
409
+ def method_missing(m, *args, &block)
410
+ return super if data.nil?
411
+
412
+ data.send(m, *args, &block)
413
+ end
414
+
415
+ def respond_to_missing?(method_name, include_private = false)
416
+ return super(method_name, include_private) if data.nil?
417
+
418
+ data.respond_to?(method_name)
419
+ end
420
+
421
+ def to_s
422
+ traverse_preorder do |level, n|
423
+ str = " " * 4 * level + "|-- " + n.segment.to_s
424
+ str += " <#{n.data}>" unless n.data.nil?
425
+ str
426
+ end.join("\n")
427
+ end
428
+
429
+ # Return a new PathTree with the nodes whith pathname matching the
430
+ # given regex.
431
+ #
432
+ # The copy will point to the same node data as the original.
433
+ #
434
+ # regex:: a Regex matching the pathname of the nodes to be included in
435
+ # the copy
436
+ # prune:: remove all parents to this node in the returned copy
437
+ #
438
+ # === Returns
439
+ # the entry node in a new PathTree with the nodes with pathnames matching the given regex
440
+ # or nil if no nodes match
441
+ def match(regex, prune: false)
442
+ copy = nil
443
+
444
+ traverse_preorder do |level, n|
445
+ p = n.pathname
446
+ next unless regex&.match?(p.to_s)
447
+
448
+ copy.nil? ? copy = PathTree.new(p, n.data) : copy.add_path(p, n.data)
449
+ end
450
+ return nil if copy.nil?
451
+
452
+ # always return the entry node but return a pruned version if
453
+ # the user wishes
454
+ entry_node = copy.node(pathname, from_root: true)
455
+ (prune ? entry_node.dup : entry_node)
456
+ end
457
+
458
+ # Return a new PathTree with the nodes matching the given block
459
+ #
460
+ # The copy will point to the same node data as the original.
461
+ #
462
+ # prune:: prune all parents to this node from the returned copy
463
+ #
464
+ # === Block
465
+ #
466
+ # The given block will receive the level (from the entry node) and
467
+ # the node itself for each node.
468
+ #
469
+ # === Returns
470
+ # the entry node to the new Pathtree or nil if no nodes matched the
471
+ # given block.
472
+ #
473
+ # === Example
474
+ #
475
+ # copy = original.filter { |l, n| n.data == "smurf" }
476
+ #
477
+ # The above will return a tree with nodes whose data is equal to 'smurf'
478
+ def filter(prune: false)
479
+ raise InvalidArgument, "No block given!" unless block_given?
480
+
481
+ # build the filtered copy
482
+ copy = nil
483
+ traverse_preorder do |level, n|
484
+ if yield(level, n)
485
+ p = n.pathname
486
+ copy.nil? ? copy = PathTree.new(p, n.data) : copy.add_path(p, n.data)
487
+ end
488
+ end
489
+
490
+ return nil if copy.nil?
491
+
492
+ # always return the entry node but return a pruned version if
493
+ # the user wishes
494
+ entry_node = copy.node(pathname, from_root: true)
495
+ (prune ? entry_node.dup : entry_node)
496
+ end
497
+
89
498
  private
90
499
 
91
- def get_child(segment_name)
92
- ch = @children.select { |c| c.name == segment_name }
93
- ch.length.zero? ? nil : ch[0]
500
+ def clean(path)
501
+ Pathname.new(path).cleanpath
94
502
  end
95
- end
96
503
 
97
- # test the class...
98
- if __FILE__ == $PROGRAM_NAME
99
- paths = %w[basedir/file_a
100
- basedir/file_a
101
- basedir/dira/file_c
102
- basedir/dirb/file_e
103
- basedir/dira/file_d
104
- basedir2/dir2/dir3/file_k
105
- basedir2/dir1/dir3/file_l
106
- basedir2/dir1/dir3/file_l
107
- basedir2/file_h
108
- basedir2/dir2/dir3/file_m]
109
-
110
- root = PathTree.new
111
- paths.each do |p|
112
- puts "adding path: #{p}"
113
- root.add_path p
114
- end
115
- root.sort_children
116
- root.traverse_top_down do |level, node|
117
- puts "#{' ' * level} - #{node.name}"
504
+ def leaf_first(left, right)
505
+ if left.leaf? != right.leaf?
506
+ # always return leaf before non-leaf
507
+ return left.leaf? ? -1 : 1
508
+ end
509
+
510
+ # for two non-leafs, return lexical order
511
+ left.segment <=> right.segment
512
+ end
513
+
514
+ def get_child(segment_name)
515
+ ch = @children.select { |c| c.segment == segment_name.to_s }
516
+ ch.length.zero? ? nil : ch[0]
118
517
  end
119
518
  end