ktheory-juicer 1.0.0.ktheory1

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 (98) hide show
  1. data/History.txt +30 -0
  2. data/Manifest.txt +58 -0
  3. data/Rakefile +96 -0
  4. data/Readme.rdoc +312 -0
  5. data/VERSION +1 -0
  6. data/bin/juicer +8 -0
  7. data/lib/juicer.rb +70 -0
  8. data/lib/juicer/asset/path.rb +275 -0
  9. data/lib/juicer/asset/path_resolver.rb +79 -0
  10. data/lib/juicer/binary.rb +171 -0
  11. data/lib/juicer/cache_buster.rb +130 -0
  12. data/lib/juicer/chainable.rb +106 -0
  13. data/lib/juicer/cli.rb +56 -0
  14. data/lib/juicer/command/install.rb +61 -0
  15. data/lib/juicer/command/list.rb +57 -0
  16. data/lib/juicer/command/merge.rb +205 -0
  17. data/lib/juicer/command/util.rb +32 -0
  18. data/lib/juicer/command/verify.rb +60 -0
  19. data/lib/juicer/css_cache_buster.rb +80 -0
  20. data/lib/juicer/datafy/datafy.rb +20 -0
  21. data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
  22. data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
  23. data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
  24. data/lib/juicer/ext/logger.rb +5 -0
  25. data/lib/juicer/ext/string.rb +47 -0
  26. data/lib/juicer/ext/symbol.rb +15 -0
  27. data/lib/juicer/image_embed.rb +136 -0
  28. data/lib/juicer/install/base.rb +186 -0
  29. data/lib/juicer/install/closure_compiler_installer.rb +69 -0
  30. data/lib/juicer/install/jslint_installer.rb +51 -0
  31. data/lib/juicer/install/rhino_installer.rb +53 -0
  32. data/lib/juicer/install/yui_compressor_installer.rb +67 -0
  33. data/lib/juicer/jslint.rb +90 -0
  34. data/lib/juicer/merger/base.rb +74 -0
  35. data/lib/juicer/merger/javascript_merger.rb +29 -0
  36. data/lib/juicer/merger/stylesheet_merger.rb +110 -0
  37. data/lib/juicer/minifyer/closure_compiler.rb +90 -0
  38. data/lib/juicer/minifyer/java_base.rb +77 -0
  39. data/lib/juicer/minifyer/yui_compressor.rb +96 -0
  40. data/test/bin/jslint-1.0.js +523 -0
  41. data/test/bin/jslint.js +523 -0
  42. data/test/bin/rhino1_7R1.zip +0 -0
  43. data/test/bin/rhino1_7R2-RC1.jar +0 -0
  44. data/test/bin/rhino1_7R2-RC1.zip +0 -0
  45. data/test/bin/yuicompressor +0 -0
  46. data/test/bin/yuicompressor-2.3.5.zip +0 -0
  47. data/test/bin/yuicompressor-2.4.2.jar +0 -0
  48. data/test/bin/yuicompressor-2.4.2.zip +0 -0
  49. data/test/data/Changelog.txt +10 -0
  50. data/test/data/a.css +3 -0
  51. data/test/data/a.js +5 -0
  52. data/test/data/a1.css +5 -0
  53. data/test/data/b.css +1 -0
  54. data/test/data/b.js +5 -0
  55. data/test/data/b1.css +5 -0
  56. data/test/data/c1.css +3 -0
  57. data/test/data/css/2.gif +1 -0
  58. data/test/data/css/test.css +11 -0
  59. data/test/data/css/test2.css +1 -0
  60. data/test/data/d1.css +3 -0
  61. data/test/data/images/1.png +1 -0
  62. data/test/data/my_app.js +2 -0
  63. data/test/data/not-ok.js +2 -0
  64. data/test/data/ok.js +3 -0
  65. data/test/data/path_test.css +5 -0
  66. data/test/data/path_test2.css +14 -0
  67. data/test/data/pkg/module/moda.js +2 -0
  68. data/test/data/pkg/module/modb.js +3 -0
  69. data/test/data/pkg/pkg.js +1 -0
  70. data/test/test_helper.rb +169 -0
  71. data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
  72. data/test/unit/juicer/asset/path_test.rb +370 -0
  73. data/test/unit/juicer/cache_buster_test.rb +104 -0
  74. data/test/unit/juicer/chainable_test.rb +94 -0
  75. data/test/unit/juicer/command/install_test.rb +58 -0
  76. data/test/unit/juicer/command/list_test.rb +81 -0
  77. data/test/unit/juicer/command/merge_test.rb +162 -0
  78. data/test/unit/juicer/command/util_test.rb +58 -0
  79. data/test/unit/juicer/command/verify_test.rb +48 -0
  80. data/test/unit/juicer/css_cache_buster_test.rb +71 -0
  81. data/test/unit/juicer/datafy_test.rb +37 -0
  82. data/test/unit/juicer/dependency_resolver/css_dependency_resolver_test.rb +36 -0
  83. data/test/unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb +50 -0
  84. data/test/unit/juicer/ext/string_test.rb +59 -0
  85. data/test/unit/juicer/ext/symbol_test.rb +27 -0
  86. data/test/unit/juicer/image_embed_test.rb +271 -0
  87. data/test/unit/juicer/install/installer_base_test.rb +214 -0
  88. data/test/unit/juicer/install/jslint_installer_test.rb +54 -0
  89. data/test/unit/juicer/install/rhino_installer_test.rb +57 -0
  90. data/test/unit/juicer/install/yui_compressor_test.rb +56 -0
  91. data/test/unit/juicer/jslint_test.rb +60 -0
  92. data/test/unit/juicer/merger/base_test.rb +122 -0
  93. data/test/unit/juicer/merger/javascript_merger_test.rb +74 -0
  94. data/test/unit/juicer/merger/stylesheet_merger_test.rb +180 -0
  95. data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
  96. data/test/unit/juicer/minifyer/yui_compressor_test.rb +116 -0
  97. data/test/unit/juicer_test.rb +1 -0
  98. metadata +265 -0
