moco 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.txt +67 -0
  4. data/Rakefile +15 -0
  5. data/bin/moco +6 -0
  6. data/lib/moco/ansi_escape.rb +37 -0
  7. data/lib/moco/application.rb +109 -0
  8. data/lib/moco/browser.rb +64 -0
  9. data/lib/moco/browser_error.rb +105 -0
  10. data/lib/moco/compile_error.rb +66 -0
  11. data/lib/moco/compiler.rb +151 -0
  12. data/lib/moco/compiler_option.rb +60 -0
  13. data/lib/moco/compiler_register.rb +29 -0
  14. data/lib/moco/compilers/coffee_compiler.rb +70 -0
  15. data/lib/moco/compilers/haml_compiler.rb +21 -0
  16. data/lib/moco/compilers/less_compiler.rb +15 -0
  17. data/lib/moco/compilers/markdown_compiler.rb +139 -0
  18. data/lib/moco/compilers/sass_compiler.rb +25 -0
  19. data/lib/moco/file_util.rb +54 -0
  20. data/lib/moco/log.rb +108 -0
  21. data/lib/moco/monitor.rb +83 -0
  22. data/lib/moco/options.rb +336 -0
  23. data/lib/moco/source_map.rb +22 -0
  24. data/lib/moco/support/error/error.css +22 -0
  25. data/lib/moco/support/error/error.html +7 -0
  26. data/lib/moco/support/error/error.js +58 -0
  27. data/lib/moco/support/reload.scpt +0 -0
  28. data/lib/moco.rb +44 -0
  29. data/moco.gemspec +35 -0
  30. data/moco.rb +35 -0
  31. data/src/error/error.coffee +49 -0
  32. data/src/reload.applescript +135 -0
  33. data/test/ansi_escape_test.rb +52 -0
  34. data/test/application_test.rb +40 -0
  35. data/test/browser_error_test.rb +101 -0
  36. data/test/browser_test.rb +29 -0
  37. data/test/compile_error_test.rb +82 -0
  38. data/test/compiler_option_test.rb +121 -0
  39. data/test/compiler_register_test.rb +41 -0
  40. data/test/compiler_test.rb +243 -0
  41. data/test/compilers/coffee_compiler_test.rb +117 -0
  42. data/test/compilers/haml_compiler_test.rb +86 -0
  43. data/test/compilers/less_compiler_test.rb +72 -0
  44. data/test/compilers/markdown_compiler_test.rb +211 -0
  45. data/test/compilers/sass_compiler_test.rb +84 -0
  46. data/test/file_util_test.rb +37 -0
  47. data/test/fixtures/_color.scss +1 -0
  48. data/test/fixtures/color.less +1 -0
  49. data/test/fixtures/css_lib.rb +2 -0
  50. data/test/fixtures/html_lib.rb +2 -0
  51. data/test/fixtures/js_lib.rb +2 -0
  52. data/test/fixtures/layout.html +13 -0
  53. data/test/fixtures/moco.rb +8 -0
  54. data/test/fixtures/options_lib.rb +2 -0
  55. data/test/fixtures/source.txt +1 -0
  56. data/test/monitor_test.rb +68 -0
  57. data/test/options_test.rb +177 -0
  58. data/test/test_helper.rb +57 -0
  59. metadata +270 -0
