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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1bf216f6a4a3693e7fbe4208e2ce90c6c1381c18
4
+ data.tar.gz: 4ca98a5e6728548265487bdc3c310009fee3462b
5
+ SHA512:
6
+ metadata.gz: 86b782cc69382beb4f4c2e9ea9408db2bd2d4f870966f83400de7bfe7feb57825abf15e31919d29212e9d8fa9a976a940a1195faecd631bbe36324b8cf720e74
7
+ data.tar.gz: c61aadfb6a5897fc065b160216fffed557624c2482ff119351c52290e37726f17db56cbfa36854a0baf7bd4abc3c66b297b41165b9c088435cd657651843ac61
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 AS Harbitz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.txt ADDED
@@ -0,0 +1,67 @@
1
+ Usage:
2
+ moco [options] SOURCE ...
3
+ moco [options] SOURCE:COMPILED ...
4
+
5
+ Description:
6
+ MoCo monitors web templates. On updates the templates are compiled and
7
+ the browser reloaded. MoCo currently supports CoffeeScript, Sass, LESS,
8
+ Markdown and Haml.
9
+
10
+ Files and directories:
11
+ The given source files and directories will be monitored for updates.
12
+ Use the SOURCE:COMPILED format to save the compiled files to another
13
+ directory or to change the compiled filename:
14
+ moco .:/www sass:/www/css README.md:/www/index.html
15
+
16
+ Options:
17
+ --monitor Keep running until Ctrl-C is pressed [DEFAULT]
18
+ --no-monitor Exit after the initial compilation
19
+
20
+ -c, --compile Compile all the supported file types [DEFAULT]
21
+ -c, --compile EXT,EXT Compile the given file types
22
+ --no-compile Disable compilation
23
+ moco -c coffee -c sass,scss .
24
+
25
+ -f, --force Force recompilation at startup
26
+ --no-force Do not compile up-to-date files [DEFAULT]
27
+
28
+ -m, --source-map Make source maps if the compiler supports it
29
+ --no-source-map Do not make source maps [DEFAULT]
30
+
31
+ -o, --option EXT:KEY:VAL Set a compiler option
32
+ moco -o coffee:header # header = true
33
+ -o haml:ugly:false # ugly = false
34
+ -o haml:format::xhtml # format = :xhtml
35
+ -o md:layout:md.html # layout = 'md.html'
36
+ -o less:paths:css: . # paths = ['css']
37
+
38
+ -r, --reload Reload after css/html/js file updates [DEFAULT]
39
+ -r, --reload EXT,EXT Set the file types that triggers reloading
40
+ --no-reload Disable reloading
41
+ moco -r rb -r css,html,js .
42
+
43
+ -b, --browser BRO,BRO The browsers to reload [all by DEFAULT]
44
+ moco -b safari -b chrome,canary .
45
+
46
+ -u, --url all Reload all active tabs
47
+ -u, --url localhost Reload active tabs with localhost urls [DEFAULT]
48
+ -u, --url URL,URL Reload active tabs where the url starts with URL
49
+ moco -u localhost -u http://app.dev/ .
50
+
51
+ --require LIB Require the library
52
+ moco --require path/to/compiler.rb .
53
+
54
+ -q, --quiet Log errors only
55
+ --no-quiet Log errors and file updates [DEFAULT]
56
+
57
+ -l, --list List the supported file types and browsers
58
+
59
+ -h, --help Display this message
60
+
61
+ The moco file:
62
+ MoCo looks for files named '.moco' and 'moco.rb' in the working directory
63
+ and in the home directory. The purpose of these files is to set options
64
+ and to define new compilers. The command line options have precedence.
65
+
66
+ More information:
67
+ https://github.com/asharbitz/moco#readme
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
2
+
3
+ require 'moco'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |test|
7
+ test.libs << 'test'
8
+ test.pattern = 'test/*_test.rb'
9
+ end
10
+
11
+ desc 'Compile files'
12
+ task :compile do
13
+ args = %w[--force --no-monitor --no-reload]
14
+ MoCo::Application.monitor_compile_and_reload(args)
15
+ end
data/bin/moco ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems' unless defined? Gem
4
+ require 'moco'
5
+
6
+ MoCo::Application.monitor_compile_and_reload(ARGV)
@@ -0,0 +1,37 @@
1
+ module MoCo
2
+
3
+ module AnsiEscape
4
+
5
+ def self.bold(text)
6
+ escape(text, '1')
7
+ end
8
+
9
+ def self.bold_red(text)
10
+ escape(text, '1;31')
11
+ end
12
+
13
+ def self.green(text)
14
+ escape(text, '32')
15
+ end
16
+
17
+ def self.unescape(text)
18
+ text.gsub(/\e\[[\d;]+m(.*?)\e\[0*m/) do
19
+ block_given? ? yield($1) : $1
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def self.escape(text, code)
26
+ if $stdout.tty?
27
+ "\e[#{code}m#{text}\e[0m"
28
+ else
29
+ text
30
+ end
31
+ end
32
+
33
+ private_class_method :escape
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,109 @@
1
+ module MoCo
2
+
3
+ class Application
4
+
5
+ def self.monitor_compile_and_reload(args)
6
+ options = parse_options(args)
7
+ new(options).monitor_compile_and_reload
8
+ end
9
+
10
+ def initialize(options)
11
+ @options = options
12
+ end
13
+
14
+ def monitor_compile_and_reload
15
+ monitor = monitor_instance
16
+ monitor.files.each { |file| compile(file, @options[:force]) }
17
+ reload
18
+ if @options[:monitor]
19
+ Log.monitor
20
+ monitor.monitor { |file| compile_and_reload(file) }
21
+ puts
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def self.parse_options(args)
28
+ options = Options.parse(args)
29
+ Log.quiet = options[:quiet]
30
+ Log.load(Options.moco_files)
31
+ options
32
+ rescue OptionError, LoadError => e
33
+ Log.load(Options.moco_files)
34
+ abort e.message
35
+ end
36
+
37
+ private_class_method :parse_options
38
+
39
+ def monitor_instance
40
+ exts = []
41
+ exts += @options[:compile_exts] if @options[:compile]
42
+ exts += @options[:reload_exts] if @options[:reload]
43
+ Monitor.new(@options[:source_files], @options[:source_dirs], exts.uniq)
44
+ end
45
+
46
+ def compile_and_reload(file)
47
+ if compiler = compile(file, true)
48
+ reload(compiler.compiled_file)
49
+ end
50
+ reload(file)
51
+ end
52
+
53
+ def compile(file, force)
54
+ if @options[:compile]
55
+ compiler = compiler_for(file)
56
+ if compiler && (force || compiler.should_compile?)
57
+ do_compile(compiler)
58
+ compiler
59
+ end
60
+ end
61
+ end
62
+
63
+ def compiler_for(file)
64
+ if compiler = MoCo.compiler_for(file)
65
+ compiler.new(file, compiled_file(file), compiled_dir(file))
66
+ end
67
+ rescue LoadError => e
68
+ abort e.message
69
+ end
70
+
71
+ def compiled_file(file)
72
+ @options[:compiled_files][file]
73
+ end
74
+
75
+ def compiled_dir(file)
76
+ @options[:compiled_dirs].keys.sort.reverse.each do |source_dir|
77
+ if file.start_with?(source_dir)
78
+ compiled_dir = @options[:compiled_dirs][source_dir]
79
+ if compiled_dir
80
+ compiled_dir = File.dirname(file).sub(source_dir, compiled_dir)
81
+ end
82
+ return compiled_dir
83
+ end
84
+ end
85
+ nil
86
+ end
87
+
88
+ def do_compile(compiler)
89
+ Log.compile(compiler)
90
+ compiler.compile
91
+ Log.update(compiler)
92
+ rescue CompileError => e
93
+ Log.error(e)
94
+ end
95
+
96
+ def reload(file = nil)
97
+ if @options[:reload]
98
+ @browser ||= browser_instance
99
+ @browser.reload if file.nil? || @browser.should_reload?(file)
100
+ end
101
+ end
102
+
103
+ def browser_instance
104
+ Browser.new(@options[:reload_exts], @options[:browsers], @options[:urls])
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,64 @@
1
+ module MoCo
2
+
3
+ class Browser
4
+
5
+ def self.extensions
6
+ %w[css html js]
7
+ end
8
+
9
+ def self.browsers
10
+ %w[Canary Chrome Firefox Opera Safari WebKit]
11
+ end
12
+
13
+ def self.localhost
14
+ %w[
15
+ file:///
16
+ file://localhost/
17
+ http://localhost/
18
+ http://localhost:
19
+ http://127.0.0.1/
20
+ http://127.0.0.1:
21
+ http://0.0.0.0/
22
+ http://0.0.0.0:
23
+ ]
24
+ end
25
+
26
+ def initialize(extensions, browsers, urls)
27
+ @extensions = extensions
28
+ @args = browsers + urls(urls)
29
+ @reload = false
30
+ at_exit { do_reload }
31
+ end
32
+
33
+ def should_reload?(file)
34
+ @extensions.include?(FileUtil.normalized_extension(file))
35
+ end
36
+
37
+ def reload
38
+ return if @reload
39
+ @reload = true
40
+ Thread.new do
41
+ sleep 0.2
42
+ do_reload
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ SCRIPT = File.expand_path('../support/reload.scpt', __FILE__)
49
+
50
+ def urls(urls)
51
+ return [] if urls.include?('all')
52
+ urls += Browser.localhost if urls.delete('localhost')
53
+ urls.uniq
54
+ end
55
+
56
+ def do_reload
57
+ return unless @reload
58
+ @reload = false
59
+ system('osascript', SCRIPT, *@args)
60
+ end
61
+
62
+ end
63
+
64
+ end
@@ -0,0 +1,105 @@
1
+ require 'cgi'
2
+
3
+ module MoCo
4
+
5
+ class BrowserError
6
+
7
+ def self.message(error)
8
+ new(error).message
9
+ end
10
+
11
+ def initialize(error)
12
+ @message = error.message
13
+ @file = file(error)
14
+ @line = error.line
15
+ @column = error.column
16
+ end
17
+
18
+ def message
19
+ escaped_message + on_line + in_file
20
+ end
21
+
22
+ private
23
+
24
+ def file(error)
25
+ FileUtil.short_path(error.file)
26
+ end
27
+
28
+ def read_file(file)
29
+ File.read(File.expand_path('../support/error/' + file, __FILE__))
30
+ end
31
+
32
+ def escaped_message(message = @message)
33
+ message = remove_ansi_color(message)
34
+ message = message.gsub('\\') { '\\\\' }.gsub('"', '\"')
35
+ message + "\n\n"
36
+ end
37
+
38
+ def remove_ansi_color(message)
39
+ AnsiEscape.unescape(message) do |escaped|
40
+ html_allowed? ? "<span>#{escaped}</span>" : escaped
41
+ end
42
+ end
43
+
44
+ def html_allowed?
45
+ true
46
+ end
47
+
48
+ def on_line
49
+ @line ? "Line: #{@line}\n" : ''
50
+ end
51
+
52
+ def in_file
53
+ "File: #{File.basename(@file)} (#{edit_link || @file})"
54
+ end
55
+
56
+ def edit_link
57
+ if html_allowed? && txmt_url_scheme?
58
+ href = "txmt://open/?url=file://#{@file}"
59
+ href << "&line=#{@line}" if @line
60
+ href << "&column=#{@column}" if @column
61
+ "<a href='#{href}'>#{@file}</a>"
62
+ end
63
+ end
64
+
65
+ def txmt_url_scheme?
66
+ return @@txmt if defined? @@txmt
67
+ @@txmt = `defaults read com.apple.LaunchServices` =~
68
+ /LSHandlerURLScheme["\s]*=["\s]*txmt["\s]*;/
69
+ end
70
+
71
+ end
72
+
73
+ class CssError < BrowserError
74
+
75
+ def message
76
+ read_file('error.css') % super.gsub(/\n/, '\a ')
77
+ end
78
+
79
+ def html_allowed?
80
+ false
81
+ end
82
+
83
+ end
84
+
85
+ class JsError < BrowserError
86
+
87
+ def message
88
+ read_file('error.js') % super.gsub(/\n/, '<br>')
89
+ end
90
+
91
+ def escaped_message
92
+ super(CGI.escapeHTML(@message))
93
+ end
94
+
95
+ end
96
+
97
+ class HtmlError < JsError
98
+
99
+ def message
100
+ read_file('error.html') % super
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,66 @@
1
+ module MoCo
2
+
3
+ class CompileError < Error
4
+
5
+ attr_reader :error
6
+ attr_reader :file
7
+ attr_reader :line
8
+ attr_reader :column
9
+
10
+ def initialize(error, file)
11
+ @error = error
12
+ @file = file
13
+ @line = get_line
14
+ @column = get_column
15
+ super(get_message)
16
+ end
17
+
18
+ private
19
+
20
+ def get_line
21
+ line = line_from_method || line_from_message || line_from_backtrace
22
+ line.to_i if line
23
+ end
24
+
25
+ def line_from_method
26
+ @error.line if @error.respond_to?(:line)
27
+ end
28
+
29
+ def line_from_message
30
+ @error.message[source_pattern, 1]
31
+ end
32
+
33
+ def line_from_backtrace
34
+ if @error.backtrace && @error.backtrace[0]
35
+ @error.backtrace[0][source_pattern, 1]
36
+ end
37
+ end
38
+
39
+ def get_column
40
+ column = column_from_method || column_from_message
41
+ column.to_i if column
42
+ end
43
+
44
+ def column_from_method
45
+ @error.column if @error.respond_to?(:column)
46
+ end
47
+
48
+ def column_from_message
49
+ @error.message[source_pattern, 2]
50
+ end
51
+
52
+ def source_pattern
53
+ file = Regexp.escape(@file)
54
+ /^#{file}:(\d+):?(\d+)?[:\s]*/
55
+ end
56
+
57
+ def get_message
58
+ message = @error.message.gsub(source_pattern, '')
59
+ message = message.sub(/\Aerror: /i, '')
60
+ message[0, 1] = message[0, 1].upcase
61
+ message
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,151 @@
1
+ module MoCo
2
+
3
+ class Compiler
4
+
5
+ def self.register(source_extension)
6
+ MoCo.register(self, source_extension)
7
+ end
8
+
9
+ def self.require_library(lib, gem_name = lib, version = nil)
10
+ @libraries ||= []
11
+ @libraries << [lib, gem_name, version]
12
+ end
13
+
14
+ def self.set_option(key, value = true)
15
+ @options ||= {}
16
+ @options[key] = value
17
+ end
18
+
19
+ def self.options
20
+ (@options || {}).dup
21
+ end
22
+
23
+ def self.convert_option(key, value)
24
+ [key.to_sym, CompilerOption.convert(value)]
25
+ end
26
+
27
+ def self.compiled_extension
28
+ raise NotImplementedError
29
+ end
30
+
31
+ attr_reader :source_file
32
+ attr_reader :compiled_file
33
+
34
+ def initialize(source_file, compiled_file = nil, compiled_dir = nil)
35
+ @source_file = source_file
36
+ @compiled_file = compiled_file || compiled_filename(compiled_dir)
37
+ validate_filenames
38
+ require_libraries
39
+ end
40
+
41
+ def should_compile?
42
+ ! FileUtil.up_to_date?(@compiled_file, @source_file)
43
+ end
44
+
45
+ def compile
46
+ write_compiled(compiled_text)
47
+ rescue SyntaxError, StandardError => e
48
+ error = CompileError.new(e, @source_file)
49
+ error.set_backtrace(e.backtrace)
50
+ write_compiled(error_text(error))
51
+ raise error
52
+ end
53
+
54
+ def options
55
+ self.class.options
56
+ end
57
+
58
+ def source_text
59
+ File.read(@source_file)
60
+ end
61
+
62
+ def compiled_text
63
+ raise NotImplementedError
64
+ end
65
+
66
+ private
67
+
68
+ def compiled_filename(compiled_dir)
69
+ compiled_ext = self.class.compiled_extension
70
+ compiled_file = FileUtil.replace_extension(@source_file, compiled_ext)
71
+ FileUtil.replace_directory(compiled_file, compiled_dir)
72
+ end
73
+
74
+ def validate_filenames
75
+ if File.expand_path(@source_file) == File.expand_path(@compiled_file)
76
+ raise Error, 'The source and compiled filenames are identical'
77
+ end
78
+ end
79
+
80
+ def require_libraries
81
+ self.class.ancestors.each do |klass|
82
+ if libs = klass.instance_variable_get(:@libraries)
83
+ libs.each { |lib| require_library(*lib) }
84
+ libs.clear
85
+ end
86
+ end
87
+ end
88
+
89
+ def require_library(lib, gem_name = lib, version = nil)
90
+ gem gem_name, version if version
91
+ require lib
92
+ rescue LoadError => e
93
+ if e.message !~ /\b(#{lib}|#{gem_name})\b/
94
+ raise # Another library failed to load
95
+ else
96
+ version = " -v '#{version}'" if version
97
+ raise e, "#{e.message}\nTry: gem install #{gem_name}#{version}"
98
+ end
99
+ end
100
+
101
+ def write_compiled(text)
102
+ write_file(@compiled_file, text)
103
+ end
104
+
105
+ def write_file(filename, text)
106
+ FileUtil.write(filename, text)
107
+ end
108
+
109
+ def error_text(error)
110
+ error.message
111
+ end
112
+
113
+ end
114
+
115
+ class HtmlCompiler < Compiler
116
+
117
+ def self.compiled_extension
118
+ 'html'
119
+ end
120
+
121
+ def error_text(error)
122
+ HtmlError.message(error)
123
+ end
124
+
125
+ end
126
+
127
+ class CssCompiler < Compiler
128
+
129
+ def self.compiled_extension
130
+ 'css'
131
+ end
132
+
133
+ def error_text(error)
134
+ CssError.message(error)
135
+ end
136
+
137
+ end
138
+
139
+ class JsCompiler < Compiler
140
+
141
+ def self.compiled_extension
142
+ 'js'
143
+ end
144
+
145
+ def error_text(error)
146
+ JsError.message(error)
147
+ end
148
+
149
+ end
150
+
151
+ end