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.
- 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
|