nanoc3 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +3 -0
- data/LICENSE +19 -0
- data/NEWS.rdoc +262 -0
- data/README.rdoc +80 -0
- data/Rakefile +11 -0
- data/bin/nanoc3 +16 -0
- data/lib/nanoc3/base/code_snippet.rb +42 -0
- data/lib/nanoc3/base/compiler.rb +225 -0
- data/lib/nanoc3/base/compiler_dsl.rb +110 -0
- data/lib/nanoc3/base/core_ext/array.rb +21 -0
- data/lib/nanoc3/base/core_ext/hash.rb +23 -0
- data/lib/nanoc3/base/core_ext/string.rb +14 -0
- data/lib/nanoc3/base/core_ext.rb +5 -0
- data/lib/nanoc3/base/data_source.rb +197 -0
- data/lib/nanoc3/base/dependency_tracker.rb +291 -0
- data/lib/nanoc3/base/errors.rb +95 -0
- data/lib/nanoc3/base/filter.rb +60 -0
- data/lib/nanoc3/base/item.rb +87 -0
- data/lib/nanoc3/base/item_rep.rb +236 -0
- data/lib/nanoc3/base/layout.rb +53 -0
- data/lib/nanoc3/base/notification_center.rb +68 -0
- data/lib/nanoc3/base/plugin.rb +88 -0
- data/lib/nanoc3/base/preprocessor_context.rb +37 -0
- data/lib/nanoc3/base/rule.rb +37 -0
- data/lib/nanoc3/base/rule_context.rb +68 -0
- data/lib/nanoc3/base/site.rb +334 -0
- data/lib/nanoc3/base.rb +25 -0
- data/lib/nanoc3/cli/base.rb +151 -0
- data/lib/nanoc3/cli/commands/autocompile.rb +89 -0
- data/lib/nanoc3/cli/commands/compile.rb +279 -0
- data/lib/nanoc3/cli/commands/create_item.rb +79 -0
- data/lib/nanoc3/cli/commands/create_layout.rb +94 -0
- data/lib/nanoc3/cli/commands/create_site.rb +320 -0
- data/lib/nanoc3/cli/commands/help.rb +71 -0
- data/lib/nanoc3/cli/commands/info.rb +114 -0
- data/lib/nanoc3/cli/commands/update.rb +96 -0
- data/lib/nanoc3/cli/commands.rb +13 -0
- data/lib/nanoc3/cli/logger.rb +73 -0
- data/lib/nanoc3/cli.rb +16 -0
- data/lib/nanoc3/data_sources/delicious.rb +66 -0
- data/lib/nanoc3/data_sources/filesystem.rb +231 -0
- data/lib/nanoc3/data_sources/filesystem_combined.rb +202 -0
- data/lib/nanoc3/data_sources/filesystem_common.rb +22 -0
- data/lib/nanoc3/data_sources/filesystem_compact.rb +232 -0
- data/lib/nanoc3/data_sources/last_fm.rb +103 -0
- data/lib/nanoc3/data_sources/twitter.rb +53 -0
- data/lib/nanoc3/data_sources.rb +20 -0
- data/lib/nanoc3/extra/auto_compiler.rb +97 -0
- data/lib/nanoc3/extra/chick.rb +119 -0
- data/lib/nanoc3/extra/context.rb +24 -0
- data/lib/nanoc3/extra/core_ext/time.rb +19 -0
- data/lib/nanoc3/extra/core_ext.rb +3 -0
- data/lib/nanoc3/extra/deployers/rsync.rb +64 -0
- data/lib/nanoc3/extra/deployers.rb +12 -0
- data/lib/nanoc3/extra/file_proxy.rb +31 -0
- data/lib/nanoc3/extra/validators/links.rb +0 -0
- data/lib/nanoc3/extra/validators/w3c.rb +71 -0
- data/lib/nanoc3/extra/validators.rb +12 -0
- data/lib/nanoc3/extra/vcs.rb +65 -0
- data/lib/nanoc3/extra/vcses/bazaar.rb +21 -0
- data/lib/nanoc3/extra/vcses/dummy.rb +20 -0
- data/lib/nanoc3/extra/vcses/git.rb +21 -0
- data/lib/nanoc3/extra/vcses/mercurial.rb +21 -0
- data/lib/nanoc3/extra/vcses/subversion.rb +21 -0
- data/lib/nanoc3/extra/vcses.rb +17 -0
- data/lib/nanoc3/extra.rb +16 -0
- data/lib/nanoc3/filters/bluecloth.rb +13 -0
- data/lib/nanoc3/filters/coderay.rb +17 -0
- data/lib/nanoc3/filters/erb.rb +19 -0
- data/lib/nanoc3/filters/erubis.rb +17 -0
- data/lib/nanoc3/filters/haml.rb +20 -0
- data/lib/nanoc3/filters/less.rb +13 -0
- data/lib/nanoc3/filters/markaby.rb +14 -0
- data/lib/nanoc3/filters/maruku.rb +14 -0
- data/lib/nanoc3/filters/rainpress.rb +13 -0
- data/lib/nanoc3/filters/rdiscount.rb +13 -0
- data/lib/nanoc3/filters/rdoc.rb +23 -0
- data/lib/nanoc3/filters/redcloth.rb +14 -0
- data/lib/nanoc3/filters/relativize_paths.rb +32 -0
- data/lib/nanoc3/filters/rubypants.rb +14 -0
- data/lib/nanoc3/filters/sass.rb +17 -0
- data/lib/nanoc3/filters.rb +37 -0
- data/lib/nanoc3/helpers/blogging.rb +226 -0
- data/lib/nanoc3/helpers/breadcrumbs.rb +25 -0
- data/lib/nanoc3/helpers/capturing.rb +71 -0
- data/lib/nanoc3/helpers/filtering.rb +46 -0
- data/lib/nanoc3/helpers/html_escape.rb +22 -0
- data/lib/nanoc3/helpers/link_to.rb +120 -0
- data/lib/nanoc3/helpers/rendering.rb +76 -0
- data/lib/nanoc3/helpers/tagging.rb +58 -0
- data/lib/nanoc3/helpers/text.rb +40 -0
- data/lib/nanoc3/helpers/xml_sitemap.rb +69 -0
- data/lib/nanoc3/helpers.rb +16 -0
- data/lib/nanoc3/package.rb +106 -0
- data/lib/nanoc3/tasks/clean.rake +16 -0
- data/lib/nanoc3/tasks/clean.rb +33 -0
- data/lib/nanoc3/tasks/deploy/rsync.rake +11 -0
- data/lib/nanoc3/tasks/validate.rake +35 -0
- data/lib/nanoc3/tasks.rb +9 -0
- data/lib/nanoc3.rb +19 -0
- data/vendor/cri/ChangeLog +0 -0
- data/vendor/cri/LICENSE +19 -0
- data/vendor/cri/NEWS +0 -0
- data/vendor/cri/README +4 -0
- data/vendor/cri/Rakefile +25 -0
- data/vendor/cri/lib/cri/base.rb +153 -0
- data/vendor/cri/lib/cri/command.rb +105 -0
- data/vendor/cri/lib/cri/core_ext/string.rb +41 -0
- data/vendor/cri/lib/cri/core_ext.rb +8 -0
- data/vendor/cri/lib/cri/option_parser.rb +186 -0
- data/vendor/cri/lib/cri.rb +12 -0
- data/vendor/cri/test/test_base.rb +6 -0
- data/vendor/cri/test/test_command.rb +6 -0
- data/vendor/cri/test/test_core_ext.rb +21 -0
- data/vendor/cri/test/test_option_parser.rb +279 -0
- 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,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
|