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,135 @@
1
+ on run argv
2
+ local browsers, urls
3
+ set {browsers, urls} to parse(argv)
4
+ if browsers contains "Canary" then reloadCanary(urls)
5
+ if browsers contains "Chrome" then reloadChrome(urls)
6
+ if browsers contains "Firefox" then reloadFirefox(urls)
7
+ if browsers contains "Opera" then reloadOpera(urls)
8
+ if browsers contains "Safari" then reloadSafari(urls)
9
+ if browsers contains "WebKit" then reloadWebKit(urls)
10
+ return
11
+ end
12
+
13
+ on parse(argv)
14
+ set urls to {}
15
+ set browsers to {}
16
+ set allBrowsers to {"Canary", "Chrome", "Firefox", "Opera", "Safari", "WebKit"}
17
+
18
+ repeat with arg in argv
19
+ set arg to arg as string
20
+ if allBrowsers contains arg then
21
+ set browsers to browsers & arg
22
+ else
23
+ set urls to urls & arg
24
+ end
25
+ end
26
+
27
+ if browsers = {} then set browsers to allBrowsers
28
+
29
+ return {browsers, urls}
30
+ end
31
+
32
+ on shouldReload(_url, urls)
33
+ if _url = "" then return false
34
+ if urls = {} then return true
35
+ repeat with u in urls
36
+ if _url starts with u then return true
37
+ end
38
+ return false
39
+ end
40
+
41
+ on reloadSafari(urls)
42
+ reloadSafariWebKit("Safari", urls)
43
+ end
44
+
45
+ on reloadWebKit(urls)
46
+ reloadSafariWebKit("WebKit", urls)
47
+ end
48
+
49
+ on reloadSafariWebKit(browser, urls)
50
+ using terms from application "Safari"
51
+ tell application browser
52
+ if it is not running then return
53
+ if (windows where visible is true) = {} then return
54
+ if not (front document exists) then return
55
+
56
+ tell front document
57
+ if my shouldReload(URL as string, urls) then
58
+ do JavaScript "location.reload()"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ on reloadChrome(urls)
66
+ reloadGoogleChrome("Google Chrome", urls)
67
+ end
68
+
69
+ on reloadCanary(urls)
70
+ reloadGoogleChrome("Google Chrome Canary", urls)
71
+ end
72
+
73
+ on reloadGoogleChrome(browser, urls)
74
+ using terms from application "Google Chrome"
75
+ tell application browser
76
+ if it is not running then return
77
+ if (windows where visible is true) = {} then return
78
+
79
+ tell active tab of front window
80
+ if my shouldReload(URL, urls) then reload
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ on reloadOpera(urls)
87
+ tell application "Opera"
88
+ if it is not running then return
89
+
90
+ if my shouldReload(URL of front document as string, urls) then
91
+ set URL of front document to "javascript:location.reload()"
92
+ end
93
+ end
94
+ end
95
+
96
+ on reloadFirefox(urls)
97
+ tell application "Firefox"
98
+ if it is not running then return
99
+ if (windows where visible is true) = {} then return
100
+
101
+ set frontApp to my findFrontApp()
102
+ activate
103
+ if my shouldReload(my copyUrl(), urls) then my doReload()
104
+ my resetFrontApp(frontApp)
105
+ end
106
+ end
107
+
108
+ on copyUrl()
109
+ set the clipboard to ""
110
+ tell application "System Events"
111
+ delay 0.01
112
+ keystroke "l" using {command down}
113
+ delay 0.01
114
+ keystroke "c" using {command down}
115
+ delay 0.1
116
+ end
117
+ return the clipboard as string
118
+ end
119
+
120
+ on doReload()
121
+ tell application "System Events"
122
+ delay 0.01
123
+ keystroke "r" using {command down}
124
+ end
125
+ end
126
+
127
+ on findFrontApp()
128
+ tell application "System Events"
129
+ return first process where frontmost is true
130
+ end
131
+ end
132
+
133
+ on resetFrontApp(frontApp)
134
+ tell frontApp to set frontmost to true
135
+ end
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+
3
+ module MoCo
4
+
5
+ describe AnsiEscape do
6
+
7
+ def stdout_tty(tty, &block)
8
+ $stdout = $stdout.dup
9
+ $stdout.define_singleton_method(:tty?) { tty }
10
+ yield
11
+ ensure
12
+ $stdout = STDOUT
13
+ end
14
+
15
+ it 'escapes the message when stdout points to a terminal' do
16
+ stdout_tty(true) do
17
+ assert_equal "\e[1mHello\e[0m", AnsiEscape.bold('Hello')
18
+ assert_equal "\e[1;31mHello\e[0m", AnsiEscape.bold_red('Hello')
19
+ end
20
+ end
21
+
22
+ it 'leaves the message alone if stdout is redirected to a file' do
23
+ stdout_tty(false) do
24
+ assert_equal 'Hello', AnsiEscape.bold('Hello')
25
+ assert_equal 'Hello', AnsiEscape.bold_red('Hello')
26
+ end
27
+ end
28
+
29
+ describe 'unescape' do
30
+
31
+ let(:hello) { "\e[1mHello\e[0m \e[1;31mWorld\e[00m!" }
32
+
33
+ it 'removes ANSI escape sequences' do
34
+ assert_equal 'Hello World!', AnsiEscape.unescape(hello)
35
+ end
36
+
37
+ it 'can replace ANSI escape sequences' do
38
+ unescaped = AnsiEscape.unescape(hello) { |msg| "<b>#{msg}</b>" }
39
+ assert_equal '<b>Hello</b> <b>World</b>!', unescaped
40
+ end
41
+
42
+ it 'does not support nested escape sequences' do
43
+ nested = "\e[;31mNested \e[1mEscape\e[m\e[0m"
44
+ refute_equal 'Nested Escape', AnsiEscape.unescape(nested)
45
+ assert_equal "Nested \e[1mEscape\e[0m", AnsiEscape.unescape(nested)
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,40 @@
1
+ require 'test_helper'
2
+
3
+ module MoCo
4
+
5
+ describe Application do
6
+
7
+ describe 'the compiled directory' do
8
+
9
+ def compiled_dir(file)
10
+ compiled_dirs = {
11
+ '/one' => '/1',
12
+ '/one/two' => nil,
13
+ '/one/two/three' => '/3',
14
+ }
15
+ Application.new(:compiled_dirs => compiled_dirs).compiled_dir(file)
16
+ end
17
+
18
+ before { Application.send(:public, :compiled_dir) }
19
+ after { Application.send(:private, :compiled_dir) }
20
+
21
+ it 'returns the directory that closest matches the filename' do
22
+ assert_equal '/1', compiled_dir('/one/file')
23
+ assert_equal '/3', compiled_dir('/one/two/three/file')
24
+ end
25
+
26
+ it 'keeps the nested directory structure' do
27
+ assert_equal '/1/nested', compiled_dir('/one/nested/file')
28
+ assert_equal '/3/nested', compiled_dir('/one/two/three/nested/file')
29
+ end
30
+
31
+ it 'returns nil when the compiled directory is unspecified' do
32
+ assert_nil compiled_dir('/one/two/file')
33
+ assert_nil compiled_dir('/one/two/nested/file')
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,101 @@
1
+ require 'test_helper'
2
+
3
+ module MoCo
4
+
5
+ describe BrowserError do
6
+
7
+ class BrowserError
8
+ define_method(:txmt_url_scheme?) { true }
9
+ end
10
+
11
+ let(:error) do
12
+ error = StandardError.new("\e[1;31m eval \e[0m error \\n&")
13
+ error.define_singleton_method(:line) { 2 }
14
+ error.define_singleton_method(:column) { 7 }
15
+ CompileError.new(error, File.expand_path('~') + '/file.txt')
16
+ end
17
+
18
+ describe 'the browser error message' do
19
+
20
+ let(:msg) { BrowserError.message(error) }
21
+
22
+ it 'contains the line number and filename' do
23
+ assert_match "\n\nLine: 2\nFile: file.txt", msg
24
+ end
25
+
26
+ it 'shortens the filename by replacing the home directory with ~' do
27
+ assert_match '>~/file.txt</a>', msg
28
+ end
29
+
30
+ end
31
+
32
+ describe 'the CSS error message' do
33
+
34
+ let(:msg) { CssError.message(error) }
35
+
36
+ it 'is CSS' do
37
+ assert_match 'body:before {', msg
38
+ end
39
+
40
+ it 'is escaped for CSS' do
41
+ assert_match 'error \\\\n&\\a \\a Line: 2', msg
42
+ end
43
+
44
+ it 'removes ansi color codes completely' do
45
+ refute_match '[1;31m', msg
46
+ refute_match '<span>', msg
47
+ end
48
+
49
+ it 'has no link' do
50
+ refute_match '<a href', msg
51
+ end
52
+
53
+ end
54
+
55
+ describe 'the JavaScript error message' do
56
+
57
+ let(:msg) { JsError.message(error) }
58
+
59
+ it 'is JavaScript' do
60
+ assert_match 'function() {', msg
61
+ end
62
+
63
+ it 'is escaped for Html' do
64
+ assert_match 'error \\\\n&amp;<br><br>Line: 2', msg
65
+ end
66
+
67
+ it 'replaces ansi color codes with a span element' do
68
+ refute_match '[1;31m', msg
69
+ assert_match '<span> eval </span>', msg
70
+ end
71
+
72
+ it 'has a TextMate link to the exact error location' do
73
+ link = "<a href='txmt://open/?url=file://~/file.txt&line=2&column=7'>"
74
+ assert_match link, msg
75
+ end
76
+
77
+ it 'has no link if the TextMate url scheme is unsupported' do
78
+ js_error = JsError.new(error)
79
+ js_error.define_singleton_method(:txmt_url_scheme?) { false }
80
+ refute_match '<a href', js_error.message
81
+ end
82
+
83
+ end
84
+
85
+ describe 'the HTML error message' do
86
+
87
+ let(:msg) { HtmlError.message(error) }
88
+
89
+ it 'is Html' do
90
+ assert_match '<!DOCTYPE html>', msg
91
+ end
92
+
93
+ it 'contains the JavaScript error message' do
94
+ assert_match JsError.message(error), msg
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,29 @@
1
+ require 'test_helper'
2
+
3
+ module MoCo
4
+
5
+ describe Browser do
6
+
7
+ describe 'urls' do
8
+
9
+ def urls(*urls)
10
+ Browser.new([], [], urls).instance_variable_get(:@args)
11
+ end
12
+
13
+ it "expands 'localhost' into all the localhost urls" do
14
+ assert_equal Browser.localhost, urls('localhost')
15
+ end
16
+
17
+ it "empties the urls if it contains 'all'" do
18
+ assert_empty urls('localhost', 'all')
19
+ end
20
+
21
+ it 'removes duplicates' do
22
+ assert_equal 1, urls('file:///', 'file:///').size
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,82 @@
1
+ require 'test_helper'
2
+
3
+ module MoCo
4
+
5
+ describe CompileError do
6
+
7
+ describe 'the error message' do
8
+
9
+ let(:error) do
10
+ message = 'script.coffee:4:8: Error: syntax error'
11
+ CompileError.new(StandardError.new(message), 'script.coffee')
12
+ end
13
+
14
+ it 'removes the filename and line number' do
15
+ refute_match 'script.coffee', error.message
16
+ refute_match /\d/, error.message
17
+ end
18
+
19
+ it 'removes error: from the start of the message' do
20
+ refute_match 'Error:', error.message
21
+ end
22
+
23
+ it 'capitalizes the first letter' do
24
+ assert_equal 'Syntax error', error.message
25
+ end
26
+
27
+ end
28
+
29
+ describe 'the line number' do
30
+
31
+ def line_from(options)
32
+ error = StandardError.new(options[:message])
33
+ error.define_singleton_method(:line) { options[:line_method] }
34
+ error.set_backtrace(options[:backtrace])
35
+ CompileError.new(error, 'index.haml').line
36
+ end
37
+
38
+ it 'uses the line method of the original error' do
39
+ assert_equal 2, line_from(:line_method => 2)
40
+ end
41
+
42
+ it 'finds the line number in the error message' do
43
+ assert_equal 42, line_from(:message => 'index.haml:42 ...')
44
+ end
45
+
46
+ it 'finds the line number in the backtrace' do
47
+ assert_equal 4, line_from(:backtrace => 'index.haml:4:2: ...')
48
+ end
49
+
50
+ it 'ignores line numbers from other files' do
51
+ assert_nil line_from(:backtrace => 'about.haml:2: ...')
52
+ end
53
+
54
+ it 'works with filenames containing special characters' do
55
+ file = 'index( |*).haml'
56
+ error = CompileError.new(StandardError.new(file + ':7:'), file)
57
+ assert_equal 7, error.line
58
+ end
59
+
60
+ end
61
+
62
+ describe 'the column' do
63
+
64
+ def column_from(options)
65
+ error = StandardError.new(options[:message])
66
+ error.define_singleton_method(:column) { options[:column_method] }
67
+ CompileError.new(error, 'index.haml').column
68
+ end
69
+
70
+ it 'uses the column method of the original error' do
71
+ assert_equal 4, column_from(:column_method => 4)
72
+ end
73
+
74
+ it 'finds the column in the error message' do
75
+ assert_equal 2, column_from(:message => 'index.haml:4:2: ...')
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,121 @@
1
+ require 'test_helper'
2
+
3
+ module MoCo
4
+
5
+ describe CompilerOption do
6
+
7
+ def convert(option)
8
+ value = option.split(':', 3)[2]
9
+ CompilerOption.convert(value)
10
+ end
11
+
12
+ specify 'convert booleans' do
13
+ assert_equal true, convert('ext:key:true')
14
+ assert_equal false, convert('ext:key:false')
15
+ end
16
+
17
+ specify 'the default option value is true' do
18
+ assert_equal true, convert('ext:key')
19
+ end
20
+
21
+ specify 'convert integers' do
22
+ assert_equal 100, convert('ext:key:100')
23
+ assert_equal -10, convert('ext:key:-10')
24
+ assert_equal 7, convert('ext:key:+007')
25
+ end
26
+
27
+ specify 'convert floats' do
28
+ assert_equal 0.5, convert('ext:key:.5')
29
+ assert_equal 0.5, convert('ext:key:+0.5')
30
+ assert_equal -0.5, convert('ext:key:-.5')
31
+ end
32
+
33
+ specify 'exponential notation is unsupported' do
34
+ assert_equal '1e2', convert('ext:key:1e2')
35
+ end
36
+
37
+ describe 'convert symbols' do
38
+
39
+ specify do
40
+ assert_equal :html5, convert('ext:key::html5')
41
+ assert_equal :false, convert('ext:key::false')
42
+ assert_equal :'007', convert('ext:key::007')
43
+ end
44
+
45
+ specify 'do not quote symbols' do
46
+ assert_equal :'"dir/file"', convert('ext:key::"dir/file"')
47
+ assert_equal :'dir/file', convert('ext:key::dir/file')
48
+ end
49
+
50
+ specify 'symbols with colons are unsupported' do
51
+ assert_equal [:'"foo', 'bar"'], convert('ext:key::"foo:bar"')
52
+ assert_equal [:'foo\\', 'bar'], convert('ext:key::foo\\:bar')
53
+ end
54
+
55
+ end
56
+
57
+ describe 'convert strings' do
58
+
59
+ specify do
60
+ assert_equal 'value', convert('ext:key:value')
61
+ assert_equal 'v a l', convert('ext:key:v a l')
62
+ assert_equal "cat's", convert("ext:key:cat's")
63
+ end
64
+
65
+ specify 'quoted strings' do
66
+ assert_equal '007', convert('ext:key:"007"')
67
+ assert_equal 'true', convert("ext:key:'true'")
68
+ assert_equal 'a:b:c', convert('ext:key:"a:b:c"')
69
+ end
70
+
71
+ specify 'double quoted strings can include single quotes and vice versa' do
72
+ assert_equal ":cat's", convert(%(ext:key:":cat's"))
73
+ assert_equal "'cat'", convert(%(ext:key:"'cat'"))
74
+ assert_equal '"cat"', convert(%(ext:key:'"cat"'))
75
+ end
76
+
77
+ specify 'quoted strings cannot include the same quote character' do
78
+ assert_equal ["'", "cat's'"], convert(%(ext:key:':cat's'))
79
+ assert_equal "''cat''", convert(%(ext:key:''cat''))
80
+ assert_equal '""cat""', convert(%(ext:key:""cat""))
81
+ end
82
+
83
+ specify 'single and double quotes cannot be mixed' do
84
+ assert_equal ["'not", 'quoted"'], convert(%(ext:key:'not:quoted"))
85
+ end
86
+
87
+ end
88
+
89
+ specify 'convert empty strings and arrays' do
90
+ assert_equal '', convert('ext:key:')
91
+ assert_equal [], convert('ext:key::')
92
+ assert_equal [''], convert('ext:key:"":')
93
+ end
94
+
95
+ describe 'convert arrays' do
96
+
97
+ specify 'string arrays' do
98
+ assert_equal ['one', 'two'], convert('ext:key:one:two')
99
+ assert_equal ['one'], convert('ext:key:one:')
100
+ end
101
+
102
+ specify 'symbol arrays' do
103
+ assert_equal [:one, :two], convert('ext:key::one::two')
104
+ assert_equal [:one], convert('ext:key::one:')
105
+ end
106
+
107
+ specify 'array with mixed types' do
108
+ assert_equal ["Cat's toy", true, :html5, -0.1, "007"],
109
+ convert('ext:key:"Cat\'s toy":true::html5:-.1:"007"')
110
+ end
111
+
112
+ specify 'array values cannot include colons' do
113
+ assert_equal ['"Title', ' MoCo"'], convert('ext:key:"Title: MoCo":')
114
+ assert_equal ["'", "not_symbol'"], convert("ext:key:':not_symbol':")
115
+ end
116
+
117
+ end
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+
3
+ module MoCo
4
+
5
+ describe CompilerRegister do
6
+
7
+ before { Singleton.__init__(CompilerRegister) }
8
+ after { reset_register }
9
+
10
+ it 'registers the compiler class for the source extension' do
11
+ HtmlCompiler.register('haml')
12
+ assert_equal HtmlCompiler, MoCo.compiler_for('haml')
13
+ end
14
+
15
+ describe 'compiler lookup' do
16
+
17
+ before do
18
+ MoCo.register(CssCompiler, 'sass')
19
+ MoCo.register(CssCompiler, 'scss')
20
+ MoCo.register(HtmlCompiler, 'md')
21
+ end
22
+
23
+ it 'works when more than one compiler is registered' do
24
+ assert_equal CssCompiler, MoCo.compiler_for('scss')
25
+ assert_equal HtmlCompiler, MoCo.compiler_for('md')
26
+ assert_equal CssCompiler, MoCo.compiler_for('sass')
27
+ end
28
+
29
+ it 'accepts a filename' do
30
+ assert_equal CssCompiler, MoCo.compiler_for('/dir/a style.css.sass')
31
+ end
32
+
33
+ it 'returns nil when the extension is unregistered' do
34
+ assert_nil MoCo.compiler_for('dummy')
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ end