nanoc 3.6.2 → 3.6.3
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.
- 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
|