moco 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|