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