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,60 @@
1
+ module MoCo
2
+
3
+ module CompilerOption
4
+
5
+ def self.convert(value)
6
+ case value
7
+ when nil, 'true'
8
+ true
9
+ when 'false'
10
+ false
11
+ when INTEGER
12
+ value.to_i
13
+ when FLOAT
14
+ value.to_f
15
+ when SYMBOL
16
+ value.delete(':').to_sym
17
+ when SINGLE_QUOTED, DOUBLE_QUOTED
18
+ strip_quotes(value)
19
+ when ARRAY
20
+ to_array(value)
21
+ else
22
+ value
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ INTEGER = /^[+-]?\d+$/
29
+ FLOAT = /^[+-]?\d*\.\d+$/
30
+ SYMBOL = /^:[^:]+$/
31
+ ARRAY = /:/
32
+ SINGLE_QUOTED = /^'[^']*'$/
33
+ DOUBLE_QUOTED = /^"[^"]*"$/
34
+
35
+ def self.strip_quotes(value)
36
+ value[1..-2]
37
+ end
38
+
39
+ private_class_method :strip_quotes
40
+
41
+ def self.to_array(value)
42
+ values = value.split(':')
43
+ rejoin_symbols(values)
44
+ values.map { |value| convert(value) }
45
+ end
46
+
47
+ private_class_method :to_array
48
+
49
+ def self.rejoin_symbols(values)
50
+ values.each_cons(2) do |v1, v2|
51
+ v2.insert(0, ':') if v1.empty?
52
+ end
53
+ values.delete('')
54
+ end
55
+
56
+ private_class_method :rejoin_symbols
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,29 @@
1
+ require 'singleton'
2
+
3
+ module MoCo
4
+
5
+ class CompilerRegister
6
+
7
+ include Singleton
8
+
9
+ def initialize
10
+ @compilers = {}
11
+ end
12
+
13
+ def register(compiler, extension)
14
+ extension = FileUtil.normalized_extension(extension)
15
+ @compilers[extension] = compiler
16
+ end
17
+
18
+ def compiler_for(file)
19
+ extension = FileUtil.normalized_extension(file)
20
+ @compilers[extension]
21
+ end
22
+
23
+ def compilers
24
+ @compilers.dup
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,70 @@
1
+ require 'uri'
2
+
3
+ module MoCo
4
+
5
+ class CoffeeCompiler < JsCompiler
6
+
7
+ require_library 'runjs'
8
+ require_library 'coffee_script/source', 'coffee-script-source', '>= 1.6.2'
9
+ register 'coffee'
10
+
11
+ include SourceMap
12
+
13
+ def self.source_map_key
14
+ :sourceMap
15
+ end
16
+
17
+ def self.context
18
+ @context ||= RunJS.context(File.read(CoffeeScript::Source.bundled_path))
19
+ end
20
+
21
+ def compiled_text
22
+ compiled_text, @source_map_text = compile_coffee(options)
23
+ compiled_text
24
+ end
25
+
26
+ def options
27
+ options = super
28
+ options[:filename] = source_file
29
+ source_map_options(options)
30
+ end
31
+
32
+ private
33
+
34
+ def context
35
+ self.class.context
36
+ end
37
+
38
+ def compile_coffee(options)
39
+ fn = 'CoffeeScript.compile'
40
+ js = context.apply(fn, 'CoffeeScript', source_text, options)
41
+ if options[:sourceMap]
42
+ source_map = js['v3SourceMap']
43
+ js = js['js'] + source_map_comment
44
+ end
45
+ [js, source_map]
46
+ rescue RunJS::JavaScriptError => e
47
+ raise e, pretty_error_message(e)
48
+ end
49
+
50
+ def source_map_comment
51
+ file = URI.escape(File.basename(source_map_file))
52
+ "\n/*\n//@ sourceMappingURL=#{file}\n*/\n"
53
+ end
54
+
55
+ def pretty_error_message(error)
56
+ return error.message unless error['location']
57
+ fn = 'CoffeeScript.helpers.prettyErrorMessage'
58
+ context.call(fn, error.error, source_file, source_text, true)
59
+ end
60
+
61
+ def source_map_options(options)
62
+ return options unless options[:sourceMap]
63
+ { :generatedFile => File.basename(compiled_file),
64
+ :sourceFiles => [FileUtil.relative_path(source_map_file, source_file)]
65
+ }.merge(options)
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,21 @@
1
+ module MoCo
2
+
3
+ class HamlCompiler < HtmlCompiler
4
+
5
+ require_library 'haml'
6
+ register 'haml'
7
+
8
+ def compiled_text
9
+ Haml::Engine.new(source_text, options).render
10
+ rescue => e
11
+ e.instance_eval { @line += 1 if @line }
12
+ raise
13
+ end
14
+
15
+ def options
16
+ super.merge(:filename => source_file)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,15 @@
1
+ module MoCo
2
+
3
+ class LessCompiler < CssCompiler
4
+
5
+ require_library 'v8', 'therubyracer'
6
+ require_library 'less'
7
+ register 'less'
8
+
9
+ def compiled_text
10
+ Less::Parser.new(options).parse(source_text).to_css(options)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,139 @@
1
+ module MoCo
2
+
3
+ class MarkdownCompiler < HtmlCompiler
4
+
5
+ if RUBY_VERSION < '1.9'
6
+ require_library 'redcarpet', 'redcarpet', '~> 2.0'
7
+ else
8
+ require_library 'redcarpet'
9
+ end
10
+
11
+ register 'markdown'
12
+ register 'md'
13
+
14
+ def self.set_option(key, value = true)
15
+ if key == :pygments
16
+ value = pygments_options(value)
17
+ end
18
+ super
19
+ end
20
+
21
+ def compiled_text
22
+ if options[:layout]
23
+ layout(toc, body)
24
+ else
25
+ toc + body
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def self.pygments_options(value)
32
+ return unless value
33
+ unless Hash === value
34
+ key, *value = Array(value)
35
+ value = value.first if value.size < 2
36
+ value = true if value.nil?
37
+ value = { key => value }
38
+ value.delete(true)
39
+ end
40
+ options[:pygments] ? options[:pygments].merge(value) : value
41
+ end
42
+
43
+ private_class_method :pygments_options
44
+
45
+ def layout(toc, body)
46
+ layout = read_layout
47
+ layout = layout.gsub('{{TITLE}}', header(body) || file)
48
+ layout = layout.gsub('{{FILE}}', file)
49
+ if layout.include?('{{TOC}}')
50
+ layout.sub('{{TOC}}', toc.chop).sub('{{BODY}}', body)
51
+ else
52
+ layout.sub('{{BODY}}', toc + body)
53
+ end
54
+ end
55
+
56
+ def read_layout
57
+ File.read(options[:layout])
58
+ end
59
+
60
+ def header(body)
61
+ h1 = body[/<h1\b[^>]*>(.*?)<\/h1>/m, 1]
62
+ h1.gsub(/<[^>]+>/, '').gsub(/\s+/, ' ').strip if h1
63
+ end
64
+
65
+ def file
66
+ File.basename(source_file)
67
+ end
68
+
69
+ def toc
70
+ if options[:toc]
71
+ toc = render(Redcarpet::Render::HTML_TOC.new)
72
+ toc << "\n" unless toc.empty?
73
+ end
74
+ toc || ''
75
+ end
76
+
77
+ def body
78
+ if options[:pygments]
79
+ renderer = pygments_renderer(options[:pygments])
80
+ else
81
+ renderer = Redcarpet::Render::HTML
82
+ end
83
+ options = render_options
84
+ render(renderer.new(options), options)
85
+ end
86
+
87
+ def render(renderer, extensions = {})
88
+ renderer.extend(Redcarpet::Render::SmartyPants) if options[:smarty]
89
+ Redcarpet::Markdown.new(renderer, extensions).render(source_text)
90
+ end
91
+
92
+ def render_options
93
+ options = options().dup
94
+ options[:with_toc_data] = true if options[:toc]
95
+ options[:fenced_code_blocks] = true if options[:pygments]
96
+ [:pygments, :smarty, :toc, :layout, :title].each do |key|
97
+ options.delete(key)
98
+ end
99
+ options
100
+ end
101
+
102
+ def pygments_renderer(options)
103
+
104
+ require_library('pygments', 'pygments.rb') unless defined? Pygments
105
+
106
+ Class.new(Redcarpet::Render::HTML) do
107
+
108
+ define_method(:options) do
109
+ options.dup
110
+ end
111
+
112
+ def block_code(code, language)
113
+ lexer = Pygments::Lexer.find(language) if language
114
+ if lexer
115
+ "\n" + lexer.highlight(code, :options => options) + "\n"
116
+ else
117
+ klass = %( class="#{escape_html(language)}") if language
118
+ "\n<pre><code#{klass}>#{escape_html(code)}</code></pre>\n"
119
+ end
120
+ end
121
+
122
+ def escape_html(text)
123
+ escaped = {
124
+ "'" => '&#39;',
125
+ '<' => '&lt;',
126
+ '&' => '&amp;',
127
+ '>' => '&gt;',
128
+ '"' => '&quot;'
129
+ }
130
+ text.gsub(/['<&>"]/) { |char| escaped[char] }
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
@@ -0,0 +1,25 @@
1
+ module MoCo
2
+
3
+ class SassCompiler < CssCompiler
4
+
5
+ require_library 'sass'
6
+ register 'sass'
7
+ register 'scss'
8
+
9
+ def compiled_text
10
+ Sass::Engine.new(source_text, options).render
11
+ rescue => e
12
+ e.instance_eval { alias :line :sass_line } if defined? e.sass_line
13
+ raise
14
+ end
15
+
16
+ def options
17
+ { :syntax => (source_file =~ /\.sass$/) ? :sass : :scss,
18
+ :cache => false,
19
+ :read_cache => false
20
+ }.merge(super)
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,54 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+
4
+ module MoCo
5
+
6
+ class FileUtil < File
7
+
8
+ def self.normalized_extension(file)
9
+ file = file.to_s
10
+ extension = extname(file)
11
+ extension = basename(file) if extension.empty?
12
+ extension.delete('.').strip
13
+ end
14
+
15
+ def self.replace_extension(file, ext)
16
+ return file unless ext
17
+ file = file.chomp(extname(file))
18
+ unless ext.empty?
19
+ ext = '.' << ext unless ext.start_with?('.')
20
+ file << ext unless file.end_with?(ext)
21
+ end
22
+ file
23
+ end
24
+
25
+ def self.replace_directory(file, dir)
26
+ return file unless dir
27
+ file = basename(file)
28
+ file = join(dir, file) unless dir.empty?
29
+ file
30
+ end
31
+
32
+ def self.short_path(path)
33
+ home = File.expand_path('~')
34
+ path.sub(home, '~')
35
+ end
36
+
37
+ def self.relative_path(from_file, to_file)
38
+ from = Pathname.new(from_file)
39
+ to = Pathname.new(to_file)
40
+ to.relative_path_from(from.dirname).to_s
41
+ end
42
+
43
+ def self.up_to_date?(file, compared_to_file)
44
+ exist?(file) && mtime(file) >= mtime(compared_to_file)
45
+ end
46
+
47
+ def self.write(file, text)
48
+ FileUtils.makedirs(dirname(file))
49
+ open(file, 'w') { |f| f.write(text) }
50
+ end
51
+
52
+ end
53
+
54
+ end
data/lib/moco/log.rb ADDED
@@ -0,0 +1,108 @@
1
+ module MoCo
2
+
3
+ module Log
4
+
5
+ @quiet = false
6
+
7
+ def self.quiet=(quiet)
8
+ @quiet = quiet
9
+ end
10
+
11
+ def self.load(files)
12
+ files.each do |file|
13
+ log([:Loading, file, files])
14
+ end
15
+ end
16
+
17
+ def self.monitor
18
+ log
19
+ log('Press Ctrl-C to stop monitoring')
20
+ end
21
+
22
+ def self.compile(compiler)
23
+ log
24
+ log([:Compile, compiler.source_file, files(compiler)])
25
+ end
26
+
27
+ def self.update(compiler)
28
+ updated_files(compiler).each do |file|
29
+ log([:Updated, file, files(compiler)])
30
+ end
31
+ end
32
+
33
+ def self.error(e)
34
+ if @quiet
35
+ log(nil, true)
36
+ log([:Compile, e.file], true)
37
+ end
38
+ log(error_on_line(e), true)
39
+ log(error_message(e), true)
40
+ end
41
+
42
+ private
43
+
44
+ def self.log(status = nil, force = false)
45
+ return if @quiet && ! force
46
+ if Array === status
47
+ status, file, files = status
48
+ dir = FileUtil.short_path(File.dirname(file))
49
+ file = File.basename(file)
50
+ file = file.ljust(max_length(files)) if files
51
+ status = "#{status}: #{file} (#{dir})"
52
+ end
53
+ puts status || ''
54
+ end
55
+
56
+ private_class_method :log
57
+
58
+ def self.max_length(files)
59
+ files = files.map { |file| File.basename(file) }
60
+ files.max_by(&:length).length
61
+ end
62
+
63
+ private_class_method :max_length
64
+
65
+ def self.files(compiler)
66
+ [compiler.source_file] + compiled_files(compiler)
67
+ end
68
+
69
+ private_class_method :files
70
+
71
+ def self.compiled_files(compiler)
72
+ [compiler.compiled_file, source_map_file(compiler)].compact
73
+ end
74
+
75
+ private_class_method :compiled_files
76
+
77
+ def self.source_map_file(compiler)
78
+ klass = compiler.class
79
+ if klass < SourceMap && klass.options[klass.source_map_key]
80
+ compiler.source_map_file
81
+ end
82
+ end
83
+
84
+ private_class_method :source_map_file
85
+
86
+ def self.updated_files(compiler)
87
+ compiled_files(compiler).select do |file|
88
+ FileUtil.up_to_date?(file, compiler.source_file)
89
+ end
90
+ end
91
+
92
+ private_class_method :updated_files
93
+
94
+ def self.error_on_line(e)
95
+ AnsiEscape.bold_red(e.line ? "Error on line #{e.line}:" : 'Error:')
96
+ end
97
+
98
+ private_class_method :error_on_line
99
+
100
+ def self.error_message(e)
101
+ $stdout.tty? ? e.message : AnsiEscape.unescape(e.message)
102
+ end
103
+
104
+ private_class_method :error_message
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,83 @@
1
+ require 'rb-fsevent'
2
+ require 'find'
3
+
4
+ module MoCo
5
+
6
+ class Monitor
7
+
8
+ def initialize(files, directories, extensions)
9
+ @files = files
10
+ @pattern = pattern(directories, extensions)
11
+ @directories = directories + files.map { |file| File.dirname(file) }
12
+ @directories = delete_nested(@directories)
13
+ end
14
+
15
+ def files
16
+ set_timestamps
17
+ @timestamps.keys
18
+ end
19
+
20
+ def monitor(&callback)
21
+ set_timestamps
22
+ options = { :no_defer => true, :latency => 0.1 }
23
+ fsevent = FSEvent.new
24
+ fsevent.watch(@directories, options) do |updated_dirs|
25
+ on_update(updated_dirs, &callback)
26
+ end
27
+ fsevent.run
28
+ end
29
+
30
+ private
31
+
32
+ def pattern(dirs, exts)
33
+ return nil if dirs.empty? || exts.empty?
34
+ dirs = delete_nested(dirs.dup)
35
+ dirs = escape(dirs).join('|')
36
+ exts = escape(exts).join('|')
37
+ /^(#{dirs}).*\.(#{exts})$/
38
+ end
39
+
40
+ def escape(values)
41
+ values.map { |value| Regexp.escape(value.to_s) }
42
+ end
43
+
44
+ def delete_nested(dirs)
45
+ dirs.delete_if do |nested_dir|
46
+ dirs.any? do |dir|
47
+ nested_dir != dir && nested_dir.start_with?(dir)
48
+ end
49
+ end.uniq
50
+ end
51
+
52
+ def set_timestamps
53
+ @timestamps = {}
54
+ Find.find(*@directories) do |file|
55
+ store_timestamp(file) if monitor?(file)
56
+ end
57
+ end
58
+
59
+ def store_timestamp(file)
60
+ @timestamps[file] = File.mtime(file)
61
+ end
62
+
63
+ def monitor?(file)
64
+ @files.include?(file) || (@pattern && file =~ @pattern && File.file?(file))
65
+ end
66
+
67
+ def updated?(file)
68
+ @timestamps[file] != File.mtime(file)
69
+ end
70
+
71
+ def on_update(dirs, &callback)
72
+ dirs = delete_nested(dirs)
73
+ Find.find(*dirs) do |file|
74
+ if monitor?(file) && updated?(file)
75
+ store_timestamp(file)
76
+ yield file
77
+ end
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ end