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.
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)