@@ -0,0 +1,186 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+ require 'fileutils'
4
+ require "juicer"
5
+
6
+ module Juicer
7
+ module Install
8
+ #
9
+ # Installer skeleton. Provides basic functionality like figuring out where
10
+ # to install, create base directories, remove unneeded directories and more
11
+ # housekeeping.
12
+ #
13
+ class Base
14
+ attr_reader :install_dir
15
+
16
+ #
17
+ # Create new installer
18
+ #
19
+ def initialize(install_dir = Juicer.home)
20
+ @install_dir = install_dir
21
+ @path = nil
22
+ @bin_path = nil
23
+ @name = nil
24
+ @dependencies = {}
25
+ end
26
+
27
+ #
28
+ # Returns the latest available version number. Must be implemented in
29
+ # subclasses. Raises an exception when called directly.
30
+ #
31
+ def latest
32
+ raise NotImplementedError.new("Implement in subclasses")
33
+ end
34
+
35
+ # Returns the path relative to installation path this installer will
36
+ # install to
37
+ def path
38
+ return @path if @path
39
+ @path = "lib/" + self.class.to_s.split("::").pop.sub(/Installer$/, "").underscore
40
+ end
41
+
42
+ # Returns the path to search for binaries from
43
+ #
44
+ def bin_path
45
+ return @bin_path if @bin_path
46
+ @bin_path = File.join(path, "bin")
47
+ end
48
+
49
+ #
50
+ # Returns name of component. Default implementation returns class name
51
+ # with "Installer" removed
52
+ #
53
+ def name
54
+ return @name if @name
55
+ @name = File.basename(path).split("_").inject("") { |str, word| (str + " #{word.capitalize}").strip }
56
+ end
57
+
58
+ #
59
+ # Checks if the component is currently installed.
60
+ #
61
+ # If no version is provided the most recent version is assumed.
62
+ #
63
+ def installed?(version = nil)
64
+ installed = File.exists?(File.join(@install_dir, path, "#{version || latest}"))
65
+ deps = @dependencies.length == 0 || dependencies.all? { |d, v| d.installed?(v) }
66
+ installed && deps
67
+ end
68
+
69
+ #
70
+ # Install the component. Creates basic directory structure.
71
+ #
72
+ def install(version = nil)
73
+ raise "#{name} #{version} is already installed in #{File.join(@install_dir, path)}" if installed?(version)
74
+ version ||= latest
75
+ log "Installing #{name} #{version} in #{File.join(@install_dir, path)}"
76
+
77
+ if @dependencies.length > 0
78
+ log "Installing dependencies"
79
+ dependencies { |dependency, ver| dependency.install(ver) unless dependency.installed?(ver) }
80
+ end
81
+
82
+ # Create directories
83
+ FileUtils.mkdir_p(File.join(@install_dir, path, "bin"))
84
+ FileUtils.mkdir_p(File.join(@install_dir, path, version))
85
+
86
+ # Return resolved version for subclass to use
87
+ version
88
+ end
89
+
90
+ #
91
+ # Uninstalls the given version of the component.
92
+ #
93
+ # If no version is provided the most recent version is assumed.
94
+ #
95
+ # If there are no more files left in INSTALLATION_PATH/<path>, the
96
+ # whole directory is removed.
97
+ #
98
+ # This method takes a block and can be used from subclasses like so:
99
+ #
100
+ # def self.uninstall(install_dir = nil, version = nil)
101
+ # super do |home_dir, version|
102
+ # # Custom uninstall logic
103
+ # end
104
+ # end
105
+ #
106
+ #
107
+ def uninstall(version = nil)
108
+ version ||= self.latest
109
+ install_dir = File.join(@install_dir, path, version)
110
+ raise "#{name} #{version} is not installed" if !File.exists?(install_dir)
111
+
112
+ FileUtils.rm_rf(install_dir)
113
+
114
+ yield(File.join(@install_dir, path), version) if block_given?
115
+
116
+ files = Dir.glob(File.join(@install_dir, path, "**", "*")).find_all { |f| File.file?(f) }
117
+ FileUtils.rm_rf(File.join(@install_dir, path)) if files.length == 0
118
+ end
119
+
120
+ #
121
+ # Download a file to Juicer temporary directory. The file will be kept
122
+ # until #purge is called to wipe it. If the installer receives a request
123
+ # to download the same file again, the disk cache will be used unless the
124
+ # force argument is true (default false)
125
+ #
126
+ def download(url, force = false)
127
+ filename = File.join(@install_dir, "download", path.sub("lib/", ""), File.basename(url))
128
+ return filename if File.exists?(filename) && !force
129
+ FileUtils.mkdir_p(File.dirname(filename))
130
+ File.delete(filename) if File.exists?(filename) && force
131
+
132
+ log "Downloading #{url}"
133
+ File.open(filename, "wb") do |file|
134
+ webpage = open(url)
135
+ file.write(webpage.read)
136
+ webpage.close
137
+ end
138
+
139
+ filename
140
+ end
141
+
142
+ #
143
+ # Display a message to the user through Juicer::LOGGER
144
+ #
145
+ def log(str)
146
+ Juicer::LOGGER.info str
147
+ end
148
+
149
+ #
150
+ # Add a dependency. Dependency should be a Juicer::Install::Base installer
151
+ # class (not instance) OR a symbol/string like :rhino/"rhino" (which will
152
+ # be expanded unto Juicer::Install::RhinoInstaller). Version is optional
153
+ # and defaults to latest and greatest.
154
+ #
155
+ def dependency(dependency, version = nil)
156
+ dependency = Juicer::Install.get(dependency) if [String, Symbol].include?(dependency.class)
157
+
158
+ @dependencies[dependency.to_s + (version || "")] = [dependency, version]
159
+ end
160
+
161
+ #
162
+ # Yields depencies one at a time: class and version and returns an array
163
+ # of arrays: [dependency, version] where dependency is an instance and
164
+ # version a string.
165
+ #
166
+ def dependencies(&block)
167
+ @dependencies.collect do |name, dependency|
168
+ version = dependency[1]
169
+ dependency = dependency[0].new(@install_dir)
170
+ block.call(dependency, version) if block
171
+ [dependency, version]
172
+ end
173
+ end
174
+ end
175
+
176
+ #
177
+ # Returns the installer. Accepts installer classes (which are returned
178
+ # directly), strings or symbols. Strings and symbols may be on the form
179
+ # :my_module which is expanded to Juicer::Install::MyModuleInstaller
180
+ #
181
+ def self.get(nameOrClass)
182
+ return nameOrClass if nameOrClass.is_a? Class
183
+ (nameOrClass.to_s + "_installer").classify(Juicer::Install)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,69 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the Google Closure Compiler.
9
+ # Installation downloads the Closure Compiler distribution, unzips it and
10
+ # storesthe jar file on disk along with the README.
11
+ #
12
+ class ClosureCompilerInstaller < Base
13
+ def initialize(install_dir = Juicer.home)
14
+ super(install_dir)
15
+ @latest = nil
16
+ @website = "http://code.google.com/p/closure-compiler/downloads/list"
17
+ @download_link = "http://closure-compiler.googlecode.com/files/compiler-%s.zip"
18
+ end
19
+
20
+ #
21
+ # Install the Closure Compiler. Downloads the distribution and keeps the jar
22
+ # file inside PATH/closure_compiler/bin and the README in
23
+ # PATH/closere_compiler/yyyymmdd/ where yyyymmdd is the version, most recent if
24
+ # not specified otherwise.
25
+ #
26
+ # Path defaults to environment variable $JUICER_HOME or default Juicer
27
+ # home
28
+ #
29
+ def install(version = nil)
30
+ version = super(version)
31
+ base = "closure-compiler-#{version}"
32
+ filename = download(@download_link % version)
33
+ target = File.join(@install_dir, path)
34
+
35
+ Zip::ZipFile.open(filename) do |file|
36
+ file.extract("README", File.join(target, version, "README"))
37
+ file.extract("compiler.jar", File.join(target, "bin", "#{base}.jar"))
38
+ end
39
+ end
40
+
41
+ #
42
+ # Uninstalls the given version of Closure Compiler. If no location is
43
+ # provided the environment variable $JUICER_HOME or Juicers default home
44
+ # directory is used.
45
+ #
46
+ # If no version is provided the most recent version is assumed.
47
+ #
48
+ # If there are no more files left in INSTALLATION_PATH/closure_compiler, the
49
+ # whole directory is removed.
50
+ #
51
+ def uninstall(version = nil)
52
+ super(version) do |dir, version|
53
+ File.delete(File.join(dir, "bin/closure-compiler-#{version}.jar"))
54
+ end
55
+ end
56
+
57
+ #
58
+ # Check which version is the most recent
59
+ #
60
+ def latest
61
+ return @latest if @latest
62
+ webpage = Nokogiri::HTML(open(@website))
63
+ @latest = (webpage / "//table[@id='resultstable']//td/a[contains(@href, 'compiler')]").map{|link|
64
+ link.get_attribute('href')[/\d{8}/].to_i
65
+ }.sort.last.to_s
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the JSLint library by Douglas Crockford.
9
+ # Installation downloads the jslintfull.js and rhino.js files and stores
10
+ # them in the Juicer installation directory.
11
+ #
12
+ class JSLintInstaller < Base
13
+ attr_reader :latest
14
+
15
+ def initialize(install_dir = Juicer.home)
16
+ super(install_dir)
17
+ @latest = "1.0"
18
+ @website = "http://www.jslint.com/"
19
+ @path = "lib/jslint"
20
+ @name = "JsLint"
21
+ dependency :rhino
22
+ end
23
+
24
+ #
25
+ # Install JSLint. Downloads the two js files and stores them in the
26
+ # installation directory.
27
+ #
28
+ def install(version = nil)
29
+ version = super(version)
30
+ filename = download(File.join(@website, "rhino/jslint.js"))
31
+ FileUtils.copy(filename, File.join(@install_dir, path, "bin", "jslint-#{version}.js"))
32
+ end
33
+
34
+ #
35
+ # Uninstalls JSLint
36
+ #
37
+ def uninstall(version = nil)
38
+ super(version) do |dir, version|
39
+ File.delete(File.join(dir, "bin", "jslint-#{version}.js"))
40
+ end
41
+ end
42
+ end
43
+
44
+ #
45
+ # This class makes it possible to do Juicer.install("jslint") instead of
46
+ # Juicer.install("j_s_lint"). Sugar, sugar...
47
+ #
48
+ class JslintInstaller < JSLintInstaller
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,53 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the Mozilla Rhino jar.
9
+ #
10
+ class RhinoInstaller < Base
11
+ attr_reader :latest
12
+
13
+ def initialize(install_dir = Juicer.home)
14
+ super(install_dir)
15
+ @latest = "1_7R2-RC1"
16
+ @website = "http://ftp.mozilla.org/pub/mozilla.org/js/"
17
+ end
18
+
19
+ #
20
+ # Install Rhino. Downloads the jar file and stores it in the installation
21
+ # directory along with the License text.
22
+ #
23
+ def install(version = nil)
24
+ version = super((version || latest).gsub(/\./, "_"))
25
+ base = "rhino#{version}"
26
+ filename = download(File.join(@website, "#{base}.zip"))
27
+ target = File.join(@install_dir, path)
28
+
29
+ Zip::ZipFile.open(filename) do |file|
30
+ FileUtils.mkdir_p(File.join(target, version))
31
+
32
+ begin
33
+ file.extract("#{base.sub(/-RC\d/, "")}/LICENSE.txt", File.join(target, version, "LICENSE.txt"))
34
+ rescue Exception
35
+ # Fail silently, some releases don't carry the license
36
+ end
37
+
38
+ file.extract("#{base.sub(/-RC\d/, "")}/js.jar", File.join(target, "bin", "#{base}.jar"))
39
+ end
40
+ end
41
+
42
+ #
43
+ # Uninstalls Rhino
44
+ #
45
+ def uninstall(version = nil)
46
+ super((version || latest).gsub(/\./, "_")) do |dir, version|
47
+ base = "rhino#{version}"
48
+ File.delete(File.join(dir, "bin/", "#{base}.jar"))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the YUI Compressor.
9
+ # Installation downloads the YUI Compressor distribution, unzips it and
10
+ # storesthe jar file on disk along with the license.
11
+ #
12
+ class YuiCompressorInstaller < Base
13
+ def initialize(install_dir = Juicer.home)
14
+ super(install_dir)
15
+ @latest = nil
16
+ @website = "http://yuilibrary.com/downloads/"
17
+ end
18
+
19
+ #
20
+ # Install the Yui Compressor. Downloads the distribution and keeps the jar
21
+ # file inside PATH/yui_compressor/bin and the README and CHANGELOG in
22
+ # PATH/yui_compressor/x.y.z/ where x.y.z is the version, most recent if
23
+ # not specified otherwise.
24
+ #
25
+ # Path defaults to environment variable $JUICER_HOME or default Juicer
26
+ # home
27
+ #
28
+ def install(version = nil)
29
+ version = super(version)
30
+ base = "yuicompressor-#{version}"
31
+ filename = download(File.join(@website, "yuicompressor", "#{base}.zip"))
32
+ target = File.join(@install_dir, path)
33
+
34
+ Zip::ZipFile.open(filename) do |file|
35
+ file.extract("#{base}/doc/README", File.join(target, version, "README"))
36
+ file.extract("#{base}/doc/CHANGELOG", File.join(target, version, "CHANGELOG"))
37
+ file.extract("#{base}/build/#{base}.jar", File.join(target, "bin", "#{base}.jar"))
38
+ end
39
+ end
40
+
41
+ #
42
+ # Uninstalls the given version of YUI Compressor. If no location is
43
+ # provided the environment variable $JUICER_HOME or Juicers default home
44
+ # directory is used.
45
+ #
46
+ # If no version is provided the most recent version is assumed.
47
+ #
48
+ # If there are no more files left in INSTALLATION_PATH/yui_compressor, the
49
+ # whole directory is removed.
50
+ #
51
+ def uninstall(version = nil)
52
+ super(version) do |dir, version|
53
+ File.delete(File.join(dir, "bin/yuicompressor-#{version}.jar"))
54
+ end
55
+ end
56
+
57
+ #
58
+ # Check which version is the most recent
59
+ #
60
+ def latest
61
+ return @latest if @latest
62
+ webpage = Nokogiri::HTML(open(@website))
63
+ @latest = (webpage / "//h2[@id='yuicompressor']/../../../..//a")[0].get_attribute("href").match(/(\d\.\d\.\d)/)[1]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,90 @@
1
+ require "juicer/binary"
2
+
3
+ module Juicer
4
+ #
5
+ # A Ruby API to Douglas Crockfords genious JsLint program
6
+ # http://www.jslint.com/
7
+ #
8
+ # JsLint parses JavaScript code and identifies (potential) problems.
9
+ # Effectively, JsLint defines a subset of JavaScript which is safe to use, and
10
+ # among other things make code minification a substantially less dangerous
11
+ # task.
12
+ #
13
+ class JsLint
14
+ include Juicer::Binary
15
+
16
+ def initialize(options = {})
17
+ super(options[:java] || "java")
18
+ path << options[:bin_path] if options[:bin_path]
19
+ end
20
+
21
+ #
22
+ # Checks if a files has problems. Also includes experimental support for CSS
23
+ # files. CSS files should begin with the line @charset "UTF-8";
24
+ #
25
+ # Returns a Juicer::JsLint::Report object
26
+ #
27
+ def check(file)
28
+ rhino_jar = rhino
29
+ js_file = locate_lib
30
+
31
+ raise FileNotFoundError.new("Unable to locate Rhino jar '#{rhino_jar}'") if !rhino_jar || !File.exists?(rhino_jar)
32
+ raise FileNotFoundError.new("Unable to locate JsLint '#{js_file}'") if !js_file || !File.exists?(js_file)
33
+ raise FileNotFoundError.new("Unable to locate input file '#{file}'") unless File.exists?(file)
34
+
35
+ lines = execute(%Q{-jar "#{rhino}" "#{locate_lib}" "#{file}"}).split("\n")
36
+ return Report.new if lines.length == 1 && lines[0] =~ /jslint: No problems/
37
+
38
+ report = Report.new
39
+ lines = lines.reject { |line| !line || "#{line}".strip == "" }
40
+ report.add_error(lines.shift, lines.shift) while lines.length > 0
41
+
42
+ return report
43
+ end
44
+
45
+ def rhino
46
+ files = locate("**/rhino*.jar", "RHINO_HOME")
47
+ !files || files.empty? ? nil : files.sort.last
48
+ end
49
+
50
+ def locate_lib
51
+ files = locate("**/jslint-*.js", "JSLINT_HOME")
52
+ !files || files.empty? ? nil : files.sort.last
53
+ end
54
+
55
+ #
56
+ # Represents the results of a JsLint run
57
+ #
58
+ class Report
59
+ attr_accessor :errors
60
+
61
+ def initialize(errors = [])
62
+ @errors = errors
63
+ end
64
+
65
+ def add_error(message, code)
66
+ @errors << JsLint::Error.new(message, code)
67
+ end
68
+
69
+ def ok?
70
+ @errors.nil? || @errors.length == 0
71
+ end
72
+ end
73
+
74
+ #
75
+ # A JsLint error
76
+ #
77
+ class Error
78
+ attr_accessor :message, :code
79
+
80
+ def initialize(message, code)
81
+ @message = message
82
+ @code = code
83
+ end
84
+
85
+ def to_s
86
+ "#@message\n#@code"
87
+ end
88
+ end
89
+ end
90
+ end