nanoc 3.2.4 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (230) hide show
  1. data/.gemtest +0 -0
  2. data/ChangeLog +3 -0
  3. data/Gemfile +32 -0
  4. data/LICENSE +19 -0
  5. data/NEWS.md +470 -0
  6. data/README.md +114 -0
  7. data/Rakefile +14 -0
  8. data/bin/nanoc +7 -27
  9. data/bin/nanoc3 +3 -0
  10. data/doc/yardoc_templates/default/layout/html/footer.erb +10 -0
  11. data/lib/nanoc.rb +41 -0
  12. data/lib/nanoc/base.rb +49 -0
  13. data/lib/nanoc/base/compilation/checksum_store.rb +57 -0
  14. data/lib/nanoc/base/compilation/compiled_content_cache.rb +62 -0
  15. data/lib/nanoc/base/compilation/compiler.rb +458 -0
  16. data/lib/nanoc/base/compilation/compiler_dsl.rb +214 -0
  17. data/lib/nanoc/base/compilation/dependency_tracker.rb +200 -0
  18. data/lib/nanoc/base/compilation/filter.rb +165 -0
  19. data/lib/nanoc/base/compilation/item_rep_proxy.rb +103 -0
  20. data/lib/nanoc/base/compilation/item_rep_recorder_proxy.rb +102 -0
  21. data/lib/nanoc/base/compilation/outdatedness_checker.rb +223 -0
  22. data/lib/nanoc/base/compilation/outdatedness_reasons.rb +46 -0
  23. data/lib/nanoc/base/compilation/rule.rb +73 -0
  24. data/lib/nanoc/base/compilation/rule_context.rb +84 -0
  25. data/lib/nanoc/base/compilation/rule_memory_calculator.rb +40 -0
  26. data/lib/nanoc/base/compilation/rule_memory_store.rb +53 -0
  27. data/lib/nanoc/base/compilation/rules_collection.rb +243 -0
  28. data/lib/nanoc/base/context.rb +47 -0
  29. data/lib/nanoc/base/core_ext.rb +6 -0
  30. data/lib/nanoc/base/core_ext/array.rb +62 -0
  31. data/lib/nanoc/base/core_ext/hash.rb +63 -0
  32. data/lib/nanoc/base/core_ext/pathname.rb +26 -0
  33. data/lib/nanoc/base/core_ext/string.rb +46 -0
  34. data/lib/nanoc/base/directed_graph.rb +275 -0
  35. data/lib/nanoc/base/errors.rb +211 -0
  36. data/lib/nanoc/base/memoization.rb +67 -0
  37. data/lib/nanoc/base/notification_center.rb +84 -0
  38. data/lib/nanoc/base/ordered_hash.rb +200 -0
  39. data/lib/nanoc/base/plugin_registry.rb +181 -0
  40. data/lib/nanoc/base/result_data/item_rep.rb +492 -0
  41. data/lib/nanoc/base/source_data/code_snippet.rb +58 -0
  42. data/lib/nanoc/base/source_data/configuration.rb +24 -0
  43. data/lib/nanoc/base/source_data/data_source.rb +234 -0
  44. data/lib/nanoc/base/source_data/item.rb +301 -0
  45. data/lib/nanoc/base/source_data/layout.rb +130 -0
  46. data/lib/nanoc/base/source_data/site.rb +361 -0
  47. data/lib/nanoc/base/store.rb +135 -0
  48. data/lib/nanoc/cli.rb +137 -0
  49. data/lib/nanoc/cli/command_runner.rb +104 -0
  50. data/lib/nanoc/cli/commands/autocompile.rb +58 -0
  51. data/lib/nanoc/cli/commands/compile.rb +297 -0
  52. data/lib/nanoc/cli/commands/create_item.rb +60 -0
  53. data/lib/nanoc/cli/commands/create_layout.rb +73 -0
  54. data/lib/nanoc/cli/commands/create_site.rb +411 -0
  55. data/lib/nanoc/cli/commands/debug.rb +117 -0
  56. data/lib/nanoc/cli/commands/deploy.rb +79 -0
  57. data/lib/nanoc/cli/commands/info.rb +98 -0
  58. data/lib/nanoc/cli/commands/nanoc.rb +38 -0
  59. data/lib/nanoc/cli/commands/prune.rb +50 -0
  60. data/lib/nanoc/cli/commands/update.rb +70 -0
  61. data/lib/nanoc/cli/commands/view.rb +82 -0
  62. data/lib/nanoc/cli/commands/watch.rb +124 -0
  63. data/lib/nanoc/cli/error_handler.rb +199 -0
  64. data/lib/nanoc/cli/logger.rb +92 -0
  65. data/lib/nanoc/data_sources.rb +29 -0
  66. data/lib/nanoc/data_sources/deprecated/delicious.rb +42 -0
  67. data/lib/nanoc/data_sources/deprecated/last_fm.rb +87 -0
  68. data/lib/nanoc/data_sources/deprecated/twitter.rb +38 -0
  69. data/lib/nanoc/data_sources/filesystem.rb +299 -0
  70. data/lib/nanoc/data_sources/filesystem_unified.rb +121 -0
  71. data/lib/nanoc/data_sources/filesystem_verbose.rb +91 -0
  72. data/lib/nanoc/extra.rb +24 -0
  73. data/lib/nanoc/extra/auto_compiler.rb +103 -0
  74. data/lib/nanoc/extra/chick.rb +125 -0
  75. data/lib/nanoc/extra/core_ext.rb +6 -0
  76. data/lib/nanoc/extra/core_ext/enumerable.rb +33 -0
  77. data/lib/nanoc/extra/core_ext/pathname.rb +30 -0
  78. data/lib/nanoc/extra/core_ext/time.rb +19 -0
  79. data/lib/nanoc/extra/deployer.rb +47 -0
  80. data/lib/nanoc/extra/deployers.rb +15 -0
  81. data/lib/nanoc/extra/deployers/fog.rb +98 -0
  82. data/lib/nanoc/extra/deployers/rsync.rb +70 -0
  83. data/lib/nanoc/extra/file_proxy.rb +40 -0
  84. data/lib/nanoc/extra/pruner.rb +86 -0
  85. data/lib/nanoc/extra/validators.rb +12 -0
  86. data/lib/nanoc/extra/validators/links.rb +268 -0
  87. data/lib/nanoc/extra/validators/w3c.rb +95 -0
  88. data/lib/nanoc/extra/vcs.rb +66 -0
  89. data/lib/nanoc/extra/vcses.rb +17 -0
  90. data/lib/nanoc/extra/vcses/bazaar.rb +25 -0
  91. data/lib/nanoc/extra/vcses/dummy.rb +24 -0
  92. data/lib/nanoc/extra/vcses/git.rb +25 -0
  93. data/lib/nanoc/extra/vcses/mercurial.rb +25 -0
  94. data/lib/nanoc/extra/vcses/subversion.rb +25 -0
  95. data/lib/nanoc/filters.rb +59 -0
  96. data/lib/nanoc/filters/asciidoc.rb +38 -0
  97. data/lib/nanoc/filters/bluecloth.rb +19 -0
  98. data/lib/nanoc/filters/coderay.rb +21 -0
  99. data/lib/nanoc/filters/coffeescript.rb +20 -0
  100. data/lib/nanoc/filters/colorize_syntax.rb +298 -0
  101. data/lib/nanoc/filters/erb.rb +38 -0
  102. data/lib/nanoc/filters/erubis.rb +34 -0
  103. data/lib/nanoc/filters/haml.rb +27 -0
  104. data/lib/nanoc/filters/kramdown.rb +20 -0
  105. data/lib/nanoc/filters/less.rb +53 -0
  106. data/lib/nanoc/filters/markaby.rb +20 -0
  107. data/lib/nanoc/filters/maruku.rb +20 -0
  108. data/lib/nanoc/filters/mustache.rb +24 -0
  109. data/lib/nanoc/filters/rainpress.rb +19 -0
  110. data/lib/nanoc/filters/rdiscount.rb +22 -0
  111. data/lib/nanoc/filters/rdoc.rb +33 -0
  112. data/lib/nanoc/filters/redcarpet.rb +62 -0
  113. data/lib/nanoc/filters/redcloth.rb +47 -0
  114. data/lib/nanoc/filters/relativize_paths.rb +94 -0
  115. data/lib/nanoc/filters/rubypants.rb +20 -0
  116. data/lib/nanoc/filters/sass.rb +74 -0
  117. data/lib/nanoc/filters/slim.rb +25 -0
  118. data/lib/nanoc/filters/typogruby.rb +23 -0
  119. data/lib/nanoc/filters/uglify_js.rb +42 -0
  120. data/lib/nanoc/filters/xsl.rb +46 -0
  121. data/lib/nanoc/filters/yui_compressor.rb +23 -0
  122. data/lib/nanoc/helpers.rb +16 -0
  123. data/lib/nanoc/helpers/blogging.rb +319 -0
  124. data/lib/nanoc/helpers/breadcrumbs.rb +40 -0
  125. data/lib/nanoc/helpers/capturing.rb +138 -0
  126. data/lib/nanoc/helpers/filtering.rb +50 -0
  127. data/lib/nanoc/helpers/html_escape.rb +55 -0
  128. data/lib/nanoc/helpers/link_to.rb +151 -0
  129. data/lib/nanoc/helpers/rendering.rb +140 -0
  130. data/lib/nanoc/helpers/tagging.rb +71 -0
  131. data/lib/nanoc/helpers/text.rb +44 -0
  132. data/lib/nanoc/helpers/xml_sitemap.rb +76 -0
  133. data/lib/nanoc/tasks.rb +10 -0
  134. data/lib/nanoc/tasks/clean.rake +16 -0
  135. data/lib/nanoc/tasks/clean.rb +29 -0
  136. data/lib/nanoc/tasks/deploy/rsync.rake +16 -0
  137. data/lib/nanoc/tasks/validate.rake +92 -0
  138. data/nanoc.gemspec +49 -0
  139. data/tasks/doc.rake +16 -0
  140. data/tasks/test.rake +46 -0
  141. data/test/base/core_ext/array_spec.rb +73 -0
  142. data/test/base/core_ext/hash_spec.rb +98 -0
  143. data/test/base/core_ext/pathname_spec.rb +27 -0
  144. data/test/base/core_ext/string_spec.rb +37 -0
  145. data/test/base/test_checksum_store.rb +35 -0
  146. data/test/base/test_code_snippet.rb +31 -0
  147. data/test/base/test_compiler.rb +403 -0
  148. data/test/base/test_compiler_dsl.rb +161 -0
  149. data/test/base/test_context.rb +31 -0
  150. data/test/base/test_data_source.rb +46 -0
  151. data/test/base/test_dependency_tracker.rb +262 -0
  152. data/test/base/test_directed_graph.rb +288 -0
  153. data/test/base/test_filter.rb +83 -0
  154. data/test/base/test_item.rb +179 -0
  155. data/test/base/test_item_rep.rb +579 -0
  156. data/test/base/test_layout.rb +59 -0
  157. data/test/base/test_memoization.rb +90 -0
  158. data/test/base/test_notification_center.rb +34 -0
  159. data/test/base/test_outdatedness_checker.rb +394 -0
  160. data/test/base/test_plugin.rb +30 -0
  161. data/test/base/test_rule.rb +19 -0
  162. data/test/base/test_rule_context.rb +65 -0
  163. data/test/base/test_site.rb +190 -0
  164. data/test/cli/commands/test_compile.rb +33 -0
  165. data/test/cli/commands/test_create_item.rb +14 -0
  166. data/test/cli/commands/test_create_layout.rb +28 -0
  167. data/test/cli/commands/test_create_site.rb +24 -0
  168. data/test/cli/commands/test_deploy.rb +74 -0
  169. data/test/cli/commands/test_help.rb +12 -0
  170. data/test/cli/commands/test_info.rb +11 -0
  171. data/test/cli/commands/test_prune.rb +98 -0
  172. data/test/cli/commands/test_update.rb +10 -0
  173. data/test/cli/test_cli.rb +102 -0
  174. data/test/cli/test_error_handler.rb +29 -0
  175. data/test/cli/test_logger.rb +10 -0
  176. data/test/data_sources/test_filesystem.rb +433 -0
  177. data/test/data_sources/test_filesystem_unified.rb +536 -0
  178. data/test/data_sources/test_filesystem_verbose.rb +357 -0
  179. data/test/extra/core_ext/test_enumerable.rb +30 -0
  180. data/test/extra/core_ext/test_pathname.rb +17 -0
  181. data/test/extra/core_ext/test_time.rb +15 -0
  182. data/test/extra/deployers/test_fog.rb +67 -0
  183. data/test/extra/deployers/test_rsync.rb +100 -0
  184. data/test/extra/test_auto_compiler.rb +417 -0
  185. data/test/extra/test_file_proxy.rb +19 -0
  186. data/test/extra/test_vcs.rb +22 -0
  187. data/test/extra/validators/test_links.rb +62 -0
  188. data/test/extra/validators/test_w3c.rb +47 -0
  189. data/test/filters/test_asciidoc.rb +22 -0
  190. data/test/filters/test_bluecloth.rb +18 -0
  191. data/test/filters/test_coderay.rb +44 -0
  192. data/test/filters/test_coffeescript.rb +18 -0
  193. data/test/filters/test_colorize_syntax.rb +379 -0
  194. data/test/filters/test_erb.rb +105 -0
  195. data/test/filters/test_erubis.rb +78 -0
  196. data/test/filters/test_haml.rb +96 -0
  197. data/test/filters/test_kramdown.rb +18 -0
  198. data/test/filters/test_less.rb +113 -0
  199. data/test/filters/test_markaby.rb +24 -0
  200. data/test/filters/test_maruku.rb +18 -0
  201. data/test/filters/test_mustache.rb +25 -0
  202. data/test/filters/test_rainpress.rb +29 -0
  203. data/test/filters/test_rdiscount.rb +31 -0
  204. data/test/filters/test_rdoc.rb +18 -0
  205. data/test/filters/test_redcarpet.rb +73 -0
  206. data/test/filters/test_redcloth.rb +33 -0
  207. data/test/filters/test_relativize_paths.rb +533 -0
  208. data/test/filters/test_rubypants.rb +18 -0
  209. data/test/filters/test_sass.rb +229 -0
  210. data/test/filters/test_slim.rb +35 -0
  211. data/test/filters/test_typogruby.rb +21 -0
  212. data/test/filters/test_uglify_js.rb +30 -0
  213. data/test/filters/test_xsl.rb +105 -0
  214. data/test/filters/test_yui_compressor.rb +44 -0
  215. data/test/gem_loader.rb +11 -0
  216. data/test/helper.rb +207 -0
  217. data/test/helpers/test_blogging.rb +754 -0
  218. data/test/helpers/test_breadcrumbs.rb +81 -0
  219. data/test/helpers/test_capturing.rb +41 -0
  220. data/test/helpers/test_filtering.rb +106 -0
  221. data/test/helpers/test_html_escape.rb +32 -0
  222. data/test/helpers/test_link_to.rb +249 -0
  223. data/test/helpers/test_rendering.rb +89 -0
  224. data/test/helpers/test_tagging.rb +87 -0
  225. data/test/helpers/test_text.rb +24 -0
  226. data/test/helpers/test_xml_sitemap.rb +103 -0
  227. data/test/tasks/test_clean.rb +67 -0
  228. metadata +327 -15
  229. data/bin/nanoc-select +0 -86
  230. data/lib/nanoc-select.rb +0 -11
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc
4
+
5
+ # Provides a context and a binding for use in filters such as the ERB and
6
+ # Haml ones.
7
+ class Context
8
+
9
+ # Creates a new context based off the contents of the hash.
10
+ #
11
+ # Each pair in the hash will be converted to an instance variable and an
12
+ # instance method. For example, passing the hash `{ :foo => 'bar' }` will
13
+ # cause `@foo` to have the value `"bar"`, and the instance method `#foo`
14
+ # to return the same value `"bar"`.
15
+ #
16
+ # @param [Hash] hash A list of key-value pairs to make available
17
+ #
18
+ # @example Defining a context and accessing values
19
+ #
20
+ # context = Nanoc::Context.new(
21
+ # :name => 'Max Payne',
22
+ # :location => 'in a cheap motel'
23
+ # )
24
+ # context.instance_eval do
25
+ # "I am #{name} and I am hiding #{@location}."
26
+ # end
27
+ # # => "I am Max Payne and I am hiding in a cheap motel."
28
+ def initialize(hash)
29
+ hash.each_pair do |key, value|
30
+ # Build instance variable
31
+ instance_variable_set('@' + key.to_s, value)
32
+
33
+ # Define method
34
+ metaclass = (class << self ; self ; end)
35
+ metaclass.send(:define_method, key) { value }
36
+ end
37
+ end
38
+
39
+ # Returns a binding for this instance.
40
+ #
41
+ # @return [Binding] A binding for this instance
42
+ def get_binding
43
+ binding
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nanoc/base/core_ext/array'
4
+ require 'nanoc/base/core_ext/hash'
5
+ require 'nanoc/base/core_ext/pathname'
6
+ require 'nanoc/base/core_ext/string'
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::ArrayExtensions
4
+
5
+ # Returns a new array where all items' keys are recursively converted to
6
+ # symbols by calling {Nanoc::ArrayExtensions#symbolize_keys} or
7
+ # {Nanoc::HashExtensions#symbolize_keys}.
8
+ #
9
+ # @return [Array] The converted array
10
+ def symbolize_keys
11
+ inject([]) do |array, element|
12
+ array + [ element.respond_to?(:symbolize_keys) ? element.symbolize_keys : element ]
13
+ end
14
+ end
15
+
16
+ # Returns a new array where all items' keys are recursively converted to
17
+ # strings by calling {Nanoc::ArrayExtensions#stringify_keys} or
18
+ # {Nanoc::HashExtensions#stringify_keys}.
19
+ #
20
+ # @return [Array] The converted array
21
+ def stringify_keys
22
+ inject([]) do |array, element|
23
+ array + [ element.respond_to?(:stringify_keys) ? element.stringify_keys : element ]
24
+ end
25
+ end
26
+
27
+ # Freezes the contents of the array, as well as all array elements. The
28
+ # array elements will be frozen using {#freeze_recursively} if they respond
29
+ # to that message, or #freeze if they do not.
30
+ #
31
+ # @see Hash#freeze_recursively
32
+ #
33
+ # @return [void]
34
+ #
35
+ # @since 3.2.0
36
+ def freeze_recursively
37
+ return if self.frozen?
38
+ freeze
39
+ each do |value|
40
+ if value.respond_to?(:freeze_recursively)
41
+ value.freeze_recursively
42
+ else
43
+ value.freeze
44
+ end
45
+ end
46
+ end
47
+
48
+ # Calculates the checksum for this array. Any change to this array will
49
+ # result in a different checksum.
50
+ #
51
+ # @return [String] The checksum for this array
52
+ #
53
+ # @api private
54
+ def checksum
55
+ Marshal.dump(self).checksum
56
+ end
57
+
58
+ end
59
+
60
+ class Array
61
+ include Nanoc::ArrayExtensions
62
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::HashExtensions
4
+
5
+ # Returns a new hash where all keys are recursively converted to symbols by
6
+ # calling {Nanoc::ArrayExtensions#symbolize_keys} or
7
+ # {Nanoc::HashExtensions#symbolize_keys}.
8
+ #
9
+ # @return [Hash] The converted hash
10
+ def symbolize_keys
11
+ inject({}) do |hash, (key, value)|
12
+ hash.merge(key.to_sym => value.respond_to?(:symbolize_keys) ? value.symbolize_keys : value)
13
+ end
14
+ end
15
+
16
+ # Returns a new hash where all keys are recursively converted to strings by
17
+ # calling {Nanoc::ArrayExtensions#stringify_keys} or
18
+ # {Nanoc::HashExtensions#stringify_keys}.
19
+ #
20
+ # @return [Hash] The converted hash
21
+ def stringify_keys
22
+ inject({}) do |hash, (key, value)|
23
+ hash.merge(key.to_s => value.respond_to?(:stringify_keys) ? value.stringify_keys : value)
24
+ end
25
+ end
26
+
27
+ # Freezes the contents of the hash, as well as all hash values. The hash
28
+ # values will be frozen using {#freeze_recursively} if they respond to
29
+ # that message, or #freeze if they do not.
30
+ #
31
+ # @see Array#freeze_recursively
32
+ #
33
+ # @return [void]
34
+ #
35
+ # @since 3.2.0
36
+ def freeze_recursively
37
+ return if self.frozen?
38
+ freeze
39
+ each_pair do |key, value|
40
+ if value.respond_to?(:freeze_recursively)
41
+ value.freeze_recursively
42
+ else
43
+ value.freeze
44
+ end
45
+ end
46
+ end
47
+
48
+ # Calculates the checksum for this hash. Any change to this hash will result
49
+ # in a different checksum.
50
+ #
51
+ # @return [String] The checksum for this hash
52
+ #
53
+ # @api private
54
+ def checksum
55
+ array = self.to_a.sort_by { |kv| kv[0].to_s }
56
+ array.checksum
57
+ end
58
+
59
+ end
60
+
61
+ class Hash
62
+ include Nanoc::HashExtensions
63
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::PathnameExtensions
4
+
5
+ # Calculates the checksum for the file referenced to by this pathname. Any
6
+ # change to the file contents will result in a different checksum.
7
+ #
8
+ # @return [String] The checksum for this file
9
+ #
10
+ # @api private
11
+ def checksum
12
+ digest = Digest::SHA1.new
13
+ File.open(self.to_s, 'r') do |io|
14
+ until io.eof
15
+ data = io.read(2**10)
16
+ digest.update(data)
17
+ end
18
+ end
19
+ digest.hexdigest
20
+ end
21
+
22
+ end
23
+
24
+ class Pathname
25
+ include Nanoc::PathnameExtensions
26
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::StringExtensions
4
+
5
+ # Transforms string into an actual identifier
6
+ #
7
+ # @return [String] The identifier generated from the receiver
8
+ def cleaned_identifier
9
+ "/#{self}/".gsub(/^\/+|\/+$/, '/')
10
+ end
11
+
12
+ # Replaces Unicode characters with their ASCII decompositions if the
13
+ # environment does not support Unicode.
14
+ #
15
+ # This method is not suited for general usage. If you need similar
16
+ # functionality, consider using the Iconv library instead.
17
+ #
18
+ # @return [String] The decomposed string
19
+ def make_compatible_with_env
20
+ # Check whether environment supports Unicode
21
+ # FIXME this is ugly, and there most likely are better ways to do this
22
+ is_unicode_supported = %w( LC_ALL LC_CTYPE LANG ).any? { |e| ENV[e] =~ /UTF/ }
23
+ return self if is_unicode_supported
24
+
25
+ # Decompose if necessary
26
+ # FIXME this decomposition is not generally usable
27
+ self.gsub(/“|”/, '"').gsub(/‘|’/, '\'').gsub('…', '...').gsub('©', '(c)')
28
+ end
29
+
30
+ # Calculates the checksum for this string. Any change to this string will
31
+ # result in a different checksum.
32
+ #
33
+ # @return [String] The checksum for this string
34
+ #
35
+ # @api private
36
+ def checksum
37
+ digest = Digest::SHA1.new
38
+ digest.update(self)
39
+ digest.hexdigest
40
+ end
41
+
42
+ end
43
+
44
+ class String
45
+ include Nanoc::StringExtensions
46
+ end
@@ -0,0 +1,275 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc
4
+
5
+ # Represents a directed graph. It is used by the dependency tracker for
6
+ # storing and querying dependencies between items.
7
+ #
8
+ # @example Creating and using a directed graph
9
+ #
10
+ # # Create a graph with three vertices
11
+ # graph = Nanoc::DirectedGraph.new(%w( a b c d ))
12
+ #
13
+ # # Add edges
14
+ # graph.add_edge('a', 'b')
15
+ # graph.add_edge('b', 'c')
16
+ # graph.add_edge('c', 'd')
17
+ #
18
+ # # Get (direct) predecessors
19
+ # graph.direct_predecessors_of('d').sort
20
+ # # => %w( c )
21
+ # graph.predecessors_of('d').sort
22
+ # # => %w( a b c )
23
+ #
24
+ # # Modify edges
25
+ # graph.delete_edge('a', 'b')
26
+ #
27
+ # # Get (direct) predecessors again
28
+ # graph.direct_predecessors_of('d').sort
29
+ # # => %w( c )
30
+ # graph.predecessors_of('d').sort
31
+ # # => %w( b c )
32
+ class DirectedGraph
33
+
34
+ # The set of vertices in this graph.
35
+ #
36
+ # @return [Set]
37
+ attr_reader :vertices
38
+
39
+ # @group Creating a graph
40
+
41
+ # Creates a new directed graph with the given vertices.
42
+ def initialize(vertices)
43
+ @vertices = Set.new(vertices)
44
+
45
+ @from_graph = {}
46
+ @to_graph = {}
47
+
48
+ @vertice_indexes = {}
49
+ @vertices.each_with_index do |v, i|
50
+ @vertice_indexes[v] = i
51
+ end
52
+
53
+ @roots = Set.new(@vertices)
54
+
55
+ invalidate_caches
56
+ end
57
+
58
+ # @group Modifying the graph
59
+
60
+ # Adds an edge from the first vertex to the second vertex.
61
+ #
62
+ # @param from Vertex where the edge should start
63
+ #
64
+ # @param to Vertex where the edge should end
65
+ #
66
+ # @return [void]
67
+ def add_edge(from, to)
68
+ add_vertex(from)
69
+ add_vertex(to)
70
+
71
+ @from_graph[from] ||= Set.new
72
+ @from_graph[from] << to
73
+
74
+ @to_graph[to] ||= Set.new
75
+ @to_graph[to] << from
76
+
77
+ @roots.delete(to)
78
+
79
+ invalidate_caches
80
+ end
81
+
82
+ # Removes the edge from the first vertex to the second vertex. If the
83
+ # edge does not exist, nothing is done.
84
+ #
85
+ # @param from Start vertex of the edge
86
+ #
87
+ # @param to End vertex of the edge
88
+ #
89
+ # @return [void]
90
+ #
91
+ # @since 3.2.0
92
+ def delete_edge(from, to)
93
+ @from_graph[from] ||= Set.new
94
+ @from_graph[from].delete(to)
95
+
96
+ @to_graph[to] ||= Set.new
97
+ @to_graph[to].delete(from)
98
+
99
+ @roots.add(to) if @to_graph[to].empty?
100
+
101
+ invalidate_caches
102
+ end
103
+
104
+ # Adds the given vertex to the graph.
105
+ #
106
+ # @param v The vertex to add to the graph
107
+ #
108
+ # @return [void]
109
+ #
110
+ # @since 3.2.0
111
+ def add_vertex(v)
112
+ return if @vertices.include?(v)
113
+
114
+ @vertices << v
115
+ @vertice_indexes[v] = @vertices.size-1
116
+
117
+ @roots << v
118
+ end
119
+
120
+ # Deletes all edges coming from the given vertex.
121
+ #
122
+ # @param from Vertex from which all edges should be removed
123
+ #
124
+ # @return [void]
125
+ #
126
+ # @since 3.2.0
127
+ def delete_edges_from(from)
128
+ return if @from_graph[from].nil?
129
+
130
+ @from_graph[from].each do |to|
131
+ @to_graph[to].delete(from)
132
+ @roots.add(to) if @to_graph[to].empty?
133
+ end
134
+ @from_graph.delete(from)
135
+ end
136
+
137
+ # Deletes all edges going to the given vertex.
138
+ #
139
+ # @param to Vertex to which all edges should be removed
140
+ #
141
+ # @return [void]
142
+ def delete_edges_to(to)
143
+ return if @to_graph[to].nil?
144
+
145
+ @to_graph[to].each do |from|
146
+ @from_graph[from].delete(to)
147
+ end
148
+ @to_graph.delete(to)
149
+ @roots.add(to)
150
+ end
151
+
152
+ # Removes the given vertex from the graph.
153
+ #
154
+ # @param v Vertex to remove from the graph
155
+ #
156
+ # @return [void]
157
+ #
158
+ # @since 3.2.0
159
+ def delete_vertex(v)
160
+ delete_edges_to(v)
161
+ delete_edges_from(v)
162
+
163
+ @vertices.delete(v)
164
+ @roots.delete(v)
165
+ end
166
+
167
+ # @group Querying the graph
168
+
169
+ # Returns the direct predecessors of the given vertex, i.e. the vertices
170
+ # x where there is an edge from x to the given vertex y.
171
+ #
172
+ # @param to The vertex of which the predecessors should be calculated
173
+ #
174
+ # @return [Array] Direct predecessors of the given vertex
175
+ def direct_predecessors_of(to)
176
+ @to_graph[to].to_a
177
+ end
178
+
179
+ # Returns the direct successors of the given vertex, i.e. the vertices y
180
+ # where there is an edge from the given vertex x to y.
181
+ #
182
+ # @param from The vertex of which the successors should be calculated
183
+ #
184
+ # @return [Array] Direct successors of the given vertex
185
+ def direct_successors_of(from)
186
+ @from_graph[from].to_a
187
+ end
188
+
189
+ # Returns the predecessors of the given vertex, i.e. the vertices x for
190
+ # which there is a path from x to the given vertex y.
191
+ #
192
+ # @param to The vertex of which the predecessors should be calculated
193
+ #
194
+ # @return [Array] Predecessors of the given vertex
195
+ def predecessors_of(to)
196
+ @predecessors[to] ||= recursively_find_vertices(to, :direct_predecessors_of)
197
+ end
198
+
199
+ # Returns the successors of the given vertex, i.e. the vertices y for
200
+ # which there is a path from the given vertex x to y.
201
+ #
202
+ # @param from The vertex of which the successors should be calculated
203
+ #
204
+ # @return [Array] Successors of the given vertex
205
+ def successors_of(from)
206
+ @successors[from] ||= recursively_find_vertices(from, :direct_successors_of)
207
+ end
208
+
209
+ # Returns an array of tuples representing the edges. The result of this
210
+ # method may take a while to compute and should be cached if possible.
211
+ #
212
+ # @return [Array] The list of all edges in this graph.
213
+ def edges
214
+ result = []
215
+ @vertices.each_with_index do |v, i|
216
+ direct_successors_of(v).map { |v2| @vertice_indexes[v2] }.each do |i2|
217
+ result << [ i, i2 ]
218
+ end
219
+ end
220
+ result
221
+ end
222
+
223
+ # Returns all root vertices, i.e. vertices where no edge points to.
224
+ #
225
+ # @return [Set] The set of all root vertices in this graph.
226
+ #
227
+ # @since 3.2.0
228
+ def roots
229
+ @roots
230
+ end
231
+
232
+ # @group Deprecated methods
233
+
234
+ # @deprecated Use {#delete_edge} instead
235
+ def remove_edge(from, to)
236
+ delete_edge(from, to)
237
+ end
238
+
239
+ private
240
+
241
+ # Invalidates cached data. This method should be called when the internal
242
+ # graph representation is changed.
243
+ def invalidate_caches
244
+ @predecessors = {}
245
+ @successors = {}
246
+ end
247
+
248
+ # Recursively finds vertices, starting at the vertex start, using the
249
+ # given method, which should be a symbol to a method that takes a vertex
250
+ # and returns related vertices (e.g. predecessors, successors).
251
+ def recursively_find_vertices(start, method)
252
+ all_vertices = Set.new
253
+
254
+ processed_vertices = Set.new
255
+ unprocessed_vertices = [ start ]
256
+
257
+ until unprocessed_vertices.empty?
258
+ # Get next unprocessed vertex
259
+ vertex = unprocessed_vertices.pop
260
+ next if processed_vertices.include?(vertex)
261
+ processed_vertices << vertex
262
+
263
+ # Add predecessors of this vertex
264
+ send(method, vertex).each do |v|
265
+ all_vertices << v unless all_vertices.include?(v)
266
+ unprocessed_vertices << v
267
+ end
268
+ end
269
+
270
+ all_vertices.to_a
271
+ end
272
+
273
+ end
274
+
275
+ end