@@ -0,0 +1,336 @@
1
+ require 'optparse'
2
+ require 'shellwords'
3
+
4
+ module MoCo
5
+
6
+ class Options
7
+
8
+ def self.moco_files
9
+ files = ['~/.moco', '~/moco.rb', './.moco', './moco.rb']
10
+ files = files.map { |file| File.expand_path(file) }
11
+ files.select { |file| File.file?(file) }
12
+ end
13
+
14
+ def self.args(args)
15
+ @args ||= []
16
+ @args += args.shellsplit
17
+ end
18
+
19
+ def self.parse(args)
20
+ new.parse(args)
21
+ end
22
+
23
+ def parse(command_line_args)
24
+ @options = {}
25
+ args = moco_file_args + command_line_args
26
+ display_help if args.empty?
27
+ option_parser.order(args) { |option| path(option) }
28
+ validate_options
29
+ default_options.merge(@options)
30
+ rescue OptionParser::ParseError => e
31
+ raise OptionError.new(e)
32
+ end
33
+
34
+ def option_parser
35
+ OptionParser.new do |op|
36
+ op.version = MoCo::VERSION
37
+ op.on( '--[no-]monitor', method(:monitor))
38
+ op.on('-c', '--[no-]compile [EXT,EXT]', method(:compile), Array)
39
+ op.on('-f', '--[no-]force', method(:force))
40
+ op.on('-m', '--[no-]source-map', method(:source_map))
41
+ op.on('-o', '--option EXT:KEY:VAL', method(:compiler_option))
42
+ op.on('-r', '--[no-]reload [EXT,EXT]', method(:reload), Array)
43
+ op.on('-b', '--browser BRO,BRO', method(:browsers), Array)
44
+ op.on('-u', '--url URL,URL', method(:urls), Array)
45
+ op.on( '--require LIB', method(:require_lib))
46
+ op.on('-q', '--[no-]quiet', method(:quiet))
47
+ op.on('-l', '--list', method(:display_list))
48
+ op.on('-h', '--help', method(:display_help))
49
+ end
50
+ end
51
+
52
+ def default_options
53
+ {
54
+ :monitor => true,
55
+ :compile => true,
56
+ :compile_exts => MoCo.compilers.keys.sort,
57
+ :force => false,
58
+ :source_map => false,
59
+ :reload => true,
60
+ :reload_exts => Browser.extensions,
61
+ :browsers => Browser.browsers,
62
+ :urls => Browser.localhost,
63
+ :quiet => false,
64
+ :source_files => [],
65
+ :source_dirs => [],
66
+ :compiled_files => {},
67
+ :compiled_dirs => {}
68
+ }
69
+ end
70
+
71
+ def help
72
+ <<-EOF.gsub(/^ {8}/, '').gsub(/`(.+?)`/, AnsiEscape.green('\1'))
73
+ Usage:
74
+ `moco [options] SOURCE ...`
75
+ `moco [options] SOURCE:COMPILED ...`
76
+
77
+ Description:
78
+ MoCo monitors web templates. On updates the templates are compiled and
79
+ the browser reloaded. MoCo currently supports CoffeeScript, Sass, LESS,
80
+ Markdown and Haml.
81
+
82
+ Files and directories:
83
+ The given source files and directories will be monitored for updates.
84
+ Use the SOURCE:COMPILED format to save the compiled files to another
85
+ directory or to change the compiled filename:
86
+ `moco .:/www sass:/www/css README.md:/www/index.html`
87
+
88
+ Options:
89
+ --monitor Keep running until Ctrl-C is pressed [DEFAULT]
90
+ --no-monitor Exit after the initial compilation
91
+
92
+ -c, --compile Compile all the supported file types [DEFAULT]
93
+ -c, --compile EXT,EXT Compile the given file types
94
+ --no-compile Disable compilation
95
+ `moco -c coffee -c sass,scss .`
96
+
97
+ -f, --force Force recompilation at startup
98
+ --no-force Do not compile up-to-date files [DEFAULT]
99
+
100
+ -m, --source-map Make source maps if the compiler supports it
101
+ --no-source-map Do not make source maps [DEFAULT]
102
+
103
+ -o, --option EXT:KEY:VAL Set a compiler option
104
+ `moco -o coffee:header` # header = true
105
+ ` -o haml:ugly:false` # ugly = false
106
+ ` -o haml:format::xhtml` # format = :xhtml
107
+ ` -o md:layout:md.html` # layout = 'md.html'
108
+ ` -o less:paths:css: .` # paths = ['css']
109
+
110
+ -r, --reload Reload after css/html/js file updates [DEFAULT]
111
+ -r, --reload EXT,EXT Set the file types that triggers reloading
112
+ --no-reload Disable reloading
113
+ `moco -r rb -r css,html,js .`
114
+
115
+ -b, --browser BRO,BRO The browsers to reload [all by DEFAULT]
116
+ `moco -b safari -b chrome,canary .`
117
+
118
+ -u, --url all Reload all active tabs
119
+ -u, --url localhost Reload active tabs with localhost urls [DEFAULT]
120
+ -u, --url URL,URL Reload active tabs where the url starts with URL
121
+ `moco -u localhost -u http://app.dev/ .`
122
+
123
+ --require LIB Require the library
124
+ `moco --require path/to/compiler.rb .`
125
+
126
+ -q, --quiet Log errors only
127
+ --no-quiet Log errors and file updates [DEFAULT]
128
+
129
+ -l, --list List the supported file types and browsers
130
+
131
+ -h, --help Display this message
132
+
133
+ The moco file:
134
+ MoCo looks for files named '.moco' and 'moco.rb' in the working directory
135
+ and in the home directory. The purpose of these files is to set options
136
+ and to define new compilers. The command line options have precedence.
137
+
138
+ More information:
139
+ https://github.com/asharbitz/moco#readme
140
+ EOF
141
+ end
142
+
143
+ private
144
+
145
+ def moco_file_args
146
+ load_moco_files
147
+ Options.instance_variable_get(:@args) || []
148
+ end
149
+
150
+ def load_moco_files
151
+ Options.moco_files.each do |file|
152
+ load file
153
+ end
154
+ end
155
+
156
+ [:monitor, :force, :browsers, :urls].each do |key|
157
+ define_method(key) do |option|
158
+ set_option(key, option)
159
+ end
160
+ end
161
+
162
+ [:compile, :reload].each do |key|
163
+ define_method(key) do |option|
164
+ case option
165
+ when Array
166
+ option.map! { |ext| FileUtil.normalized_extension(ext) }
167
+ set_option("#{key}_exts".to_sym, option)
168
+ set_option(key, true)
169
+ when nil
170
+ set_option(key, true)
171
+ else
172
+ set_option(key, option)
173
+ end
174
+ end
175
+ end
176
+
177
+ def set_option(key, option)
178
+ case option
179
+ when Array
180
+ @options[key] ||= []
181
+ @options[key] += option
182
+ @options[key].uniq!
183
+ when Hash
184
+ @options[key] ||= {}
185
+ @options[key].merge!(option)
186
+ else
187
+ @options[key] = option
188
+ end
189
+ end
190
+
191
+ def compiler_option(option)
192
+ ext, key, value = option.split(':', 3)
193
+ compiler = MoCo.compiler_for(ext)
194
+ raise OptionError.new(:missing_option_key) unless key
195
+ raise OptionError.new(:invalid_extension, ext) unless compiler
196
+ key, value = compiler.convert_option(key, value)
197
+ compiler.set_option(key, value)
198
+ end
199
+
200
+ def source_map(option)
201
+ MoCo.compilers.each_value do |compiler|
202
+ if compiler < SourceMap
203
+ compiler.set_option(compiler.source_map_key, option)
204
+ end
205
+ end
206
+ end
207
+
208
+ def require_lib(option)
209
+ require option
210
+ rescue LoadError
211
+ require File.expand_path(option)
212
+ end
213
+
214
+ def quiet(option)
215
+ set_option(:quiet, option)
216
+ SassCompiler.set_option(:quiet, option)
217
+ end
218
+
219
+ def display_list(option)
220
+ display(list)
221
+ end
222
+
223
+ def display_help(option = nil)
224
+ display(help)
225
+ end
226
+
227
+ def display(text)
228
+ puts
229
+ puts text
230
+ puts
231
+ exit 0
232
+ end
233
+
234
+ def list
235
+ [ ['Compile:', :compile_exts],
236
+ ['Reload:', :reload_exts],
237
+ ['Browsers:', :browsers],
238
+ ['Localhost:', :urls]
239
+ ].map do |header, key|
240
+ [header, *default_options[key]].join("\n ")
241
+ end.join("\n\n")
242
+ end
243
+
244
+ def path(option)
245
+ source, compiled = option.split(':')
246
+ source = File.expand_path(source)
247
+ compiled = File.expand_path(compiled) if compiled
248
+ if File.directory?(source)
249
+ set_option(:source_dirs, [source])
250
+ set_option(:compiled_dirs, source => compiled)
251
+ elsif File.file?(source)
252
+ set_option(:source_files, [source])
253
+ set_option(:compiled_files, source => compiled)
254
+ else
255
+ raise OptionError.new(:invalid_file, source)
256
+ end
257
+ end
258
+
259
+ def validate_options
260
+ validate_files
261
+ validate_compilers
262
+ validate_browsers
263
+ end
264
+
265
+ def validate_files
266
+ unless @options[:source_dirs] || @options[:source_files]
267
+ raise OptionError.new(:missing_file)
268
+ end
269
+ (@options[:compiled_dirs] || {}).each_value do |dir|
270
+ if dir && File.file?(dir)
271
+ raise OptionError.new(:dir_expected, dir)
272
+ end
273
+ end
274
+ (@options[:compiled_files] || {}).each_value do |file|
275
+ if file && File.directory?(file)
276
+ raise OptionError.new(:file_expected, file)
277
+ end
278
+ end
279
+ end
280
+
281
+ def validate_compilers
282
+ (@options[:compile_exts] || []).each do |ext|
283
+ unless MoCo.compiler_for(ext)
284
+ raise OptionError.new(:invalid_extension, ext)
285
+ end
286
+ end
287
+ end
288
+
289
+ def validate_browsers
290
+ supported_browsers = Browser.browsers.map(&:downcase)
291
+ (@options[:browsers] || []).each do |browser|
292
+ unless supported_browsers.include?(browser.downcase)
293
+ raise OptionError.new(:invalid_browser, browser)
294
+ end
295
+ end
296
+ end
297
+
298
+ end
299
+
300
+ class OptionError < Error
301
+
302
+ def initialize(error, *args)
303
+ super(error_message(error) % args)
304
+ end
305
+
306
+ private
307
+
308
+ def error_message(error)
309
+ case error
310
+ when :missing_file
311
+ [ 'No directory or file provided. ' +
312
+ 'To monitor files in the current directory:',
313
+ 'moco .' ]
314
+ when :invalid_file
315
+ [ "No such file or directory: '%s'" ]
316
+ when :dir_expected
317
+ [ "Expected a directory, but got a filename: '%s'" ]
318
+ when :file_expected
319
+ [ "Expected a filename, but got a directory: '%s'" ]
320
+ when :invalid_extension
321
+ [ "No compiler registered for '%s' files. The supported file types are:",
322
+ MoCo.compilers.keys.sort.join(' ') ]
323
+ when :missing_option_key
324
+ [ 'The option key is missing. Set compiler options like this:',
325
+ '-o coffee:header -o haml:format::xhtml' ]
326
+ when :invalid_browser
327
+ [ "Unknown browser '%s'. The supported browsers are:",
328
+ Browser.browsers.join(' ') ]
329
+ when Exception
330
+ [ error.message ]
331
+ end.join("\n")
332
+ end
333
+
334
+ end
335
+
336
+ end
@@ -0,0 +1,22 @@
1
+ module MoCo
2
+
3
+ module SourceMap
4
+
5
+ def self.source_map_key
6
+ raise NotImplementedError
7
+ end
8
+
9
+ attr_reader :source_map_text
10
+
11
+ def compile
12
+ super
13
+ write_file(source_map_file, source_map_text) if source_map_text
14
+ end
15
+
16
+ def source_map_file
17
+ compiled_file + '.map'
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,22 @@
1
+ body:before {
2
+ content: "%s";
3
+ white-space: pre-wrap;
4
+ font-family: Menlo, Monaco, monospace;
5
+ font-size: 14px;
6
+ line-height: 1.5;
7
+ background: white;
8
+ border: 3px solid red;
9
+ border-radius: 5px;
10
+ box-shadow: 5px 5px 15px #CCC;
11
+ display: block;
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ padding: 20px;
16
+ margin: 20px;
17
+ z-index: 10000;
18
+ }
19
+
20
+ body:hover:before {
21
+ z-index: 10001;
22
+ }
@@ -0,0 +1,7 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Error</title>
5
+ <script type='text/javascript'>%s</script>
6
+ </head>
7
+ </html>
@@ -0,0 +1,58 @@
1
+ // Generated by CoffeeScript 1.6.2
2
+ (function() {
3
+ var MoCo;
4
+
5
+ MoCo = {};
6
+
7
+ MoCo.errorMessage = "%s";
8
+
9
+ MoCo.css = '\
10
+ .moco-compile-error {\
11
+ white-space: pre-wrap;\
12
+ font-family: Menlo, Monaco, monospace;\
13
+ font-size: 14px;\
14
+ line-height: 1.5;\
15
+ color: black;\
16
+ background: white;\
17
+ border: 3px solid red;\
18
+ border-radius: 5px;\
19
+ box-shadow: 5px 5px 15px #CCC;\
20
+ display: block;\
21
+ position: fixed;\
22
+ top: 0;\
23
+ left: 0;\
24
+ padding: 20px;\
25
+ margin: 20px;\
26
+ z-index: 10000;\
27
+ }\
28
+ .moco-compile-error:hover {\
29
+ z-index: 10001;\
30
+ }\
31
+ .moco-compile-error span {\
32
+ color: red;\
33
+ }\
34
+ .moco-compile-error a, .moco-compile-error a:visited {\
35
+ color: black;\
36
+ text-decoration: none;\
37
+ }\
38
+ .moco-compile-error a:hover, .moco-compile-error a:active {\
39
+ color: red;\
40
+ text-decoration: none;\
41
+ }\
42
+ ';
43
+
44
+ MoCo.appendErrorElement = function() {
45
+ var pre, style;
46
+
47
+ style = document.createElement('style');
48
+ style.innerHTML = MoCo.css;
49
+ pre = document.createElement('pre');
50
+ pre.innerHTML = MoCo.errorMessage;
51
+ pre.className = 'moco-compile-error';
52
+ document.body.appendChild(style);
53
+ return document.body.appendChild(pre);
54
+ };
55
+
56
+ window.addEventListener('load', MoCo.appendErrorElement, false);
57
+
58
+ }).call(this);
Binary file
data/lib/moco.rb ADDED
@@ -0,0 +1,44 @@
1
+ module MoCo
2
+
3
+ VERSION = '0.1.0'
4
+
5
+ class Error < StandardError
6
+ end
7
+
8
+ def self.compilers
9
+ CompilerRegister.instance.compilers
10
+ end
11
+
12
+ def self.register(compiler, extension)
13
+ CompilerRegister.instance.register(compiler, extension)
14
+ end
15
+
16
+ def self.compiler_for(file)
17
+ CompilerRegister.instance.compiler_for(file)
18
+ end
19
+
20
+ def self.args(args)
21
+ Options.args(args)
22
+ end
23
+
24
+ end
25
+
26
+ require 'moco/ansi_escape'
27
+ require 'moco/application'
28
+ require 'moco/browser_error'
29
+ require 'moco/browser'
30
+ require 'moco/compile_error'
31
+ require 'moco/compiler_option'
32
+ require 'moco/compiler_register'
33
+ require 'moco/compiler'
34
+ require 'moco/file_util'
35
+ require 'moco/log'
36
+ require 'moco/monitor'
37
+ require 'moco/options'
38
+ require 'moco/source_map'
39
+
40
+ require 'moco/compilers/coffee_compiler'
41
+ require 'moco/compilers/haml_compiler'
42
+ require 'moco/compilers/less_compiler'
43
+ require 'moco/compilers/markdown_compiler'
44
+ require 'moco/compilers/sass_compiler'
data/moco.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'moco'
3
+ spec.version = '0.1.0'
4
+ spec.license = 'MIT'
5
+
6
+ spec.author = 'AS Harbitz'
7
+ spec.email = 'asharbitz@gmail.com'
8
+ spec.homepage = 'https://github.com/asharbitz/moco'
9
+
10
+ spec.summary = 'Monitors and compiles web templates. Reloads the browser.'
11
+ spec.description = 'MoCo monitors web templates. On updates the templates ' +
12
+ 'are compiled and the browser reloaded. MoCo currently ' +
13
+ 'supports CoffeeScript, Sass, LESS, Markdown and Haml. ' +
14
+ 'Mac OS X only.'
15
+
16
+ spec.files = Dir['LICENSE', 'moco.gemspec', 'moco.rb', 'Rakefile',
17
+ 'README.txt', '{lib,src}/**/*.*']
18
+ spec.test_files = Dir['test/**/*.*']
19
+ spec.executables << 'moco'
20
+
21
+ spec.add_dependency 'rb-fsevent', '~> 0.9'
22
+
23
+ spec.add_development_dependency 'minitest', '~> 4.0'
24
+ spec.add_development_dependency 'coffee-script-source', '>= 1.6.2'
25
+ spec.add_development_dependency 'runjs'
26
+ spec.add_development_dependency 'haml'
27
+ spec.add_development_dependency 'less'
28
+ spec.add_development_dependency 'therubyracer'
29
+ spec.add_development_dependency 'redcarpet'
30
+ spec.add_development_dependency 'pygments.rb'
31
+ spec.add_development_dependency 'sass'
32
+
33
+ spec.required_ruby_version = '>= 1.8.7'
34
+ spec.requirements << 'Mac OS X'
35
+ end
data/moco.rb ADDED
@@ -0,0 +1,35 @@
1
+ MoCo.args '--option coffee:header src:lib/moco/support'
2
+
3
+ class AppleScriptCompiler < MoCo::Compiler
4
+
5
+ require_library 'open3'
6
+ register 'applescript'
7
+
8
+ def self.compiled_extension
9
+ 'scpt'
10
+ end
11
+
12
+ def compile
13
+ cmd = ['osacompile', '-o', compiled_file, source_file]
14
+ message = Open3.popen3(*cmd) { |_, _, stderr| stderr.read.strip }
15
+ unless message.empty?
16
+ error = StandardError.new(message)
17
+ error = MoCo::CompileError.new(error, source_file)
18
+ write_compiled(error_text(error))
19
+ raise error
20
+ end
21
+ end
22
+
23
+ def error_text(error)
24
+ applescript = <<-EOF
25
+ tell application "AppleScript Runner"
26
+ display alert "%s" giving up after 10
27
+ return
28
+ end
29
+ EOF
30
+ message = "File: #{error.file}\n\nLine: #{error.line}\n\n#{error.message}"
31
+ message = message.gsub('\\') { '\\\\' }.gsub('"', '\"')
32
+ applescript % message
33
+ end
34
+
35
+ end
@@ -0,0 +1,49 @@
1
+ MoCo = {}
2
+
3
+ MoCo.errorMessage = "%s"
4
+
5
+ MoCo.css = '
6
+ .moco-compile-error {
7
+ white-space: pre-wrap;
8
+ font-family: Menlo, Monaco, monospace;
9
+ font-size: 14px;
10
+ line-height: 1.5;
11
+ color: black;
12
+ background: white;
13
+ border: 3px solid red;
14
+ border-radius: 5px;
15
+ box-shadow: 5px 5px 15px #CCC;
16
+ display: block;
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ padding: 20px;
21
+ margin: 20px;
22
+ z-index: 10000;
23
+ }
24
+ .moco-compile-error:hover {
25
+ z-index: 10001;
26
+ }
27
+ .moco-compile-error span {
28
+ color: red;
29
+ }
30
+ .moco-compile-error a, .moco-compile-error a:visited {
31
+ color: black;
32
+ text-decoration: none;
33
+ }
34
+ .moco-compile-error a:hover, .moco-compile-error a:active {
35
+ color: red;
36
+ text-decoration: none;
37
+ }
38
+ '
39
+
40
+ MoCo.appendErrorElement = ->
41
+ style = document.createElement('style')
42
+ style.innerHTML = MoCo.css
43
+ pre = document.createElement('pre')
44
+ pre.innerHTML = MoCo.errorMessage
45
+ pre.className = 'moco-compile-error'
46
+ document.body.appendChild(style)
47
+ document.body.appendChild(pre)
48
+
49
+ window.addEventListener('load', MoCo.appendErrorElement, false)