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