nanoc 3.2.4 → 3.3.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.
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