nanoc 3.6.2 → 3.6.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/Architecture.md +70 -0
- data/Gemfile.lock +44 -35
- data/NEWS.md +21 -6
- data/TODO.md +95 -0
- data/lib/nanoc.rb +1 -1
- data/lib/nanoc/base/compilation/compiler_dsl.rb +5 -1
- data/lib/nanoc/cli/command_runner.rb +2 -2
- data/lib/nanoc/cli/commands/compile.rb +69 -24
- data/lib/nanoc/cli/commands/watch.rb +48 -15
- data/lib/nanoc/data_sources/filesystem.rb +7 -2
- data/lib/nanoc/data_sources/static.rb +9 -5
- data/lib/nanoc/extra.rb +1 -0
- data/lib/nanoc/extra/checking/checks/external_links.rb +15 -2
- data/lib/nanoc/extra/file_proxy.rb +1 -1
- data/lib/nanoc/extra/filesystem_tools.rb +124 -0
- data/lib/nanoc/filters/colorize_syntax.rb +4 -4
- data/lib/nanoc/filters/relativize_paths.rb +1 -8
- data/lib/nanoc/filters/uglify_js.rb +2 -22
- data/test/base/test_compiler_dsl.rb +30 -0
- data/test/cli/commands/test_compile.rb +27 -1
- data/test/cli/commands/test_watch.rb +10 -0
- data/test/data_sources/test_static.rb +28 -0
- data/test/extra/checking/checks/test_external_links.rb +27 -1
- data/test/extra/checking/checks/test_html.rb +1 -1
- data/test/extra/test_filesystem_tools.rb +100 -0
- data/test/filters/test_pandoc.rb +1 -1
- data/test/filters/test_relativize_paths.rb +23 -0
- data/test/filters/test_uglify_js.rb +8 -6
- metadata +6 -2
@@ -33,6 +33,18 @@ module Nanoc::CLI::Commands
|
|
33
33
|
# @abstract Subclasses must override {#start} and may override {#stop}.
|
34
34
|
class Listener
|
35
35
|
|
36
|
+
def initialize(params={})
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [Nanoc::CLI::CommandRunner] command_runner The command runner for this listener
|
40
|
+
#
|
41
|
+
# @return [Boolean] true if this listener should be enabled for the given command runner, false otherwise
|
42
|
+
#
|
43
|
+
# @abstract Returns `true` by default, but subclasses may override this.
|
44
|
+
def self.enable_for?(command_runner)
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
36
48
|
# Starts the listener. Subclasses should override this method and set up listener notifications.
|
37
49
|
#
|
38
50
|
# @return [void]
|
@@ -53,6 +65,11 @@ module Nanoc::CLI::Commands
|
|
53
65
|
# Generates diffs for every output file written
|
54
66
|
class DiffGenerator < Listener
|
55
67
|
|
68
|
+
# @see Listener#enable_for?
|
69
|
+
def self.enable_for?(command_runner)
|
70
|
+
command_runner.site.config[:enable_output_diff]
|
71
|
+
end
|
72
|
+
|
56
73
|
# @see Listener#start
|
57
74
|
def start
|
58
75
|
require 'tempfile'
|
@@ -138,6 +155,11 @@ module Nanoc::CLI::Commands
|
|
138
155
|
# Records the time spent per filter and per item representation
|
139
156
|
class TimingRecorder < Listener
|
140
157
|
|
158
|
+
# @see Listener#enable_for?
|
159
|
+
def self.enable_for?(command_runner)
|
160
|
+
command_runner.options.fetch(:verbose, false)
|
161
|
+
end
|
162
|
+
|
141
163
|
# @option params [Array<Nanoc::ItemRep>] :reps The list of item representations in the site
|
142
164
|
def initialize(params={})
|
143
165
|
@filter_times = {}
|
@@ -205,7 +227,7 @@ module Nanoc::CLI::Commands
|
|
205
227
|
tot = format('%5.2f', tot)
|
206
228
|
|
207
229
|
# Output stats
|
208
|
-
filter_name = format("%#{
|
230
|
+
filter_name = format("%#{max}s", filter_name)
|
209
231
|
puts "#{filter_name} | #{count} #{min}s #{avg}s #{max}s #{tot}s"
|
210
232
|
end
|
211
233
|
|
@@ -214,7 +236,12 @@ module Nanoc::CLI::Commands
|
|
214
236
|
# Controls garbage collection so that it only occurs once every 20 items
|
215
237
|
class GCController < Listener
|
216
238
|
|
217
|
-
|
239
|
+
# @see Listener#enable_for?
|
240
|
+
def self.enable_for?(command_runner)
|
241
|
+
! ENV.has_key?('TRAVIS')
|
242
|
+
end
|
243
|
+
|
244
|
+
def initialize(params={})
|
218
245
|
@gc_count = 0
|
219
246
|
end
|
220
247
|
|
@@ -241,6 +268,11 @@ module Nanoc::CLI::Commands
|
|
241
268
|
# Prints debug information (compilation started/ended, filtering started/ended, …)
|
242
269
|
class DebugPrinter < Listener
|
243
270
|
|
271
|
+
# @see Listener#enable_for?
|
272
|
+
def self.enable_for?(command_runner)
|
273
|
+
command_runner.debug?
|
274
|
+
end
|
275
|
+
|
244
276
|
# @see Listener#start
|
245
277
|
def start
|
246
278
|
Nanoc::NotificationCenter.on(:compilation_started) do |rep|
|
@@ -315,15 +347,23 @@ module Nanoc::CLI::Commands
|
|
315
347
|
|
316
348
|
end
|
317
349
|
|
350
|
+
def initialize(options, arguments, command, params={})
|
351
|
+
super(options, arguments, command)
|
352
|
+
@listener_classes = params.fetch(:listener_classes, self.default_listener_classes)
|
353
|
+
end
|
354
|
+
|
318
355
|
def run
|
356
|
+
time_before = Time.now
|
357
|
+
|
319
358
|
self.load_site
|
320
359
|
self.check_for_deprecated_usage
|
321
|
-
self.setup_listeners
|
322
360
|
|
323
361
|
puts "Compiling site…"
|
324
|
-
|
325
|
-
|
326
|
-
|
362
|
+
self.run_listeners_while do
|
363
|
+
self.site.compile
|
364
|
+
self.prune
|
365
|
+
end
|
366
|
+
|
327
367
|
time_after = Time.now
|
328
368
|
puts
|
329
369
|
puts "Site compiled in #{format('%.2f', time_after - time_before)}s."
|
@@ -337,28 +377,33 @@ module Nanoc::CLI::Commands
|
|
337
377
|
end
|
338
378
|
end
|
339
379
|
|
340
|
-
def
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
end
|
380
|
+
def default_listener_classes
|
381
|
+
[
|
382
|
+
Nanoc::CLI::Commands::Compile::DiffGenerator,
|
383
|
+
Nanoc::CLI::Commands::Compile::DebugPrinter,
|
384
|
+
Nanoc::CLI::Commands::Compile::TimingRecorder,
|
385
|
+
Nanoc::CLI::Commands::Compile::GCController,
|
386
|
+
Nanoc::CLI::Commands::Compile::FileActionPrinter
|
387
|
+
]
|
388
|
+
end
|
350
389
|
|
351
|
-
|
352
|
-
|
353
|
-
|
390
|
+
def setup_listeners
|
391
|
+
@listeners = @listener_classes.
|
392
|
+
select { |klass| klass.enable_for?(self) }.
|
393
|
+
map { |klass| klass.new(:reps => self.reps) }
|
354
394
|
|
355
|
-
|
356
|
-
|
357
|
-
end
|
395
|
+
@listeners.each { |s| s.start }
|
396
|
+
end
|
358
397
|
|
359
|
-
|
398
|
+
def listeners
|
399
|
+
@listeners
|
400
|
+
end
|
360
401
|
|
361
|
-
|
402
|
+
def run_listeners_while
|
403
|
+
self.setup_listeners
|
404
|
+
yield
|
405
|
+
ensure
|
406
|
+
self.teardown_listeners
|
362
407
|
end
|
363
408
|
|
364
409
|
def teardown_listeners
|
@@ -80,12 +80,12 @@ module Nanoc::CLI::Commands
|
|
80
80
|
rebuilder.call(removed[0]) if removed[0]
|
81
81
|
end
|
82
82
|
|
83
|
-
listener = Listen::
|
84
|
-
listener_root = Listen::
|
83
|
+
listener = Listen::Listener.new(*dirs_to_watch).change(&callback)
|
84
|
+
listener_root = Listen::Listener.new('.', :filter => files_to_watch, :ignore => ignore_dir).change(&callback)
|
85
85
|
|
86
86
|
begin
|
87
|
-
listener_root.start
|
88
|
-
listener.start
|
87
|
+
listener_root.start
|
88
|
+
listener.start!
|
89
89
|
rescue Interrupt
|
90
90
|
listener.stop
|
91
91
|
listener_root.stop
|
@@ -98,30 +98,47 @@ module Nanoc::CLI::Commands
|
|
98
98
|
# A list of commandline tool names that can be used to send notifications
|
99
99
|
TOOLS = %w( growlnotify notify-send ) unless defined? TOOLS
|
100
100
|
|
101
|
-
# The tool to use for discovering binaries' locations
|
102
|
-
FIND_BINARY_COMMAND = RUBY_PLATFORM =~ /mingw|mswin/ ? "where" : "which" unless defined? FIND_BINARY_COMMAND
|
103
|
-
|
104
101
|
# Send a notification. If no notifier is found, no notification will be
|
105
102
|
# created.
|
106
103
|
#
|
107
104
|
# @param [String] message The message to include in the notification
|
108
105
|
def notify(message)
|
109
106
|
return if tool.nil?
|
110
|
-
|
107
|
+
if tool == 'growlnotify' && self.on_windows?
|
108
|
+
self.growlnotify_windows(message)
|
109
|
+
else
|
110
|
+
send(tool.tr('-', '_'), message)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def have_tool_nix?(tool)
|
117
|
+
!`which #{tool}`.empty?
|
118
|
+
rescue Errno::ENOENT
|
119
|
+
false
|
120
|
+
end
|
121
|
+
|
122
|
+
def have_tool_windows?(tool)
|
123
|
+
!`where #{tool} 2> nul`.empty?
|
124
|
+
rescue Errno::ENOENT
|
125
|
+
false
|
111
126
|
end
|
112
127
|
|
113
|
-
|
128
|
+
def have_tool?(tool)
|
129
|
+
if self.on_windows?
|
130
|
+
self.have_tool_windows?(tool)
|
131
|
+
else
|
132
|
+
self.have_tool_nix?(tool)
|
133
|
+
end
|
134
|
+
end
|
114
135
|
|
115
136
|
def tool
|
116
137
|
@tool ||= begin
|
117
138
|
require 'terminal-notifier'
|
118
139
|
'terminal-notify'
|
119
140
|
rescue LoadError
|
120
|
-
|
121
|
-
TOOLS.find { |t| !`#{FIND_BINARY_COMMAND} #{t}`.empty? }
|
122
|
-
rescue Errno::ENOENT
|
123
|
-
nil
|
124
|
-
end
|
141
|
+
TOOLS.find { |t| have_tool?(t) }
|
125
142
|
end
|
126
143
|
end
|
127
144
|
|
@@ -129,14 +146,30 @@ module Nanoc::CLI::Commands
|
|
129
146
|
TerminalNotifier.notify(message, :title => "nanoc")
|
130
147
|
end
|
131
148
|
|
149
|
+
def growlnotify_cmd_for(message)
|
150
|
+
[ 'growlnotify', '-m', message ]
|
151
|
+
end
|
152
|
+
|
132
153
|
def growlnotify(message)
|
133
|
-
system(
|
154
|
+
system(*self.growlnotify_cmd_for(message))
|
155
|
+
end
|
156
|
+
|
157
|
+
def growlnotify_windows_cmd_for(message)
|
158
|
+
[ 'growlnotify', '/t:nanoc', message ]
|
159
|
+
end
|
160
|
+
|
161
|
+
def growlnotify_windows(message)
|
162
|
+
system(*self.growlnotify_windows_cmd_for(message))
|
134
163
|
end
|
135
164
|
|
136
165
|
def notify_send(message)
|
137
166
|
system('notify-send', message)
|
138
167
|
end
|
139
168
|
|
169
|
+
def on_windows?
|
170
|
+
!!(RUBY_PLATFORM =~ /(mingw|bccwin|wince|mswin32)/i)
|
171
|
+
end
|
172
|
+
|
140
173
|
end
|
141
174
|
|
142
175
|
end
|
@@ -52,7 +52,7 @@ module Nanoc::DataSources
|
|
52
52
|
create_object('layouts', content, attributes, identifier, params)
|
53
53
|
end
|
54
54
|
|
55
|
-
|
55
|
+
protected
|
56
56
|
|
57
57
|
# Creates a new object (item or layout) on disk in dir_name according to
|
58
58
|
# the given identifier. The file will have its attributes taken from the
|
@@ -144,7 +144,7 @@ module Nanoc::DataSources
|
|
144
144
|
# }
|
145
145
|
def all_split_files_in(dir_name)
|
146
146
|
# Get all good file names
|
147
|
-
filenames =
|
147
|
+
filenames = self.all_files_in(dir_name)
|
148
148
|
filenames.reject! { |fn| fn =~ /(~|\.orig|\.rej|\.bak)$/ }
|
149
149
|
|
150
150
|
# Group by identifier
|
@@ -173,6 +173,11 @@ module Nanoc::DataSources
|
|
173
173
|
grouped_filenames
|
174
174
|
end
|
175
175
|
|
176
|
+
# Returns all files in the given directory and directories below it.
|
177
|
+
def all_files_in(dir_name)
|
178
|
+
Nanoc::Extra::FilesystemTools.all_files_in(dir_name)
|
179
|
+
end
|
180
|
+
|
176
181
|
# Returns the filename for the given base filename and the extension.
|
177
182
|
#
|
178
183
|
# If the extension is nil, this function should return nil as well.
|
@@ -26,24 +26,21 @@ module Nanoc::DataSources
|
|
26
26
|
# exclude them from the Blogging helper's atom feed generator, among other
|
27
27
|
# things.
|
28
28
|
class Static < Nanoc::DataSource
|
29
|
+
|
29
30
|
identifier :static
|
30
31
|
|
31
32
|
def items
|
32
33
|
# Get prefix
|
33
34
|
prefix = config[:prefix] || 'static'
|
34
35
|
|
35
|
-
# Get all files under prefix dir
|
36
|
-
filenames = Dir[prefix + '/**/*'].select { |f| File.file?(f) }
|
37
|
-
|
38
36
|
# Convert filenames to items
|
39
|
-
|
37
|
+
self.all_files_in(prefix).map do |filename|
|
40
38
|
attributes = {
|
41
39
|
:extension => File.extname(filename)[1..-1],
|
42
40
|
:filename => filename,
|
43
41
|
}
|
44
42
|
attributes[:is_hidden] = true unless config[:hide_items] == false
|
45
43
|
identifier = filename[(prefix.length+1)..-1] + '/'
|
46
|
-
|
47
44
|
mtime = File.mtime(filename)
|
48
45
|
checksum = Pathname.new(filename).checksum
|
49
46
|
|
@@ -56,5 +53,12 @@ module Nanoc::DataSources
|
|
56
53
|
end
|
57
54
|
end
|
58
55
|
|
56
|
+
protected
|
57
|
+
|
58
|
+
def all_files_in(dir_name)
|
59
|
+
Nanoc::Extra::FilesystemTools.all_files_in(dir_name)
|
60
|
+
end
|
61
|
+
|
59
62
|
end
|
63
|
+
|
60
64
|
end
|
data/lib/nanoc/extra.rb
CHANGED
@@ -5,6 +5,7 @@ module Nanoc::Extra
|
|
5
5
|
autoload 'AutoCompiler', 'nanoc/extra/auto_compiler'
|
6
6
|
autoload 'Checking', 'nanoc/extra/checking'
|
7
7
|
autoload 'CHiCk', 'nanoc/extra/chick'
|
8
|
+
autoload 'FilesystemTools', 'nanoc/extra/filesystem_tools'
|
8
9
|
autoload 'LinkCollector', 'nanoc/extra/link_collector.rb'
|
9
10
|
autoload 'Pruner', 'nanoc/extra/pruner'
|
10
11
|
autoload 'Validators', 'nanoc/extra/validators'
|
@@ -136,9 +136,22 @@ module ::Nanoc::Extra::Checking::Checks
|
|
136
136
|
raise 'should not have gotten here'
|
137
137
|
end
|
138
138
|
|
139
|
+
def path_for_url(url)
|
140
|
+
if url.path.nil? || url.path.empty?
|
141
|
+
path = '/'
|
142
|
+
else
|
143
|
+
path = url.path
|
144
|
+
end
|
145
|
+
|
146
|
+
if url.query
|
147
|
+
path << '?' << url.query
|
148
|
+
end
|
149
|
+
|
150
|
+
path
|
151
|
+
end
|
152
|
+
|
139
153
|
def request_url_once(url, req_method = Net::HTTP::Head)
|
140
|
-
|
141
|
-
req = req_method.new(path)
|
154
|
+
req = req_method.new(self.path_for_url(url))
|
142
155
|
http = Net::HTTP.new(url.host, url.port)
|
143
156
|
if url.instance_of? URI::HTTPS
|
144
157
|
http.use_ssl = true
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Nanoc::Extra
|
4
|
+
|
5
|
+
# Contains useful functions for managing the filesystem.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
module FilesystemTools
|
9
|
+
|
10
|
+
# Error that is raised when too many symlink indirections are encountered.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class MaxSymlinkDepthExceededError < ::Nanoc::Errors::GenericTrivial
|
14
|
+
|
15
|
+
# @return [String] The last filename that was attempted to be
|
16
|
+
# resolved before giving up
|
17
|
+
attr_reader :filename
|
18
|
+
|
19
|
+
# @param [String] filename The last filename that was attempted to be
|
20
|
+
# resolved before giving up
|
21
|
+
def initialize(filename)
|
22
|
+
@filename = filename
|
23
|
+
super("Too many indirections while resolving symlinks. I gave up after finding out #{filename} was yet another symlink. Sorry!")
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
# Error that is raised when a file of an unknown type is encountered
|
29
|
+
# (something other than file, directory or link).
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
class UnsupportedFileTypeError < ::Nanoc::Errors::GenericTrivial
|
33
|
+
|
34
|
+
# @return [String] The filename of the file whose type is not supported
|
35
|
+
attr_reader :filename
|
36
|
+
|
37
|
+
# @param [String] filename The filename of the file whose type is not
|
38
|
+
# supported
|
39
|
+
def initialize(filename)
|
40
|
+
@filename = filename
|
41
|
+
super("The file at #{filename} is of an unsupported type (expected file, directory or link, but it is #{File.ftype(filename)}")
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns all files in the given directory and directories below it,
|
47
|
+
# following symlinks up to a maximum of `recursion_limit` times.
|
48
|
+
#
|
49
|
+
# @param [String] dir_name The name of the directory whose contents to
|
50
|
+
# fetch
|
51
|
+
#
|
52
|
+
# @param [Integer] recursion_limit The maximum number of times to
|
53
|
+
# recurse into a symlink to a directory
|
54
|
+
#
|
55
|
+
# @return [Array<String>] A list of filenames
|
56
|
+
#
|
57
|
+
# @raise [MaxSymlinkDepthExceededError] if too many indirections are
|
58
|
+
# encountered while resolving symlinks
|
59
|
+
#
|
60
|
+
# @raise [UnsupportedFileTypeError] if a file of an unsupported type is
|
61
|
+
# detected (something other than file, directory or link)
|
62
|
+
def all_files_in(dir_name, recursion_limit=10)
|
63
|
+
Dir[dir_name + '/**/*'].map do |fn|
|
64
|
+
case File.ftype(fn)
|
65
|
+
when 'link'
|
66
|
+
if 0 == recursion_limit
|
67
|
+
raise MaxSymlinkDepthExceededError.new(fn)
|
68
|
+
else
|
69
|
+
absolute_target = self.resolve_symlink(fn)
|
70
|
+
if File.file?(absolute_target)
|
71
|
+
fn
|
72
|
+
else
|
73
|
+
all_files_in(absolute_target, recursion_limit-1).map do |sfn|
|
74
|
+
fn + sfn[absolute_target.size..-1]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
when 'file'
|
79
|
+
fn
|
80
|
+
when 'directory'
|
81
|
+
nil
|
82
|
+
else
|
83
|
+
raise UnsupportedFileTypeError.new(fn)
|
84
|
+
end
|
85
|
+
end.compact.flatten
|
86
|
+
end
|
87
|
+
module_function :all_files_in
|
88
|
+
|
89
|
+
# Resolves the given symlink into an absolute path.
|
90
|
+
#
|
91
|
+
# @param [String] filename The filename of the symlink to resolve
|
92
|
+
#
|
93
|
+
# @param [Integer] recursion_limit The maximum number of times to recurse
|
94
|
+
# into a symlink
|
95
|
+
#
|
96
|
+
# @return [String] The absolute resolved filename of the symlink target
|
97
|
+
#
|
98
|
+
# @raise [MaxSymlinkDepthExceededError] if too many indirections are
|
99
|
+
# encountered while resolving symlinks
|
100
|
+
#
|
101
|
+
# @raise [UnsupportedFileTypeError] if a file of an unsupported type is
|
102
|
+
# detected (something other than file, directory or link)
|
103
|
+
def resolve_symlink(filename, recursion_limit=5)
|
104
|
+
target = File.readlink(filename)
|
105
|
+
absolute_target = File.expand_path(target, File.dirname(filename))
|
106
|
+
|
107
|
+
case File.ftype(absolute_target)
|
108
|
+
when 'link'
|
109
|
+
if 0 == recursion_limit
|
110
|
+
raise MaxSymlinkDepthExceededError.new(absolute_target)
|
111
|
+
else
|
112
|
+
self.resolve_symlink(absolute_target, recursion_limit-1)
|
113
|
+
end
|
114
|
+
when 'file', 'directory'
|
115
|
+
absolute_target
|
116
|
+
else
|
117
|
+
raise UnsupportedFileTypeError.new(absolute_target)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
module_function :resolve_symlink
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|