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.
- data/History.txt +30 -0
- data/Manifest.txt +58 -0
- data/Rakefile +96 -0
- data/Readme.rdoc +312 -0
- data/VERSION +1 -0
- data/bin/juicer +8 -0
- data/lib/juicer.rb +70 -0
- data/lib/juicer/asset/path.rb +275 -0
- data/lib/juicer/asset/path_resolver.rb +79 -0
- data/lib/juicer/binary.rb +171 -0
- data/lib/juicer/cache_buster.rb +130 -0
- data/lib/juicer/chainable.rb +106 -0
- data/lib/juicer/cli.rb +56 -0
- data/lib/juicer/command/install.rb +61 -0
- data/lib/juicer/command/list.rb +57 -0
- data/lib/juicer/command/merge.rb +205 -0
- data/lib/juicer/command/util.rb +32 -0
- data/lib/juicer/command/verify.rb +60 -0
- data/lib/juicer/css_cache_buster.rb +80 -0
- data/lib/juicer/datafy/datafy.rb +20 -0
- data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
- data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
- data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
- data/lib/juicer/ext/logger.rb +5 -0
- data/lib/juicer/ext/string.rb +47 -0
- data/lib/juicer/ext/symbol.rb +15 -0
- data/lib/juicer/image_embed.rb +136 -0
- data/lib/juicer/install/base.rb +186 -0
- data/lib/juicer/install/closure_compiler_installer.rb +69 -0
- data/lib/juicer/install/jslint_installer.rb +51 -0
- data/lib/juicer/install/rhino_installer.rb +53 -0
- data/lib/juicer/install/yui_compressor_installer.rb +67 -0
- data/lib/juicer/jslint.rb +90 -0
- data/lib/juicer/merger/base.rb +74 -0
- data/lib/juicer/merger/javascript_merger.rb +29 -0
- data/lib/juicer/merger/stylesheet_merger.rb +110 -0
- data/lib/juicer/minifyer/closure_compiler.rb +90 -0
- data/lib/juicer/minifyer/java_base.rb +77 -0
- data/lib/juicer/minifyer/yui_compressor.rb +96 -0
- data/test/bin/jslint-1.0.js +523 -0
- data/test/bin/jslint.js +523 -0
- data/test/bin/rhino1_7R1.zip +0 -0
- data/test/bin/rhino1_7R2-RC1.jar +0 -0
- data/test/bin/rhino1_7R2-RC1.zip +0 -0
- data/test/bin/yuicompressor +0 -0
- data/test/bin/yuicompressor-2.3.5.zip +0 -0
- data/test/bin/yuicompressor-2.4.2.jar +0 -0
- data/test/bin/yuicompressor-2.4.2.zip +0 -0
- data/test/data/Changelog.txt +10 -0
- data/test/data/a.css +3 -0
- data/test/data/a.js +5 -0
- data/test/data/a1.css +5 -0
- data/test/data/b.css +1 -0
- data/test/data/b.js +5 -0
- data/test/data/b1.css +5 -0
- data/test/data/c1.css +3 -0
- data/test/data/css/2.gif +1 -0
- data/test/data/css/test.css +11 -0
- data/test/data/css/test2.css +1 -0
- data/test/data/d1.css +3 -0
- data/test/data/images/1.png +1 -0
- data/test/data/my_app.js +2 -0
- data/test/data/not-ok.js +2 -0
- data/test/data/ok.js +3 -0
- data/test/data/path_test.css +5 -0
- data/test/data/path_test2.css +14 -0
- data/test/data/pkg/module/moda.js +2 -0
- data/test/data/pkg/module/modb.js +3 -0
- data/test/data/pkg/pkg.js +1 -0
- data/test/test_helper.rb +169 -0
- data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
- data/test/unit/juicer/asset/path_test.rb +370 -0
- data/test/unit/juicer/cache_buster_test.rb +104 -0
- data/test/unit/juicer/chainable_test.rb +94 -0
- data/test/unit/juicer/command/install_test.rb +58 -0
- data/test/unit/juicer/command/list_test.rb +81 -0
- data/test/unit/juicer/command/merge_test.rb +162 -0
- data/test/unit/juicer/command/util_test.rb +58 -0
- data/test/unit/juicer/command/verify_test.rb +48 -0
- data/test/unit/juicer/css_cache_buster_test.rb +71 -0
- data/test/unit/juicer/datafy_test.rb +37 -0
- data/test/unit/juicer/dependency_resolver/css_dependency_resolver_test.rb +36 -0
- data/test/unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb +50 -0
- data/test/unit/juicer/ext/string_test.rb +59 -0
- data/test/unit/juicer/ext/symbol_test.rb +27 -0
- data/test/unit/juicer/image_embed_test.rb +271 -0
- data/test/unit/juicer/install/installer_base_test.rb +214 -0
- data/test/unit/juicer/install/jslint_installer_test.rb +54 -0
- data/test/unit/juicer/install/rhino_installer_test.rb +57 -0
- data/test/unit/juicer/install/yui_compressor_test.rb +56 -0
- data/test/unit/juicer/jslint_test.rb +60 -0
- data/test/unit/juicer/merger/base_test.rb +122 -0
- data/test/unit/juicer/merger/javascript_merger_test.rb +74 -0
- data/test/unit/juicer/merger/stylesheet_merger_test.rb +180 -0
- data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
- data/test/unit/juicer/minifyer/yui_compressor_test.rb +116 -0
- data/test/unit/juicer_test.rb +1 -0
- metadata +265 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
module Juicer
|
4
|
+
module Command
|
5
|
+
# Utilities for Juicer command objects
|
6
|
+
#
|
7
|
+
module Util
|
8
|
+
# Returns an array of files from a variety of input. Input may be a single
|
9
|
+
# file, a single glob pattern or multiple files and/or patterns. It may
|
10
|
+
# even be an array of mixed input.
|
11
|
+
#
|
12
|
+
def files(*args)
|
13
|
+
args.flatten.collect { |file| Dir.glob(file) }.flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Uses Pathname to calculate the shortest relative path from +path+ to
|
18
|
+
# +reference_path+ (default is +Dir.cwd+)
|
19
|
+
#
|
20
|
+
def relative(paths, reference_path = Dir.pwd)
|
21
|
+
paths = [paths].flatten.collect do |path|
|
22
|
+
path = Pathname.new(File.expand_path(path))
|
23
|
+
reference_path = Pathname.new(File.expand_path(reference_path))
|
24
|
+
path.relative_path_from(reference_path).to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
paths.length == 1 ? paths.first : paths
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "juicer/command/util"
|
2
|
+
require "rubygems"
|
3
|
+
require "cmdparse"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module Juicer
|
7
|
+
module Command
|
8
|
+
# Verifies problem-free-ness of source code (JavaScript and CSS)
|
9
|
+
#
|
10
|
+
class Verify < CmdParse::Command
|
11
|
+
include Juicer::Command::Util
|
12
|
+
|
13
|
+
# Initializes command
|
14
|
+
#
|
15
|
+
def initialize(log = nil)
|
16
|
+
super('verify', false, true)
|
17
|
+
@log = log || Logger.new($STDIO)
|
18
|
+
self.short_desc = "Verifies that the given JavaScript/CSS file is problem free"
|
19
|
+
self.description = <<-EOF
|
20
|
+
Uses JsLint (http://www.jslint.com) to check that code adheres to good coding
|
21
|
+
practices to avoid potential bugs, and protect against introducing bugs by
|
22
|
+
minifying.
|
23
|
+
EOF
|
24
|
+
end
|
25
|
+
|
26
|
+
# Execute command
|
27
|
+
#
|
28
|
+
def execute(args)
|
29
|
+
# Need atleast one file
|
30
|
+
raise ArgumentError.new('Please provide atleast one input file/pattern') if args.length == 0
|
31
|
+
Juicer::Command::Verify.check_all(files(args), @log)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.check_all(files, log = nil)
|
35
|
+
log ||= Logger.new($stdio)
|
36
|
+
jslint = Juicer::JsLint.new(:bin_path => Juicer.home)
|
37
|
+
problems = false
|
38
|
+
|
39
|
+
# Check that JsLint is installed
|
40
|
+
raise FileNotFoundError.new("Missing 3rd party library JsLint, install with\njuicer install jslint") if jslint.locate_lib.nil?
|
41
|
+
|
42
|
+
# Verify all files
|
43
|
+
files.each do |file|
|
44
|
+
log.info "Verifying #{file} with JsLint"
|
45
|
+
report = jslint.check(file)
|
46
|
+
|
47
|
+
if report.ok?
|
48
|
+
log.info " OK!"
|
49
|
+
else
|
50
|
+
problems = true
|
51
|
+
log.warn " Problems detected"
|
52
|
+
log.warn " #{report.errors.join("\n").gsub(/\n/, "\n ")}\n"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
!problems
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "juicer/chainable"
|
2
|
+
require "juicer/cache_buster"
|
3
|
+
require "juicer/asset/path_resolver"
|
4
|
+
|
5
|
+
module Juicer
|
6
|
+
#
|
7
|
+
# The CssCacheBuster is a tool that can parse a CSS file and substitute all
|
8
|
+
# referenced URLs by a URL appended with a timestamp denoting it's last change.
|
9
|
+
# This causes the URLs to be unique every time they've been modified, thus
|
10
|
+
# facilitating using a far future expires header on your web server.
|
11
|
+
#
|
12
|
+
# See Juicer::CacheBuster for more information on how the cache buster URLs
|
13
|
+
# work.
|
14
|
+
#
|
15
|
+
# When dealing with CSS files that reference absolute URLs like /images/1.png
|
16
|
+
# you must specify the :web_root option that these URLs should be resolved
|
17
|
+
# against.
|
18
|
+
#
|
19
|
+
# When dealing with full URLs (ie including hosts) you can optionally specify
|
20
|
+
# an array of hosts to recognize as "local", meaning they serve assets from
|
21
|
+
# the :web_root directory. This way even asset host cycling can benefit from
|
22
|
+
# cache busters.
|
23
|
+
#
|
24
|
+
class CssCacheBuster
|
25
|
+
include Juicer::Chainable
|
26
|
+
|
27
|
+
def initialize(options = {})
|
28
|
+
@web_root = options[:web_root]
|
29
|
+
@web_root.sub!(%r{/?$}, "") if @web_root
|
30
|
+
@type = options[:type] || :soft
|
31
|
+
@hosts = (options[:hosts] || []).collect { |h| h.sub!(%r{/?$}, "") }
|
32
|
+
@contents = nil
|
33
|
+
@path_resolver = Juicer::Asset::PathResolver.new(:document_root => options[:web_root],
|
34
|
+
:hosts => options[:hosts])
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Update file. If no +output+ is provided, the input file is overwritten
|
39
|
+
#
|
40
|
+
def save(file, output = nil)
|
41
|
+
@contents = File.read(file)
|
42
|
+
@path_resolver = Juicer::Asset::PathResolver.new(:document_root => @web_root,
|
43
|
+
:hosts => @hosts,
|
44
|
+
:base => File.dirname(file))
|
45
|
+
used = []
|
46
|
+
|
47
|
+
urls(file).each do |asset|
|
48
|
+
begin
|
49
|
+
next if used.include?(asset.path)
|
50
|
+
@contents.gsub!(asset.path, asset.path(:cache_buster_type => @type))
|
51
|
+
rescue Errno::ENOENT
|
52
|
+
puts "Unable to locate file #{asset.path}, skipping cache buster"
|
53
|
+
rescue ArgumentError => e
|
54
|
+
if e.message =~ /No document root/
|
55
|
+
raise FileNotFoundError.new("Unable to resolve path #{asset.path} without :web_root option")
|
56
|
+
else
|
57
|
+
raise e
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
File.open(output || file, "w") { |f| f.puts @contents }
|
63
|
+
@contents = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
chain_method :save
|
67
|
+
|
68
|
+
#
|
69
|
+
# Returns all referenced URLs in +file+. Returned paths are absolute (ie,
|
70
|
+
# they're resolved relative to the +file+ path.
|
71
|
+
#
|
72
|
+
def urls(file)
|
73
|
+
@contents = File.read(file) unless @contents
|
74
|
+
|
75
|
+
@contents.scan(/url\([\s"']*([^\)"'\s]*)[\s"']*\)/m).collect do |match|
|
76
|
+
@path_resolver.resolve(match.first)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
# Datafy code lifted from http://segment7.net/projects/ruby/datafy/
|
4
|
+
|
5
|
+
require 'base64'
|
6
|
+
require 'cgi'
|
7
|
+
|
8
|
+
module Datafy
|
9
|
+
def Datafy::make_data_uri(content, content_type)
|
10
|
+
outuri = 'data:' + content_type
|
11
|
+
unless content_type =~ /^text/i # base64 encode if not text
|
12
|
+
outuri += ';base64'
|
13
|
+
content = Base64.encode64(content).gsub("\n", '')
|
14
|
+
else
|
15
|
+
content = CGI::escape(content)
|
16
|
+
end
|
17
|
+
outuri += ",#{content}"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "juicer/dependency_resolver/dependency_resolver"
|
2
|
+
|
3
|
+
module Juicer
|
4
|
+
|
5
|
+
# Resolves @import statements in CSS files and builds a list of all
|
6
|
+
# files, in order.
|
7
|
+
#
|
8
|
+
class CssDependencyResolver < DependencyResolver
|
9
|
+
# Regexp borrowed from similar project:
|
10
|
+
# http://github.com/cgriego/front-end-blender/tree/master/lib/front_end_architect/blender.rb
|
11
|
+
@@import_pattern = /^\s*@import(?: url\(| )(['"]?)([^\?'"\)\s]+)(\?(?:[^'"\)]+)?)?\1\)?(?:[^?;]+)?;?/im
|
12
|
+
|
13
|
+
private
|
14
|
+
def parse(line, imported_file = nil)
|
15
|
+
return $2 if line =~ @@import_pattern
|
16
|
+
|
17
|
+
# At first sight of actual CSS rules we abort (TODO: This does not take
|
18
|
+
# into account the fact that rules may be commented out and that more
|
19
|
+
# imports may follow)
|
20
|
+
throw :done if imported_file && line =~ %r{/*}
|
21
|
+
throw :done if line =~ /^[\.\#a-zA-Z\:]/
|
22
|
+
end
|
23
|
+
|
24
|
+
def extension
|
25
|
+
".css"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Juicer
|
2
|
+
class DependencyResolver
|
3
|
+
include Enumerable
|
4
|
+
attr_reader :files
|
5
|
+
|
6
|
+
# Constructor
|
7
|
+
def initialize(options = {})
|
8
|
+
@files = []
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Resolve dependencies.
|
14
|
+
# This method accepts an optional block. The block will receive each
|
15
|
+
# file in succession. The file is included in the returned collection
|
16
|
+
# if the block is true for the given file. Without a block every found
|
17
|
+
# file is returned.
|
18
|
+
#
|
19
|
+
def resolve(file, &block)
|
20
|
+
@files = []
|
21
|
+
_resolve(file, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Yield files recursively. Resolve dependencies first, then call each, or
|
26
|
+
# any other enumerable methods.
|
27
|
+
#
|
28
|
+
def each(&block)
|
29
|
+
@files.each(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Resolves a path relative to another. If the path is absolute (ie it
|
34
|
+
# starts with a protocol or /) the <tt>:web_root</tt> options has to be
|
35
|
+
# set as well.
|
36
|
+
#
|
37
|
+
def resolve_path(path, reference)
|
38
|
+
# Absolute URL
|
39
|
+
if path =~ %r{^(/|[a-z]+:)}
|
40
|
+
if @options[:web_root].nil?
|
41
|
+
msg = "Cannot resolve absolute path '#{path}' without web root option"
|
42
|
+
raise ArgumentError.new(msg)
|
43
|
+
end
|
44
|
+
|
45
|
+
path.sub!(%r{^[a-z]+://[^/]+/}, '')
|
46
|
+
return File.expand_path(File.join(@options[:web_root], path))
|
47
|
+
end
|
48
|
+
|
49
|
+
File.expand_path(File.join(File.dirname(reference), path))
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def parse(line)
|
54
|
+
raise NotImplementedError.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def extension
|
58
|
+
raise NotImplementedError.new
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Carries out the actual work of resolve. resolve resets the internal
|
63
|
+
# file list and yields control to _resolve for rebuilding the file list.
|
64
|
+
#
|
65
|
+
def _resolve(file)
|
66
|
+
imported_path = nil
|
67
|
+
|
68
|
+
IO.foreach(file) do |line|
|
69
|
+
# Implementing subclasses may throw :done from the parse method when
|
70
|
+
# the file is exhausted for dependency declaration possibilities.
|
71
|
+
catch(:done) do
|
72
|
+
imported_path = parse(line, imported_path)
|
73
|
+
|
74
|
+
# If a dependency declaration was found
|
75
|
+
if imported_path
|
76
|
+
# Resolves a path relative to the file that imported it
|
77
|
+
imported_path = resolve_path(imported_path, file)
|
78
|
+
|
79
|
+
if File.directory?(imported_path)
|
80
|
+
imported_files = Dir.glob(File.join(imported_path, "**", "*#{extension}"))
|
81
|
+
else
|
82
|
+
imported_files = [imported_path]
|
83
|
+
end
|
84
|
+
|
85
|
+
imported_files.each do |imported_file|
|
86
|
+
# Only keep processing file if it's not already included.
|
87
|
+
# Yield to block to allow caller to ignore file
|
88
|
+
if !@files.include?(imported_file) && (!block_given? || yield(imported_file))
|
89
|
+
# Check this file for imports before adding it to get order right
|
90
|
+
_resolve(imported_file) { |f| f != File.expand_path(file) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
file = File.expand_path(file)
|
98
|
+
@files << file if !@files.include?(file) && (!block_given? || yield(file))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "juicer/dependency_resolver/dependency_resolver"
|
2
|
+
|
3
|
+
module Juicer
|
4
|
+
# Resolves @depends and @depend statements in comments in JavaScript files.
|
5
|
+
# Only the first comment in a JavaScript file is parsed
|
6
|
+
#
|
7
|
+
class JavaScriptDependencyResolver < DependencyResolver
|
8
|
+
@@depends_pattern = /\@depends?\s+([^\s\'\"\;]+)/
|
9
|
+
|
10
|
+
private
|
11
|
+
def parse(line, imported_file = nil)
|
12
|
+
return $1 if line =~ @@depends_pattern
|
13
|
+
|
14
|
+
# If we have already skimmed through some @depend/@depends or a
|
15
|
+
# closing comment we're done.
|
16
|
+
throw :done unless imported_file.nil? || !(line =~ /\*\//)
|
17
|
+
end
|
18
|
+
|
19
|
+
def extension
|
20
|
+
".js"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#
|
2
|
+
# Additions to core Ruby objects
|
3
|
+
#
|
4
|
+
|
5
|
+
class String
|
6
|
+
|
7
|
+
unless String.method_defined?(:camel_case)
|
8
|
+
#
|
9
|
+
# Turn an underscored string into camel case, ie this_becomes -> ThisBecomes
|
10
|
+
#
|
11
|
+
def camel_case
|
12
|
+
self.split("_").inject("") { |str, piece| str + piece.capitalize }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
unless String.method_defined?(:to_class)
|
17
|
+
#
|
18
|
+
# Treat a string as a class name and return the class. Optionally provide a
|
19
|
+
# module to look up the class in.
|
20
|
+
#
|
21
|
+
def to_class(mod = nil)
|
22
|
+
res = "#{mod}::#{self}".sub(/^::/, "").split("::").inject(Object) do |mod, obj|
|
23
|
+
raise "No such class/module" unless mod.const_defined?(obj)
|
24
|
+
mod = mod.const_get(obj)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
unless String.method_defined?(:classify)
|
30
|
+
#
|
31
|
+
# Turn a string in either underscore or camel case form into a class directly
|
32
|
+
#
|
33
|
+
def classify(mod = nil)
|
34
|
+
self.camel_case.to_class(mod)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
unless String.method_defined?(:underscore)
|
39
|
+
#
|
40
|
+
# Turn a camelcase string into underscore string
|
41
|
+
#
|
42
|
+
def underscore
|
43
|
+
self.split(/([A-Z][^A-Z]*)/).find_all { |str| str != "" }.join("_").downcase
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Symbol
|
2
|
+
#
|
3
|
+
# Converts symbol to string and calls String#camel_case
|
4
|
+
#
|
5
|
+
def camel_case
|
6
|
+
self.to_s.camel_case
|
7
|
+
end
|
8
|
+
|
9
|
+
#
|
10
|
+
# Converts symbol to string and calls String#classify
|
11
|
+
#
|
12
|
+
def classify(mod = nil)
|
13
|
+
self.to_s.classify(mod)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require "juicer/chainable"
|
2
|
+
require "juicer/cache_buster"
|
3
|
+
require "juicer/asset/path_resolver"
|
4
|
+
|
5
|
+
module Juicer
|
6
|
+
#
|
7
|
+
# The ImageEmbed is a tool that can parse a CSS file and substitute all
|
8
|
+
# referenced URLs by a data uri
|
9
|
+
#
|
10
|
+
# - data uri (http://en.wikipedia.org/wiki/Data_URI_scheme)
|
11
|
+
#
|
12
|
+
# Only local resources will be processed this way, external resources referenced
|
13
|
+
# by absolute urls will be left alone
|
14
|
+
#
|
15
|
+
class ImageEmbed
|
16
|
+
include Juicer::Chainable
|
17
|
+
|
18
|
+
# The maximum supported limit for modern browsers, See the Readme.rdoc for details
|
19
|
+
SIZE_LIMIT = 32768
|
20
|
+
|
21
|
+
#
|
22
|
+
# Returns the size limit
|
23
|
+
#
|
24
|
+
def size_limit
|
25
|
+
SIZE_LIMIT
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(options = {})
|
29
|
+
@web_root = options[:web_root]
|
30
|
+
@web_root.sub!(%r{/?$}, "") if @web_root # Remove trailing slash
|
31
|
+
@type = options[:type] || :none
|
32
|
+
@contents = nil
|
33
|
+
@hosts = options[:hosts]
|
34
|
+
@path_resolver = Juicer::Asset::PathResolver.new(:document_root => options[:web_root],
|
35
|
+
:hosts => options[:hosts])
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Update file. If no +output+ is provided, the input file is overwritten
|
40
|
+
#
|
41
|
+
def save(file, output = nil)
|
42
|
+
return unless @type == :data_uri
|
43
|
+
|
44
|
+
output_file = output || file
|
45
|
+
@contents = File.read(file)
|
46
|
+
used = []
|
47
|
+
|
48
|
+
@path_resolver = Juicer::Asset::PathResolver.new(:document_root => @web_root,
|
49
|
+
:hosts => @hosts,
|
50
|
+
:base => File.dirname(file))
|
51
|
+
|
52
|
+
assets = urls(file)
|
53
|
+
|
54
|
+
# TODO: Remove "?embed=true" from duplicate urls
|
55
|
+
duplicates = duplicate_urls(assets)
|
56
|
+
|
57
|
+
if duplicates.length > 0
|
58
|
+
Juicer::LOGGER.warn("Duplicate image urls detected, these images will not be embedded: #{duplicates.collect { |v| v.gsub('?embed=true', '') }.inspect}")
|
59
|
+
end
|
60
|
+
|
61
|
+
assets.each do |asset|
|
62
|
+
begin
|
63
|
+
next if used.include?(asset) || duplicates.include?(asset.path)
|
64
|
+
used << asset
|
65
|
+
|
66
|
+
# make sure we do not exceed SIZE_LIMIT
|
67
|
+
new_path = embed_data_uri(asset.filename)
|
68
|
+
|
69
|
+
if new_path.length < SIZE_LIMIT
|
70
|
+
# replace the url in the css file with the data uri
|
71
|
+
@contents.gsub!(asset.path, embed_data_uri(asset.path))
|
72
|
+
else
|
73
|
+
Juicer::LOGGER.warn("The final data uri for the image located at #{asset.path.gsub('?embed=true', '')} exceeds #{SIZE_LIMIT} and will not be embedded to maintain compatability.")
|
74
|
+
end
|
75
|
+
rescue Errno::ENOENT
|
76
|
+
puts "Unable to locate file #{asset.path}, skipping image embedding"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
File.open(output || file, "w") { |f| f.puts @contents }
|
81
|
+
@contents = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
chain_method :save
|
85
|
+
|
86
|
+
def embed_data_uri( path )
|
87
|
+
new_path = path
|
88
|
+
|
89
|
+
if path.match( /\?embed=true$/ )
|
90
|
+
supported_file_matches = path.match( /(?:\.)(png|gif|jpg|jpeg)(?:\?embed=true)$/i )
|
91
|
+
filetype = supported_file_matches[1] if supported_file_matches
|
92
|
+
|
93
|
+
if ( filetype )
|
94
|
+
filename = path.gsub('?embed=true','')
|
95
|
+
|
96
|
+
# check if file exists, throw an error if it doesn't exist
|
97
|
+
if File.exist?( filename )
|
98
|
+
|
99
|
+
# read contents of file into memory
|
100
|
+
content = File.read( filename )
|
101
|
+
content_type = "image/#{filetype}"
|
102
|
+
|
103
|
+
# encode the url
|
104
|
+
new_path = Datafy::make_data_uri( content, content_type )
|
105
|
+
else
|
106
|
+
puts "Unable to locate file #{filename} on local file system, skipping image embedding"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
return new_path
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Returns all referenced URLs in +file+.
|
115
|
+
#
|
116
|
+
def urls(file)
|
117
|
+
@contents = File.read(file) unless @contents
|
118
|
+
|
119
|
+
@contents.scan(/url\([\s"']*([^\)"'\s]*)[\s"']*\)/m).collect do |match|
|
120
|
+
@path_resolver.resolve(match.first)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
def duplicate_urls(urls)
|
126
|
+
urls.inject({}) { |h,v| h[v.path] = h[v.path].to_i+1; h }.reject{ |k,v| v == 1 }.keys
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# http://snippets.dzone.com/posts/show/3838
|
132
|
+
#module Enumerable
|
133
|
+
# def duplicates
|
134
|
+
# inject({}) {|h,v| h[v]=h[v].to_i+1; h}.reject{|k,v| v==1}.keys
|
135
|
+
# end
|
136
|
+
#end
|