nanoc3 3.0.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 (116) hide show
  1. data/ChangeLog +3 -0
  2. data/LICENSE +19 -0
  3. data/NEWS.rdoc +262 -0
  4. data/README.rdoc +80 -0
  5. data/Rakefile +11 -0
  6. data/bin/nanoc3 +16 -0
  7. data/lib/nanoc3/base/code_snippet.rb +42 -0
  8. data/lib/nanoc3/base/compiler.rb +225 -0
  9. data/lib/nanoc3/base/compiler_dsl.rb +110 -0
  10. data/lib/nanoc3/base/core_ext/array.rb +21 -0
  11. data/lib/nanoc3/base/core_ext/hash.rb +23 -0
  12. data/lib/nanoc3/base/core_ext/string.rb +14 -0
  13. data/lib/nanoc3/base/core_ext.rb +5 -0
  14. data/lib/nanoc3/base/data_source.rb +197 -0
  15. data/lib/nanoc3/base/dependency_tracker.rb +291 -0
  16. data/lib/nanoc3/base/errors.rb +95 -0
  17. data/lib/nanoc3/base/filter.rb +60 -0
  18. data/lib/nanoc3/base/item.rb +87 -0
  19. data/lib/nanoc3/base/item_rep.rb +236 -0
  20. data/lib/nanoc3/base/layout.rb +53 -0
  21. data/lib/nanoc3/base/notification_center.rb +68 -0
  22. data/lib/nanoc3/base/plugin.rb +88 -0
  23. data/lib/nanoc3/base/preprocessor_context.rb +37 -0
  24. data/lib/nanoc3/base/rule.rb +37 -0
  25. data/lib/nanoc3/base/rule_context.rb +68 -0
  26. data/lib/nanoc3/base/site.rb +334 -0
  27. data/lib/nanoc3/base.rb +25 -0
  28. data/lib/nanoc3/cli/base.rb +151 -0
  29. data/lib/nanoc3/cli/commands/autocompile.rb +89 -0
  30. data/lib/nanoc3/cli/commands/compile.rb +279 -0
  31. data/lib/nanoc3/cli/commands/create_item.rb +79 -0
  32. data/lib/nanoc3/cli/commands/create_layout.rb +94 -0
  33. data/lib/nanoc3/cli/commands/create_site.rb +320 -0
  34. data/lib/nanoc3/cli/commands/help.rb +71 -0
  35. data/lib/nanoc3/cli/commands/info.rb +114 -0
  36. data/lib/nanoc3/cli/commands/update.rb +96 -0
  37. data/lib/nanoc3/cli/commands.rb +13 -0
  38. data/lib/nanoc3/cli/logger.rb +73 -0
  39. data/lib/nanoc3/cli.rb +16 -0
  40. data/lib/nanoc3/data_sources/delicious.rb +66 -0
  41. data/lib/nanoc3/data_sources/filesystem.rb +231 -0
  42. data/lib/nanoc3/data_sources/filesystem_combined.rb +202 -0
  43. data/lib/nanoc3/data_sources/filesystem_common.rb +22 -0
  44. data/lib/nanoc3/data_sources/filesystem_compact.rb +232 -0
  45. data/lib/nanoc3/data_sources/last_fm.rb +103 -0
  46. data/lib/nanoc3/data_sources/twitter.rb +53 -0
  47. data/lib/nanoc3/data_sources.rb +20 -0
  48. data/lib/nanoc3/extra/auto_compiler.rb +97 -0
  49. data/lib/nanoc3/extra/chick.rb +119 -0
  50. data/lib/nanoc3/extra/context.rb +24 -0
  51. data/lib/nanoc3/extra/core_ext/time.rb +19 -0
  52. data/lib/nanoc3/extra/core_ext.rb +3 -0
  53. data/lib/nanoc3/extra/deployers/rsync.rb +64 -0
  54. data/lib/nanoc3/extra/deployers.rb +12 -0
  55. data/lib/nanoc3/extra/file_proxy.rb +31 -0
  56. data/lib/nanoc3/extra/validators/links.rb +0 -0
  57. data/lib/nanoc3/extra/validators/w3c.rb +71 -0
  58. data/lib/nanoc3/extra/validators.rb +12 -0
  59. data/lib/nanoc3/extra/vcs.rb +65 -0
  60. data/lib/nanoc3/extra/vcses/bazaar.rb +21 -0
  61. data/lib/nanoc3/extra/vcses/dummy.rb +20 -0
  62. data/lib/nanoc3/extra/vcses/git.rb +21 -0
  63. data/lib/nanoc3/extra/vcses/mercurial.rb +21 -0
  64. data/lib/nanoc3/extra/vcses/subversion.rb +21 -0
  65. data/lib/nanoc3/extra/vcses.rb +17 -0
  66. data/lib/nanoc3/extra.rb +16 -0
  67. data/lib/nanoc3/filters/bluecloth.rb +13 -0
  68. data/lib/nanoc3/filters/coderay.rb +17 -0
  69. data/lib/nanoc3/filters/erb.rb +19 -0
  70. data/lib/nanoc3/filters/erubis.rb +17 -0
  71. data/lib/nanoc3/filters/haml.rb +20 -0
  72. data/lib/nanoc3/filters/less.rb +13 -0
  73. data/lib/nanoc3/filters/markaby.rb +14 -0
  74. data/lib/nanoc3/filters/maruku.rb +14 -0
  75. data/lib/nanoc3/filters/rainpress.rb +13 -0
  76. data/lib/nanoc3/filters/rdiscount.rb +13 -0
  77. data/lib/nanoc3/filters/rdoc.rb +23 -0
  78. data/lib/nanoc3/filters/redcloth.rb +14 -0
  79. data/lib/nanoc3/filters/relativize_paths.rb +32 -0
  80. data/lib/nanoc3/filters/rubypants.rb +14 -0
  81. data/lib/nanoc3/filters/sass.rb +17 -0
  82. data/lib/nanoc3/filters.rb +37 -0
  83. data/lib/nanoc3/helpers/blogging.rb +226 -0
  84. data/lib/nanoc3/helpers/breadcrumbs.rb +25 -0
  85. data/lib/nanoc3/helpers/capturing.rb +71 -0
  86. data/lib/nanoc3/helpers/filtering.rb +46 -0
  87. data/lib/nanoc3/helpers/html_escape.rb +22 -0
  88. data/lib/nanoc3/helpers/link_to.rb +120 -0
  89. data/lib/nanoc3/helpers/rendering.rb +76 -0
  90. data/lib/nanoc3/helpers/tagging.rb +58 -0
  91. data/lib/nanoc3/helpers/text.rb +40 -0
  92. data/lib/nanoc3/helpers/xml_sitemap.rb +69 -0
  93. data/lib/nanoc3/helpers.rb +16 -0
  94. data/lib/nanoc3/package.rb +106 -0
  95. data/lib/nanoc3/tasks/clean.rake +16 -0
  96. data/lib/nanoc3/tasks/clean.rb +33 -0
  97. data/lib/nanoc3/tasks/deploy/rsync.rake +11 -0
  98. data/lib/nanoc3/tasks/validate.rake +35 -0
  99. data/lib/nanoc3/tasks.rb +9 -0
  100. data/lib/nanoc3.rb +19 -0
  101. data/vendor/cri/ChangeLog +0 -0
  102. data/vendor/cri/LICENSE +19 -0
  103. data/vendor/cri/NEWS +0 -0
  104. data/vendor/cri/README +4 -0
  105. data/vendor/cri/Rakefile +25 -0
  106. data/vendor/cri/lib/cri/base.rb +153 -0
  107. data/vendor/cri/lib/cri/command.rb +105 -0
  108. data/vendor/cri/lib/cri/core_ext/string.rb +41 -0
  109. data/vendor/cri/lib/cri/core_ext.rb +8 -0
  110. data/vendor/cri/lib/cri/option_parser.rb +186 -0
  111. data/vendor/cri/lib/cri.rb +12 -0
  112. data/vendor/cri/test/test_base.rb +6 -0
  113. data/vendor/cri/test/test_command.rb +6 -0
  114. data/vendor/cri/test/test_core_ext.rb +21 -0
  115. data/vendor/cri/test/test_option_parser.rb +279 -0
  116. metadata +225 -0
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3
4
+
5
+ # Nanoc3::CompilerDSL contains methods that will be executed by the site's
6
+ # rules file.
7
+ class CompilerDSL
8
+
9
+ # Creates a new compiler DSL for the given compiler.
10
+ def initialize(site)
11
+ @site = site
12
+ end
13
+
14
+ # Creates a preprocessor block that will be executed after all data is
15
+ # loaded, but before the site is compiled.
16
+ def preprocess(&block)
17
+ @site.preprocessor = block
18
+ end
19
+
20
+ # Creates a compilation rule for all items whose identifier match the
21
+ # given identifier, which may either be a string containing the *
22
+ # wildcard, or a regular expression.
23
+ #
24
+ # This rule will be applicable to reps with a name equal to "default"
25
+ # unless an explicit :rep parameter is given.
26
+ #
27
+ # An item rep will be compiled by calling the given block and passing the
28
+ # rep as a block argument.
29
+ #
30
+ # Example:
31
+ #
32
+ # compile '/foo/*' do
33
+ # rep.filter :erb
34
+ # end
35
+ #
36
+ # compile '/bar/*', :rep => 'raw' do
37
+ # # do nothing
38
+ # end
39
+ def compile(identifier, params={}, &block)
40
+ # Require block
41
+ raise ArgumentError.new("#compile requires a block") unless block_given?
42
+
43
+ # Get rep name
44
+ rep_name = params[:rep] || :default
45
+
46
+ # Create rule
47
+ rule = Rule.new(identifier_to_regex(identifier), rep_name, block)
48
+ @site.compiler.item_compilation_rules << rule
49
+ end
50
+
51
+ # Creates a routing rule for all items whose identifier match the
52
+ # given identifier, which may either be a string containing the *
53
+ # wildcard, or a regular expression.
54
+ #
55
+ # This rule will be applicable to reps with a name equal to "default";
56
+ # this can be changed by givign an explicit :rep parameter.
57
+ #
58
+ # The path of an item rep will be determined by calling the given block
59
+ # and passing the rep as a block argument.
60
+ #
61
+ # Example:
62
+ #
63
+ # route '/foo/*' do
64
+ # '/blahblah' + rep.item.identifier + 'index.html'
65
+ # end
66
+ #
67
+ # route '/bar/*', :rep => 'raw' do
68
+ # '/blahblah' + rep.item.identifier + 'index.txt'
69
+ # end
70
+ def route(identifier, params={}, &block)
71
+ # Require block
72
+ raise ArgumentError.new("#route requires a block") unless block_given?
73
+
74
+ # Get rep name
75
+ rep_name = params[:rep] || :default
76
+
77
+ # Create rule
78
+ rule = Rule.new(identifier_to_regex(identifier), rep_name, block)
79
+ @site.compiler.item_routing_rules << rule
80
+ end
81
+
82
+ # Creates a layout rule for all layouts whose identifier match the given
83
+ # identifier, which may either be a string containing the * wildcard, or a
84
+ # regular expression. The layouts matching the identifier will be filtered
85
+ # using the filter specified in the second argument. The params hash
86
+ # contains filter arguments that will be passed to the filter.
87
+ #
88
+ # Example:
89
+ #
90
+ # layout '/default/', :erb
91
+ # layout '/custom/', :haml, :format => :html5
92
+ def layout(identifier, filter_name, params={})
93
+ @site.compiler.layout_filter_mapping[identifier_to_regex(identifier)] = [ filter_name, params ]
94
+ end
95
+
96
+ private
97
+
98
+ # Converts the given identifier, which can contain the '*' wildcard, to a regex.
99
+ # For example, 'foo/*/bar' is transformed into /^foo\/(.*?)\/bar$/.
100
+ def identifier_to_regex(identifier)
101
+ if identifier.is_a? String
102
+ /^#{identifier.cleaned_identifier.gsub('*', '(.*?)')}?$/
103
+ else
104
+ identifier
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::ArrayExtensions
4
+
5
+ def symbolize_keys
6
+ inject([]) do |array, element|
7
+ array + [ element.respond_to?(:symbolize_keys) ? element.symbolize_keys : element ]
8
+ end
9
+ end
10
+
11
+ def stringify_keys
12
+ inject([]) do |array, element|
13
+ array + [ element.respond_to?(:stringify_keys) ? element.symbolize_keys : element ]
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ class Array
20
+ include Nanoc3::ArrayExtensions
21
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::HashExtensions
4
+
5
+ # Returns a new hash where all keys are recursively converted into symbols.
6
+ def symbolize_keys
7
+ inject({}) do |hash, (key, value)|
8
+ hash.merge(key.to_sym => value.respond_to?(:symbolize_keys) ? value.symbolize_keys : value)
9
+ end
10
+ end
11
+
12
+ # Returns a new hash where all keys are recursively converted to strings.
13
+ def stringify_keys
14
+ inject({}) do |hash, (key, value)|
15
+ hash.merge(key.to_s => value.respond_to?(:stringify_keys) ? value.stringify_keys : value)
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ class Hash
22
+ include Nanoc3::HashExtensions
23
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::StringExtensions
4
+
5
+ # Transforms string into an actual identifier
6
+ def cleaned_identifier
7
+ "/#{self}/".gsub(/^\/+|\/+$/, '/')
8
+ end
9
+
10
+ end
11
+
12
+ class String
13
+ include Nanoc3::StringExtensions
14
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nanoc3/base/core_ext/array'
4
+ require 'nanoc3/base/core_ext/hash'
5
+ require 'nanoc3/base/core_ext/string'
@@ -0,0 +1,197 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3
4
+
5
+ # Nanoc3::DataSource is responsible for loading data. It is the (abstract)
6
+ # superclass for all data sources. Subclasses must at least implement the
7
+ # data reading methods (+items+, +layouts+, and +code_snippets+); all other
8
+ # methods involving data manipulation are optional.
9
+ #
10
+ # Apart from the methods for loading and storing data, there are the +up+
11
+ # and +down+ methods for bringing up and tearing down the connection to the
12
+ # data source. These should be overridden in subclasses. The +loading+
13
+ # method wraps +up+ and +down+.
14
+ #
15
+ # The +setup+ method is used for setting up a site's data source for the
16
+ # first time. This method should be overridden in subclasses.
17
+ class DataSource < Plugin
18
+
19
+ # A string containing the root where items returned by this data source
20
+ # should be mounted.
21
+ attr_reader :items_root
22
+
23
+ # A string containing the root where layouts returned by this data source
24
+ # should be mounted.
25
+ attr_reader :layouts_root
26
+
27
+ # A hash containing the configuration for this data source. For example,
28
+ # online data sources could contain authentication details.
29
+ attr_reader :config
30
+
31
+ # Creates a new data source for the given site.
32
+ #
33
+ # +site+:: The site this data source belongs to.
34
+ # +items_root+:: The prefix that should be given to all items returned by
35
+ # the #items method (comparable to mount points for
36
+ # filesystems in Unix-ish OSes).
37
+ # +layouts_root+:: The prefix that should be given to all layouts returned
38
+ # by the #layouts method (comparable to mount points for
39
+ # filesystems in Unix-ish OSes).
40
+ # +config+:: The configuration for this data source.
41
+ def initialize(site, items_root, layouts_root, config)
42
+ @site = site
43
+ @items_root = items_root
44
+ @layouts_root = layouts_root
45
+ @config = config
46
+
47
+ @references = 0
48
+ end
49
+
50
+ # Sets the identifiers for this data source.
51
+ def self.identifiers(*identifiers)
52
+ Nanoc3::DataSource.register(self, *identifiers)
53
+ end
54
+
55
+ # Sets the identifier for this data source.
56
+ def self.identifier(identifier)
57
+ Nanoc3::DataSource.register(self, identifier)
58
+ end
59
+
60
+ # Registers the given class as a data source with the given identifier.
61
+ def self.register(class_or_name, *identifiers)
62
+ Nanoc3::Plugin.register(Nanoc3::DataSource, class_or_name, *identifiers)
63
+ end
64
+
65
+ # Loads the data source when necessary (calling +up+), yields, and unloads
66
+ # the data source when it is not being used elsewhere. All data source
67
+ # queries and data manipulations should be wrapped in a +loading+ block;
68
+ # it ensures that the data source is loaded when necessary and makes sure
69
+ # the data source does not get unloaded while it is still being used
70
+ # elsewhere.
71
+ def loading
72
+ use
73
+ yield
74
+ ensure
75
+ unuse
76
+ end
77
+
78
+ # Marks the data source as used by the caller.
79
+ #
80
+ # Calling this method increases the internal reference count. When the
81
+ # data source is used for the first time (first #use call), the data
82
+ # source will be loaded (#up will be called). Similarly, when the
83
+ # reference count reaches zero, the data source will be unloaded (#down
84
+ # will be called).
85
+ def use
86
+ up if @references == 0
87
+ @references += 1
88
+ end
89
+
90
+ # Marks the data source as unused by the caller.
91
+ #
92
+ # Calling this method increases the internal reference count. When the
93
+ # data source is used for the first time (first #use call), the data
94
+ # source will be loaded (#up will be called). Similarly, when the
95
+ # reference count reaches zero, the data source will be unloaded (#down
96
+ # will be called).
97
+ def unuse
98
+ @references -= 1
99
+ down if @references == 0
100
+ end
101
+
102
+ ########## Loading and unloading
103
+
104
+ # Brings up the connection to the data. This is an abstract method
105
+ # implemented by the subclass. Depending on the way data is stored, this
106
+ # may not be necessary. This is the ideal place to connect to the
107
+ # database, for example.
108
+ #
109
+ # Subclasses may implement this method.
110
+ def up
111
+ end
112
+
113
+ # Brings down the connection to the data. This is an abstract method
114
+ # implemented by the subclass. This method should undo the effects of
115
+ # +up+.
116
+ #
117
+ # Subclasses may implement this method.
118
+ def down
119
+ end
120
+
121
+ ########## Creating/updating
122
+
123
+ # Creates the bare minimum essentials for this data source to work. This
124
+ # action will likely be destructive. This method should not create sample
125
+ # data such as a default home page, a default layout, etc. For example, if
126
+ # you're using a database, this is where you should create the necessary
127
+ # tables for the data source to function properly.
128
+ #
129
+ # Subclasses must implement this method.
130
+ def setup
131
+ not_implemented('setup')
132
+ end
133
+
134
+ # Updated the content stored in this site to a newer version. A newer
135
+ # version of a data source may store content in a different format, and
136
+ # this method will update the stored content to this newer format.
137
+ #
138
+ # Subclasses may implement this method.
139
+ def update
140
+ end
141
+
142
+ ########## Loading data
143
+
144
+ # Returns the list of items (represented by Nanoc3::Item) in this site.
145
+ # The default implementation simply returns an empty array.
146
+ #
147
+ # Subclasses should not prepend items_root to the item's identifiers, as
148
+ # this will be done automatically.
149
+ #
150
+ # Subclasses may implement this method.
151
+ def items
152
+ []
153
+ end
154
+
155
+ # Returns the list of layouts (represented by Nanoc3::Layout) in this
156
+ # site. The default implementation simply returns an empty array.
157
+ #
158
+ # Subclasses should prepend layout_root to the layout's identifiers, since
159
+ # this is not done automatically.
160
+ #
161
+ # Subclasses may implement this method.
162
+ def layouts
163
+ []
164
+ end
165
+
166
+ # Returns the custom code snippets (represented by Nanoc3::CodeSnippet)
167
+ # for this site. The default implementation simply returns an empty array.
168
+ # This can be code for custom filters and more, but pretty much any code
169
+ # can be put in there (global helper functions are very useful).
170
+ #
171
+ # Subclasses may implement this method.
172
+ def code_snippets
173
+ []
174
+ end
175
+
176
+ ########## Creating data
177
+
178
+ # Creates a new item with the given content, attributes and identifier.
179
+ def create_item(content, attributes, identifier)
180
+ not_implemented('create_item')
181
+ end
182
+
183
+ # Creates a new layout with the given content, attributes and identifier.
184
+ def create_layout(content, attributes, identifier)
185
+ not_implemented('create_layout')
186
+ end
187
+
188
+ private
189
+
190
+ def not_implemented(name)
191
+ raise NotImplementedError.new(
192
+ "#{self.class} does not implement ##{name}"
193
+ )
194
+ end
195
+
196
+ end
197
+ end
@@ -0,0 +1,291 @@
1
+ # encoding: utf-8
2
+
3
+ require 'pstore'
4
+
5
+ module Nanoc3
6
+
7
+ # Nanoc3::DependencyTracker is responsible for remembering dependencies
8
+ # between items. It is used to speed up compilation by only letting an item
9
+ # be recompiled when it is outdated or any of its dependencies (or
10
+ # dependencies' dependencies, etc) is outdated.
11
+ #
12
+ # The dependencies tracked by the dependency tracker are not dependencies
13
+ # based on an item's content. When one item uses an attribute of another
14
+ # item, then this is also treated as a dependency. While dependencies based
15
+ # on an item's content (handled in Nanoc3::Compiler) cannot be mutually
16
+ # recursive, the more general dependencies in Nanoc3::DependencyTracker can
17
+ # (e.g. item A can use an attribute of item B and vice versa without
18
+ # problems).
19
+ class DependencyTracker
20
+
21
+ attr_accessor :filename
22
+
23
+ # FIXME The way the graph is stored is not exactly great. An adjacency
24
+ # matrix (wrapped in a Graph class, perhaps) would be a lot easier to work
25
+ # with, and it would not require an inverse graph to be maintained.
26
+
27
+ # Creates a new dependency tracker for the given items.
28
+ def initialize(items)
29
+ @items = items
30
+
31
+ @filename = 'tmp/dependencies'
32
+
33
+ @graph = {}
34
+ @inverse_graph = {}
35
+ end
36
+
37
+ # Starts listening for dependency messages (+:visit_started+ and
38
+ # +:visit_ended+) and start recording dependencies.
39
+ def start
40
+ # Initialize dependency stack. An item will be pushed onto this stack
41
+ # when it is visited. Therefore, an item on the stack always depends on
42
+ # all items pushed above it.
43
+ @stack = []
44
+
45
+ # Register start of visits
46
+ Nanoc3::NotificationCenter.on(:visit_started, self) do |item|
47
+ # Record possible dependency
48
+ unless @stack.empty?
49
+ self.record_dependency(@stack[-1], item)
50
+ end
51
+
52
+ @stack.push(item)
53
+ end
54
+
55
+ # Register end of visits
56
+ Nanoc3::NotificationCenter.on(:visit_ended, self) do |item|
57
+ @stack.pop
58
+ end
59
+ end
60
+
61
+ # Stop listening for dependency messages and stop recording dependencies.
62
+ def stop
63
+ # Unregister
64
+ Nanoc3::NotificationCenter.remove(:visit_started, self)
65
+ Nanoc3::NotificationCenter.remove(:visit_ended, self)
66
+ end
67
+
68
+ # Returns the direct dependencies for +item+, i.e. the items that, when
69
+ # outdated, will cause +item+ to be marked as outdated. Indirect
70
+ # dependencies will not be returned (e.g. if A depends on B which depends
71
+ # on C, then the direct dependencies of A do not include C).
72
+ def direct_dependencies_for(item)
73
+ @graph[item] || []
74
+ end
75
+
76
+ # Returns all dependencies (direct and indirect) for +item+, i.e. the
77
+ # items that, when outdated, will cause +item+ to be marked as outdated.
78
+ def all_dependencies_for(item)
79
+ # FIXME can result in an infinite loop
80
+
81
+ direct_dependencies = direct_dependencies_for(item)
82
+ indirect_dependencies = direct_dependencies.map { |i| all_dependencies_for(i) }
83
+
84
+ (direct_dependencies + indirect_dependencies).flatten
85
+ end
86
+
87
+ # Returns the direct inverse dependencies for +item+, i.e. the items that
88
+ # will be marked as outdated when +item+ is outdated. Indirect
89
+ # dependencies will not be returned (e.g. if A depends on B which depends
90
+ # on C, then the direct inverse dependencies of C do not include A).
91
+ def direct_inverse_dependencies_for(item)
92
+ inverted_graph[item] || []
93
+ end
94
+
95
+ # Returns all inverse dependencies (direct and indirect) for +item+, i.e.
96
+ # the items that will be marked as outdated when +item+ is outdated.
97
+ def all_inverse_dependencies_for(item)
98
+ # Init list of all found dependencies
99
+ all_dependencies = []
100
+
101
+ # Init lists with already checked and not yet checked dependencies
102
+ checked_direct_dependencies = []
103
+ pending_direct_dependencies = direct_inverse_dependencies_for(item)
104
+
105
+ while !pending_direct_dependencies.empty?
106
+ # Get next unchecked dependency
107
+ dependency = pending_direct_dependencies.shift
108
+ next if checked_direct_dependencies.include?(dependency)
109
+
110
+ # Add dependencies of this unchecked dependency
111
+ pending_direct_dependencies += direct_inverse_dependencies_for(dependency)
112
+
113
+ # Mark this dependency as handled
114
+ all_dependencies << dependency
115
+ checked_direct_dependencies << dependency
116
+ end
117
+
118
+ all_dependencies
119
+ end
120
+
121
+ # Records a dependency from +src+ to +dst+ in the dependency graph. When
122
+ # +dst+ is oudated, +src+ will also become outdated.
123
+ def record_dependency(src, dst)
124
+ # Initialize graph if necessary
125
+ @graph[src] ||= []
126
+
127
+ # Don't include self or doubles in dependencies
128
+ return if src == dst
129
+ return if @graph[src].include?(dst)
130
+
131
+ # Record dependency
132
+ invalidate_inverted_graph
133
+ @graph[src] << dst
134
+ end
135
+
136
+ # Stores the dependency graph into the file specified by the +filename+
137
+ # attribute.
138
+ def store_graph
139
+ # Create dir
140
+ FileUtils.mkdir_p(File.dirname(self.filename))
141
+
142
+ # Complete the graph
143
+ complete_graph
144
+
145
+ # Convert graph of items into graph of item identifiers
146
+ new_graph = {}
147
+ @graph.each_pair do |second_item, first_items|
148
+ # Don't store nil because that would be pointless (if first_item is
149
+ # outdated, something that does not exist is also outdated… makes no
150
+ # sense).
151
+ # FIXME can second_item really be nil?
152
+ next if second_item.nil?
153
+
154
+ new_graph[second_item.identifier] = first_items.map { |f| f && f.identifier }.compact
155
+ end
156
+
157
+ # Store dependencies
158
+ store = PStore.new(self.filename)
159
+ store.transaction do
160
+ store[:dependencies] = new_graph
161
+ end
162
+ end
163
+
164
+ # Loads the dependency graph from the file specified by the +filename+
165
+ # attribute. This method will overwrite an existing dependency graph.
166
+ def load_graph
167
+ # Create new graph
168
+ @graph = {}
169
+
170
+ # Don't do anything if dependencies haven't been stored yet
171
+ return if !File.file?(self.filename)
172
+
173
+ # Load dependencies
174
+ store = PStore.new(self.filename)
175
+ store.transaction do
176
+ # Convert graph of identifiers into graph of items
177
+ store[:dependencies].each_pair do |second_item_identifier, first_item_identifiers|
178
+ # Convert second and first item identifiers into items
179
+ second_item = item_with_identifier(second_item_identifier)
180
+ first_items = first_item_identifiers.map { |p| item_with_identifier(p) }
181
+
182
+ @graph[second_item] = first_items
183
+ end
184
+ end
185
+ end
186
+
187
+ # Traverses the dependency graph and marks all items that (directly or
188
+ # indirectly) depend on an outdated item as outdated.
189
+ def mark_outdated_items
190
+ # Unmark everything
191
+ @items.each { |i| i.dependencies_outdated = false }
192
+
193
+ # Mark items that appear in @items but not in the dependency graph
194
+ added_items = @items - @graph.keys
195
+ added_items.each { |i| i.dependencies_outdated = true }
196
+
197
+ # Walk graph and mark items as outdated if necessary
198
+ # (#keys and #sort is used instead of #each_pair to add determinism)
199
+ first_items = inverted_graph.keys.sort_by { |i| i.nil? ? '/' : i.identifier }
200
+ something_changed = true
201
+ while something_changed
202
+ something_changed = false
203
+
204
+ first_items.each do |first_item|
205
+ second_items = inverted_graph[first_item]
206
+
207
+ if first_item.nil? || # item was removed
208
+ first_item.outdated? || # item itself is outdated
209
+ first_item.dependencies_outdated? # item is outdated because of its dependencies
210
+ second_items.each do |item|
211
+ # Ignore this item
212
+ next if item.nil?
213
+
214
+ something_changed = true if !item.dependencies_outdated?
215
+ item.dependencies_outdated = true
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ # Empties the list of dependencies for the given item. This is necessary
223
+ # before recompiling the given item, because otherwise old dependencies
224
+ # will stick around and new dependencies will appear twice.
225
+ def forget_dependencies_for(item)
226
+ @graph[item] = []
227
+ end
228
+
229
+ private
230
+
231
+ # Returns the item with the given identifier, or nil if no item is found.
232
+ def item_with_identifier(identifier)
233
+ @items.find { |i| i.identifier == identifier }
234
+ end
235
+
236
+ # Returns the inverted dependency graph, creating it first if it does not
237
+ # exist yet or is outdated. In this graph, the keys will be outdated when
238
+ # any of the values are outdated.
239
+ def inverted_graph
240
+ @inverted_graph ||= invert_graph(@graph)
241
+ end
242
+
243
+ # Marks the inverted graph as outdated so that it will be regenerated the
244
+ # next time it is used.
245
+ def invalidate_inverted_graph
246
+ @inverted_graph = nil
247
+ end
248
+
249
+ # Inverts the given graph (keys become values and values become keys).
250
+ #
251
+ # For example, this graph
252
+ #
253
+ # {
254
+ # :a => [ :b, :c ],
255
+ # :b => [ :x, :c ]
256
+ # }
257
+ #
258
+ # is turned into
259
+ #
260
+ # {
261
+ # :b => [ :a ],
262
+ # :c => [ :a, :b ],
263
+ # :x => [ :b ]
264
+ # }
265
+ def invert_graph(graph)
266
+ inverted_graph = {}
267
+
268
+ graph.each_pair do |key, values|
269
+ values.each do |v|
270
+ inverted_graph[v] ||= []
271
+ inverted_graph[v] << key
272
+ end
273
+ end
274
+
275
+ inverted_graph
276
+ end
277
+
278
+ # Ensures that all items in the dependency graph have a list of
279
+ # dependecies, even if it is empty. Items without a list of dependencies
280
+ # will be treated as "added" and will depend on all other pages, which is
281
+ # not necessary for non-added items.
282
+ def complete_graph
283
+ @items.each do |item|
284
+ @graph[item] ||= []
285
+ end
286
+
287
+ end
288
+
289
+ end
290
+
291
+ end