moco 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.txt +67 -0
- data/Rakefile +15 -0
- data/bin/moco +6 -0
- data/lib/moco/ansi_escape.rb +37 -0
- data/lib/moco/application.rb +109 -0
- data/lib/moco/browser.rb +64 -0
- data/lib/moco/browser_error.rb +105 -0
- data/lib/moco/compile_error.rb +66 -0
- data/lib/moco/compiler.rb +151 -0
- data/lib/moco/compiler_option.rb +60 -0
- data/lib/moco/compiler_register.rb +29 -0
- data/lib/moco/compilers/coffee_compiler.rb +70 -0
- data/lib/moco/compilers/haml_compiler.rb +21 -0
- data/lib/moco/compilers/less_compiler.rb +15 -0
- data/lib/moco/compilers/markdown_compiler.rb +139 -0
- data/lib/moco/compilers/sass_compiler.rb +25 -0
- data/lib/moco/file_util.rb +54 -0
- data/lib/moco/log.rb +108 -0
- data/lib/moco/monitor.rb +83 -0
- data/lib/moco/options.rb +336 -0
- data/lib/moco/source_map.rb +22 -0
- data/lib/moco/support/error/error.css +22 -0
- data/lib/moco/support/error/error.html +7 -0
- data/lib/moco/support/error/error.js +58 -0
- data/lib/moco/support/reload.scpt +0 -0
- data/lib/moco.rb +44 -0
- data/moco.gemspec +35 -0
- data/moco.rb +35 -0
- data/src/error/error.coffee +49 -0
- data/src/reload.applescript +135 -0
- data/test/ansi_escape_test.rb +52 -0
- data/test/application_test.rb +40 -0
- data/test/browser_error_test.rb +101 -0
- data/test/browser_test.rb +29 -0
- data/test/compile_error_test.rb +82 -0
- data/test/compiler_option_test.rb +121 -0
- data/test/compiler_register_test.rb +41 -0
- data/test/compiler_test.rb +243 -0
- data/test/compilers/coffee_compiler_test.rb +117 -0
- data/test/compilers/haml_compiler_test.rb +86 -0
- data/test/compilers/less_compiler_test.rb +72 -0
- data/test/compilers/markdown_compiler_test.rb +211 -0
- data/test/compilers/sass_compiler_test.rb +84 -0
- data/test/file_util_test.rb +37 -0
- data/test/fixtures/_color.scss +1 -0
- data/test/fixtures/color.less +1 -0
- data/test/fixtures/css_lib.rb +2 -0
- data/test/fixtures/html_lib.rb +2 -0
- data/test/fixtures/js_lib.rb +2 -0
- data/test/fixtures/layout.html +13 -0
- data/test/fixtures/moco.rb +8 -0
- data/test/fixtures/options_lib.rb +2 -0
- data/test/fixtures/source.txt +1 -0
- data/test/monitor_test.rb +68 -0
- data/test/options_test.rb +177 -0
- data/test/test_helper.rb +57 -0
- metadata +270 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
module MoCo
|
2
|
+
|
3
|
+
module CompilerOption
|
4
|
+
|
5
|
+
def self.convert(value)
|
6
|
+
case value
|
7
|
+
when nil, 'true'
|
8
|
+
true
|
9
|
+
when 'false'
|
10
|
+
false
|
11
|
+
when INTEGER
|
12
|
+
value.to_i
|
13
|
+
when FLOAT
|
14
|
+
value.to_f
|
15
|
+
when SYMBOL
|
16
|
+
value.delete(':').to_sym
|
17
|
+
when SINGLE_QUOTED, DOUBLE_QUOTED
|
18
|
+
strip_quotes(value)
|
19
|
+
when ARRAY
|
20
|
+
to_array(value)
|
21
|
+
else
|
22
|
+
value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
INTEGER = /^[+-]?\d+$/
|
29
|
+
FLOAT = /^[+-]?\d*\.\d+$/
|
30
|
+
SYMBOL = /^:[^:]+$/
|
31
|
+
ARRAY = /:/
|
32
|
+
SINGLE_QUOTED = /^'[^']*'$/
|
33
|
+
DOUBLE_QUOTED = /^"[^"]*"$/
|
34
|
+
|
35
|
+
def self.strip_quotes(value)
|
36
|
+
value[1..-2]
|
37
|
+
end
|
38
|
+
|
39
|
+
private_class_method :strip_quotes
|
40
|
+
|
41
|
+
def self.to_array(value)
|
42
|
+
values = value.split(':')
|
43
|
+
rejoin_symbols(values)
|
44
|
+
values.map { |value| convert(value) }
|
45
|
+
end
|
46
|
+
|
47
|
+
private_class_method :to_array
|
48
|
+
|
49
|
+
def self.rejoin_symbols(values)
|
50
|
+
values.each_cons(2) do |v1, v2|
|
51
|
+
v2.insert(0, ':') if v1.empty?
|
52
|
+
end
|
53
|
+
values.delete('')
|
54
|
+
end
|
55
|
+
|
56
|
+
private_class_method :rejoin_symbols
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module MoCo
|
4
|
+
|
5
|
+
class CompilerRegister
|
6
|
+
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@compilers = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def register(compiler, extension)
|
14
|
+
extension = FileUtil.normalized_extension(extension)
|
15
|
+
@compilers[extension] = compiler
|
16
|
+
end
|
17
|
+
|
18
|
+
def compiler_for(file)
|
19
|
+
extension = FileUtil.normalized_extension(file)
|
20
|
+
@compilers[extension]
|
21
|
+
end
|
22
|
+
|
23
|
+
def compilers
|
24
|
+
@compilers.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module MoCo
|
4
|
+
|
5
|
+
class CoffeeCompiler < JsCompiler
|
6
|
+
|
7
|
+
require_library 'runjs'
|
8
|
+
require_library 'coffee_script/source', 'coffee-script-source', '>= 1.6.2'
|
9
|
+
register 'coffee'
|
10
|
+
|
11
|
+
include SourceMap
|
12
|
+
|
13
|
+
def self.source_map_key
|
14
|
+
:sourceMap
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.context
|
18
|
+
@context ||= RunJS.context(File.read(CoffeeScript::Source.bundled_path))
|
19
|
+
end
|
20
|
+
|
21
|
+
def compiled_text
|
22
|
+
compiled_text, @source_map_text = compile_coffee(options)
|
23
|
+
compiled_text
|
24
|
+
end
|
25
|
+
|
26
|
+
def options
|
27
|
+
options = super
|
28
|
+
options[:filename] = source_file
|
29
|
+
source_map_options(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def context
|
35
|
+
self.class.context
|
36
|
+
end
|
37
|
+
|
38
|
+
def compile_coffee(options)
|
39
|
+
fn = 'CoffeeScript.compile'
|
40
|
+
js = context.apply(fn, 'CoffeeScript', source_text, options)
|
41
|
+
if options[:sourceMap]
|
42
|
+
source_map = js['v3SourceMap']
|
43
|
+
js = js['js'] + source_map_comment
|
44
|
+
end
|
45
|
+
[js, source_map]
|
46
|
+
rescue RunJS::JavaScriptError => e
|
47
|
+
raise e, pretty_error_message(e)
|
48
|
+
end
|
49
|
+
|
50
|
+
def source_map_comment
|
51
|
+
file = URI.escape(File.basename(source_map_file))
|
52
|
+
"\n/*\n//@ sourceMappingURL=#{file}\n*/\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
def pretty_error_message(error)
|
56
|
+
return error.message unless error['location']
|
57
|
+
fn = 'CoffeeScript.helpers.prettyErrorMessage'
|
58
|
+
context.call(fn, error.error, source_file, source_text, true)
|
59
|
+
end
|
60
|
+
|
61
|
+
def source_map_options(options)
|
62
|
+
return options unless options[:sourceMap]
|
63
|
+
{ :generatedFile => File.basename(compiled_file),
|
64
|
+
:sourceFiles => [FileUtil.relative_path(source_map_file, source_file)]
|
65
|
+
}.merge(options)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module MoCo
|
2
|
+
|
3
|
+
class HamlCompiler < HtmlCompiler
|
4
|
+
|
5
|
+
require_library 'haml'
|
6
|
+
register 'haml'
|
7
|
+
|
8
|
+
def compiled_text
|
9
|
+
Haml::Engine.new(source_text, options).render
|
10
|
+
rescue => e
|
11
|
+
e.instance_eval { @line += 1 if @line }
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
|
15
|
+
def options
|
16
|
+
super.merge(:filename => source_file)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module MoCo
|
2
|
+
|
3
|
+
class MarkdownCompiler < HtmlCompiler
|
4
|
+
|
5
|
+
if RUBY_VERSION < '1.9'
|
6
|
+
require_library 'redcarpet', 'redcarpet', '~> 2.0'
|
7
|
+
else
|
8
|
+
require_library 'redcarpet'
|
9
|
+
end
|
10
|
+
|
11
|
+
register 'markdown'
|
12
|
+
register 'md'
|
13
|
+
|
14
|
+
def self.set_option(key, value = true)
|
15
|
+
if key == :pygments
|
16
|
+
value = pygments_options(value)
|
17
|
+
end
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def compiled_text
|
22
|
+
if options[:layout]
|
23
|
+
layout(toc, body)
|
24
|
+
else
|
25
|
+
toc + body
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def self.pygments_options(value)
|
32
|
+
return unless value
|
33
|
+
unless Hash === value
|
34
|
+
key, *value = Array(value)
|
35
|
+
value = value.first if value.size < 2
|
36
|
+
value = true if value.nil?
|
37
|
+
value = { key => value }
|
38
|
+
value.delete(true)
|
39
|
+
end
|
40
|
+
options[:pygments] ? options[:pygments].merge(value) : value
|
41
|
+
end
|
42
|
+
|
43
|
+
private_class_method :pygments_options
|
44
|
+
|
45
|
+
def layout(toc, body)
|
46
|
+
layout = read_layout
|
47
|
+
layout = layout.gsub('{{TITLE}}', header(body) || file)
|
48
|
+
layout = layout.gsub('{{FILE}}', file)
|
49
|
+
if layout.include?('{{TOC}}')
|
50
|
+
layout.sub('{{TOC}}', toc.chop).sub('{{BODY}}', body)
|
51
|
+
else
|
52
|
+
layout.sub('{{BODY}}', toc + body)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_layout
|
57
|
+
File.read(options[:layout])
|
58
|
+
end
|
59
|
+
|
60
|
+
def header(body)
|
61
|
+
h1 = body[/<h1\b[^>]*>(.*?)<\/h1>/m, 1]
|
62
|
+
h1.gsub(/<[^>]+>/, '').gsub(/\s+/, ' ').strip if h1
|
63
|
+
end
|
64
|
+
|
65
|
+
def file
|
66
|
+
File.basename(source_file)
|
67
|
+
end
|
68
|
+
|
69
|
+
def toc
|
70
|
+
if options[:toc]
|
71
|
+
toc = render(Redcarpet::Render::HTML_TOC.new)
|
72
|
+
toc << "\n" unless toc.empty?
|
73
|
+
end
|
74
|
+
toc || ''
|
75
|
+
end
|
76
|
+
|
77
|
+
def body
|
78
|
+
if options[:pygments]
|
79
|
+
renderer = pygments_renderer(options[:pygments])
|
80
|
+
else
|
81
|
+
renderer = Redcarpet::Render::HTML
|
82
|
+
end
|
83
|
+
options = render_options
|
84
|
+
render(renderer.new(options), options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def render(renderer, extensions = {})
|
88
|
+
renderer.extend(Redcarpet::Render::SmartyPants) if options[:smarty]
|
89
|
+
Redcarpet::Markdown.new(renderer, extensions).render(source_text)
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_options
|
93
|
+
options = options().dup
|
94
|
+
options[:with_toc_data] = true if options[:toc]
|
95
|
+
options[:fenced_code_blocks] = true if options[:pygments]
|
96
|
+
[:pygments, :smarty, :toc, :layout, :title].each do |key|
|
97
|
+
options.delete(key)
|
98
|
+
end
|
99
|
+
options
|
100
|
+
end
|
101
|
+
|
102
|
+
def pygments_renderer(options)
|
103
|
+
|
104
|
+
require_library('pygments', 'pygments.rb') unless defined? Pygments
|
105
|
+
|
106
|
+
Class.new(Redcarpet::Render::HTML) do
|
107
|
+
|
108
|
+
define_method(:options) do
|
109
|
+
options.dup
|
110
|
+
end
|
111
|
+
|
112
|
+
def block_code(code, language)
|
113
|
+
lexer = Pygments::Lexer.find(language) if language
|
114
|
+
if lexer
|
115
|
+
"\n" + lexer.highlight(code, :options => options) + "\n"
|
116
|
+
else
|
117
|
+
klass = %( class="#{escape_html(language)}") if language
|
118
|
+
"\n<pre><code#{klass}>#{escape_html(code)}</code></pre>\n"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def escape_html(text)
|
123
|
+
escaped = {
|
124
|
+
"'" => ''',
|
125
|
+
'<' => '<',
|
126
|
+
'&' => '&',
|
127
|
+
'>' => '>',
|
128
|
+
'"' => '"'
|
129
|
+
}
|
130
|
+
text.gsub(/['<&>"]/) { |char| escaped[char] }
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module MoCo
|
2
|
+
|
3
|
+
class SassCompiler < CssCompiler
|
4
|
+
|
5
|
+
require_library 'sass'
|
6
|
+
register 'sass'
|
7
|
+
register 'scss'
|
8
|
+
|
9
|
+
def compiled_text
|
10
|
+
Sass::Engine.new(source_text, options).render
|
11
|
+
rescue => e
|
12
|
+
e.instance_eval { alias :line :sass_line } if defined? e.sass_line
|
13
|
+
raise
|
14
|
+
end
|
15
|
+
|
16
|
+
def options
|
17
|
+
{ :syntax => (source_file =~ /\.sass$/) ? :sass : :scss,
|
18
|
+
:cache => false,
|
19
|
+
:read_cache => false
|
20
|
+
}.merge(super)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module MoCo
|
5
|
+
|
6
|
+
class FileUtil < File
|
7
|
+
|
8
|
+
def self.normalized_extension(file)
|
9
|
+
file = file.to_s
|
10
|
+
extension = extname(file)
|
11
|
+
extension = basename(file) if extension.empty?
|
12
|
+
extension.delete('.').strip
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.replace_extension(file, ext)
|
16
|
+
return file unless ext
|
17
|
+
file = file.chomp(extname(file))
|
18
|
+
unless ext.empty?
|
19
|
+
ext = '.' << ext unless ext.start_with?('.')
|
20
|
+
file << ext unless file.end_with?(ext)
|
21
|
+
end
|
22
|
+
file
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.replace_directory(file, dir)
|
26
|
+
return file unless dir
|
27
|
+
file = basename(file)
|
28
|
+
file = join(dir, file) unless dir.empty?
|
29
|
+
file
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.short_path(path)
|
33
|
+
home = File.expand_path('~')
|
34
|
+
path.sub(home, '~')
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.relative_path(from_file, to_file)
|
38
|
+
from = Pathname.new(from_file)
|
39
|
+
to = Pathname.new(to_file)
|
40
|
+
to.relative_path_from(from.dirname).to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.up_to_date?(file, compared_to_file)
|
44
|
+
exist?(file) && mtime(file) >= mtime(compared_to_file)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.write(file, text)
|
48
|
+
FileUtils.makedirs(dirname(file))
|
49
|
+
open(file, 'w') { |f| f.write(text) }
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/lib/moco/log.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
module MoCo
|
2
|
+
|
3
|
+
module Log
|
4
|
+
|
5
|
+
@quiet = false
|
6
|
+
|
7
|
+
def self.quiet=(quiet)
|
8
|
+
@quiet = quiet
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(files)
|
12
|
+
files.each do |file|
|
13
|
+
log([:Loading, file, files])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.monitor
|
18
|
+
log
|
19
|
+
log('Press Ctrl-C to stop monitoring')
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.compile(compiler)
|
23
|
+
log
|
24
|
+
log([:Compile, compiler.source_file, files(compiler)])
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.update(compiler)
|
28
|
+
updated_files(compiler).each do |file|
|
29
|
+
log([:Updated, file, files(compiler)])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.error(e)
|
34
|
+
if @quiet
|
35
|
+
log(nil, true)
|
36
|
+
log([:Compile, e.file], true)
|
37
|
+
end
|
38
|
+
log(error_on_line(e), true)
|
39
|
+
log(error_message(e), true)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def self.log(status = nil, force = false)
|
45
|
+
return if @quiet && ! force
|
46
|
+
if Array === status
|
47
|
+
status, file, files = status
|
48
|
+
dir = FileUtil.short_path(File.dirname(file))
|
49
|
+
file = File.basename(file)
|
50
|
+
file = file.ljust(max_length(files)) if files
|
51
|
+
status = "#{status}: #{file} (#{dir})"
|
52
|
+
end
|
53
|
+
puts status || ''
|
54
|
+
end
|
55
|
+
|
56
|
+
private_class_method :log
|
57
|
+
|
58
|
+
def self.max_length(files)
|
59
|
+
files = files.map { |file| File.basename(file) }
|
60
|
+
files.max_by(&:length).length
|
61
|
+
end
|
62
|
+
|
63
|
+
private_class_method :max_length
|
64
|
+
|
65
|
+
def self.files(compiler)
|
66
|
+
[compiler.source_file] + compiled_files(compiler)
|
67
|
+
end
|
68
|
+
|
69
|
+
private_class_method :files
|
70
|
+
|
71
|
+
def self.compiled_files(compiler)
|
72
|
+
[compiler.compiled_file, source_map_file(compiler)].compact
|
73
|
+
end
|
74
|
+
|
75
|
+
private_class_method :compiled_files
|
76
|
+
|
77
|
+
def self.source_map_file(compiler)
|
78
|
+
klass = compiler.class
|
79
|
+
if klass < SourceMap && klass.options[klass.source_map_key]
|
80
|
+
compiler.source_map_file
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private_class_method :source_map_file
|
85
|
+
|
86
|
+
def self.updated_files(compiler)
|
87
|
+
compiled_files(compiler).select do |file|
|
88
|
+
FileUtil.up_to_date?(file, compiler.source_file)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private_class_method :updated_files
|
93
|
+
|
94
|
+
def self.error_on_line(e)
|
95
|
+
AnsiEscape.bold_red(e.line ? "Error on line #{e.line}:" : 'Error:')
|
96
|
+
end
|
97
|
+
|
98
|
+
private_class_method :error_on_line
|
99
|
+
|
100
|
+
def self.error_message(e)
|
101
|
+
$stdout.tty? ? e.message : AnsiEscape.unescape(e.message)
|
102
|
+
end
|
103
|
+
|
104
|
+
private_class_method :error_message
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
data/lib/moco/monitor.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'rb-fsevent'
|
2
|
+
require 'find'
|
3
|
+
|
4
|
+
module MoCo
|
5
|
+
|
6
|
+
class Monitor
|
7
|
+
|
8
|
+
def initialize(files, directories, extensions)
|
9
|
+
@files = files
|
10
|
+
@pattern = pattern(directories, extensions)
|
11
|
+
@directories = directories + files.map { |file| File.dirname(file) }
|
12
|
+
@directories = delete_nested(@directories)
|
13
|
+
end
|
14
|
+
|
15
|
+
def files
|
16
|
+
set_timestamps
|
17
|
+
@timestamps.keys
|
18
|
+
end
|
19
|
+
|
20
|
+
def monitor(&callback)
|
21
|
+
set_timestamps
|
22
|
+
options = { :no_defer => true, :latency => 0.1 }
|
23
|
+
fsevent = FSEvent.new
|
24
|
+
fsevent.watch(@directories, options) do |updated_dirs|
|
25
|
+
on_update(updated_dirs, &callback)
|
26
|
+
end
|
27
|
+
fsevent.run
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def pattern(dirs, exts)
|
33
|
+
return nil if dirs.empty? || exts.empty?
|
34
|
+
dirs = delete_nested(dirs.dup)
|
35
|
+
dirs = escape(dirs).join('|')
|
36
|
+
exts = escape(exts).join('|')
|
37
|
+
/^(#{dirs}).*\.(#{exts})$/
|
38
|
+
end
|
39
|
+
|
40
|
+
def escape(values)
|
41
|
+
values.map { |value| Regexp.escape(value.to_s) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete_nested(dirs)
|
45
|
+
dirs.delete_if do |nested_dir|
|
46
|
+
dirs.any? do |dir|
|
47
|
+
nested_dir != dir && nested_dir.start_with?(dir)
|
48
|
+
end
|
49
|
+
end.uniq
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_timestamps
|
53
|
+
@timestamps = {}
|
54
|
+
Find.find(*@directories) do |file|
|
55
|
+
store_timestamp(file) if monitor?(file)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def store_timestamp(file)
|
60
|
+
@timestamps[file] = File.mtime(file)
|
61
|
+
end
|
62
|
+
|
63
|
+
def monitor?(file)
|
64
|
+
@files.include?(file) || (@pattern && file =~ @pattern && File.file?(file))
|
65
|
+
end
|
66
|
+
|
67
|
+
def updated?(file)
|
68
|
+
@timestamps[file] != File.mtime(file)
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_update(dirs, &callback)
|
72
|
+
dirs = delete_nested(dirs)
|
73
|
+
Find.find(*dirs) do |file|
|
74
|
+
if monitor?(file) && updated?(file)
|
75
|
+
store_timestamp(file)
|
76
|
+
yield file
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|