web_resource_bundler 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.bundle/config +2 -0
  2. data/.gitignore +21 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +10 -0
  5. data/README +118 -0
  6. data/Rakefile +29 -0
  7. data/VERSION +1 -0
  8. data/lib/web_resource_bundler.rb +195 -0
  9. data/lib/web_resource_bundler/content_management/block_data.rb +57 -0
  10. data/lib/web_resource_bundler/content_management/block_parser.rb +63 -0
  11. data/lib/web_resource_bundler/content_management/css_url_rewriter.rb +23 -0
  12. data/lib/web_resource_bundler/content_management/resource_file.rb +35 -0
  13. data/lib/web_resource_bundler/exceptions.rb +26 -0
  14. data/lib/web_resource_bundler/file_manager.rb +30 -0
  15. data/lib/web_resource_bundler/filters.rb +9 -0
  16. data/lib/web_resource_bundler/filters/base_filter.rb +28 -0
  17. data/lib/web_resource_bundler/filters/bundle_filter.rb +58 -0
  18. data/lib/web_resource_bundler/filters/bundle_filter/resource_packager.rb +49 -0
  19. data/lib/web_resource_bundler/filters/cdn_filter.rb +48 -0
  20. data/lib/web_resource_bundler/filters/image_encode_filter.rb +56 -0
  21. data/lib/web_resource_bundler/filters/image_encode_filter/css_generator.rb +85 -0
  22. data/lib/web_resource_bundler/filters/image_encode_filter/image_data.rb +51 -0
  23. data/lib/web_resource_bundler/rails_app_helpers.rb +65 -0
  24. data/lib/web_resource_bundler/settings.rb +46 -0
  25. data/lib/web_resource_bundler/web_resource_bundler_init.rb +17 -0
  26. data/spec/public/foo.css +4 -0
  27. data/spec/public/images/good.jpg +0 -0
  28. data/spec/public/images/logo.jpg +0 -0
  29. data/spec/public/images/sdfo.jpg +0 -0
  30. data/spec/public/images/too_big_image.jpg +0 -0
  31. data/spec/public/marketing.js +14 -0
  32. data/spec/public/salog20.js +6 -0
  33. data/spec/public/sample.css +6 -0
  34. data/spec/public/seal.js +10 -0
  35. data/spec/public/set_cookies.js +8 -0
  36. data/spec/public/styles/boo.css +4 -0
  37. data/spec/public/styles/for_import.css +7 -0
  38. data/spec/public/temp.css +1 -0
  39. data/spec/public/test.css +2 -0
  40. data/spec/sample_block_helper.rb +81 -0
  41. data/spec/spec_helper.rb +82 -0
  42. data/spec/web_resource_bundler/content_management/block_data_spec.rb +33 -0
  43. data/spec/web_resource_bundler/content_management/block_parser_spec.rb +100 -0
  44. data/spec/web_resource_bundler/content_management/css_url_rewriter_spec.rb +27 -0
  45. data/spec/web_resource_bundler/content_management/resource_file_spec.rb +37 -0
  46. data/spec/web_resource_bundler/file_manager_spec.rb +60 -0
  47. data/spec/web_resource_bundler/filters/bundle_filter/filter_spec.rb +40 -0
  48. data/spec/web_resource_bundler/filters/bundle_filter/resource_packager_spec.rb +41 -0
  49. data/spec/web_resource_bundler/filters/cdn_filter_spec.rb +76 -0
  50. data/spec/web_resource_bundler/filters/image_encode_filter/css_generator_spec.rb +104 -0
  51. data/spec/web_resource_bundler/filters/image_encode_filter/filter_spec.rb +73 -0
  52. data/spec/web_resource_bundler/filters/image_encode_filter/image_data_spec.rb +53 -0
  53. data/spec/web_resource_bundler/settings_spec.rb +45 -0
  54. data/spec/web_resource_bundler/web_resource_bundler_spec.rb +90 -0
  55. data/web_resource_bundler.gemspec +111 -0
  56. metadata +146 -0
@@ -0,0 +1,63 @@
1
+ module WebResourceBundler
2
+ class BlockParser
3
+ CONDITIONAL_BLOCK_PATTERN = /<!--\s*\[\s*if[^>]*IE\s*\d*[^>]*\]\s*>(.*?)<!\s*\[\s*endif\s*\]\s*-->/xmi
4
+ CONDITION_PATTERN = /<!--\s*(\[[^<]*\])\s*>/
5
+ LINK_PATTERN = /(<(link|script[^>]*?src\s*=).*?(><\/script>|>))/
6
+ URL_PATTERN = /(href|src) *= *["']([^"'?]+)/i
7
+
8
+ #parsing block content recursively
9
+ #nested comments NOT supported
10
+ #result is BlockData with conditional blocks in child_blocks
11
+ def parse_block_with_childs(block, condition)
12
+ block_data = BlockData.new(condition)
13
+ block.gsub!(CONDITIONAL_BLOCK_PATTERN) do |s|
14
+ new_block = CONDITIONAL_BLOCK_PATTERN.match(s)[1]
15
+ new_condition = CONDITION_PATTERN.match(s)[1]
16
+ block_data.child_blocks << parse_block_with_childs(new_block, new_condition)
17
+ s = ""
18
+ end
19
+ files = find_files(block)
20
+ block_data.files = files
21
+ block_data.inline_block = remove_links(block)
22
+ block_data
23
+ end
24
+
25
+ #removing resource links from block
26
+ #example: "<link href="bla"><script src="bla"></script>my inline content" => "my inline content"
27
+ def remove_links(block)
28
+ inline_block = block.gsub(LINK_PATTERN) do |s|
29
+ extension = File.extname(URL_PATTERN.match(s)[2])
30
+ if /\.js|\.css/.match(extension) and not s.include?('://')
31
+ #we should delete link to local css or js resource
32
+ ''
33
+ else
34
+ #link to remote resource should be kept
35
+ s
36
+ end
37
+ end
38
+ return inline_block
39
+ end
40
+
41
+ #looking for css and js files included and create BlockFiles with files paths
42
+ def find_files(block)
43
+ files = []
44
+ block.scan(URL_PATTERN).each do |property, value|
45
+ unless value.include?('://')
46
+ case property
47
+ when "src"
48
+ then files << WebResourceBundler::ResourceFile.new(WebResourceBundler::ResourceFileType::JS, value) if File.extname(value) == '.js'
49
+ when "href"
50
+ then files << WebResourceBundler::ResourceFile.new(WebResourceBundler::ResourceFileType::CSS, value) if File.extname(value) == '.css'
51
+ end
52
+ end
53
+ end
54
+ files
55
+ end
56
+
57
+ #just a short method to start parsing passed block
58
+ def parse(block)
59
+ parse_block_with_childs(block, "")
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,23 @@
1
+ class WebResourceBundler::CssUrlRewriter
2
+ class << self
3
+ # rewrites a relative path to an absolute path, removing excess "../" and "./"
4
+ # rewrite_relative_path("stylesheets/default/global.css", "../image.gif") => "/stylesheets/image.gif"
5
+
6
+ def rewrite_relative_path(source_url, relative_url)
7
+ return relative_url if relative_url.include?('http://')
8
+ File.expand_path(relative_url, File.dirname(source_url))
9
+ end
10
+
11
+ # rewrite the URL reference paths
12
+ # url(../../../images/active_scaffold/default/add.gif);
13
+ # url(/stylesheets/active_scaffold/default/../../../images/active_scaffold/default/add.gif);
14
+ # url(/stylesheets/active_scaffold/../../images/active_scaffold/default/add.gif);
15
+ # url(/stylesheets/../images/active_scaffold/default/add.gif);
16
+ # url('/images/active_scaffold/default/add.gif');
17
+ def rewrite_content_urls!(filename, content)
18
+ content.gsub!(/url\s*\(['|"]?([^\)'"]+)['|"]?\)/) { "url('#{rewrite_relative_path(filename, $1)}')" }
19
+ content
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ module WebResourceBundler
2
+
3
+ class ResourceFileType
4
+ CSS = {:value => 1, :name => 'style', :ext => 'css'}
5
+ JS = {:value => 2, :name => 'script', :ext => 'js'}
6
+ IE_CSS = {:value => 3, :name => 'style', :ext => 'css'}
7
+ MHTML = {:value => 4, :name => 'style', :ext => 'mhtml'}
8
+ end
9
+
10
+ class ResourceFile
11
+ attr_reader :type
12
+ attr_accessor :path, :content
13
+ def initialize(type, path, content = "")
14
+ @type = type
15
+ @content = content
16
+ @path = path
17
+ end
18
+ def self.new_js_file(path, content = "")
19
+ ResourceFile.new(ResourceFileType::JS, path, content)
20
+ end
21
+ def self.new_css_file(path, content = "")
22
+ ResourceFile.new(ResourceFileType::CSS, path, content)
23
+ end
24
+ def self.new_ie_css_file(path, content ="")
25
+ ResourceFile.new(ResourceFileType::IE_CSS, path, content)
26
+ end
27
+ def self.new_mhtml_file(path, content = "")
28
+ ResourceFile.new(ResourceFileType::MHTML, path, content)
29
+ end
30
+ def clone
31
+ ResourceFile.new(self.type.dup, self.path.dup, self.content.dup)
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,26 @@
1
+ module WebResourceBundler::Exceptions
2
+ class WebResourceBundlerError < Exception
3
+
4
+ def initialize(message = "Unknown error occured")
5
+ @message = message
6
+ end
7
+
8
+ def to_s
9
+ @message
10
+ end
11
+
12
+ end
13
+
14
+ class ResourceNotFoundError < WebResourceBundlerError
15
+ def initialize(path)
16
+ super "Resource #{path} not found"
17
+ end
18
+ end
19
+
20
+ class NonExistentCssImage < WebResourceBundlerError
21
+ def initialize(image_path)
22
+ super "Css has url to incorrect image path: #{image_path}"
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,30 @@
1
+ module WebResourceBundler
2
+ class FileManager
3
+ attr_accessor :resource_dir, :cache_dir
4
+
5
+ def initialize(resource_dir, cache_dir)
6
+ @resource_dir, @cache_dir = resource_dir, cache_dir
7
+ end
8
+
9
+ def full_path(relative_path)
10
+ File.join(@resource_dir, relative_path)
11
+ end
12
+
13
+ def exist?(relative_path)
14
+ File.exist? full_path(relative_path)
15
+ end
16
+
17
+ def get_content(relative_path)
18
+ raise Exceptions::ResourceNotFoundError.new(full_path(relative_path)) unless exist?(relative_path)
19
+ File.read(full_path(relative_path))
20
+ end
21
+
22
+ def create_cache_dir
23
+ path = File.join(@resource_dir, @cache_dir)
24
+ unless File.exist?(path)
25
+ Dir.mkdir(path)
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ require 'filters/base_filter'
2
+ require 'filters/image_encode_filter'
3
+ require 'filters/bundle_filter'
4
+ require 'filters/cdn_filter'
5
+
6
+ module WebResourceBundler
7
+ module Filters
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ module WebResourceBundler
2
+ module Filters
3
+ #virtaul base class for filters
4
+ class BaseFilter
5
+ attr_reader :settings
6
+
7
+ def initialize(settings, file_manager)
8
+ @settings = Settings.new(settings)
9
+ @file_manager = file_manager
10
+ end
11
+
12
+ def set_settings(settings)
13
+ @settings.set(settings)
14
+ end
15
+
16
+ def apply(block_data = nil)
17
+ #applies filter to block_data
18
+ end
19
+
20
+ #resource is hash {:css => ResourceBundle::Data, :js => ResourceBundle::Data}
21
+ def change_resulted_files!(resource = nil)
22
+ #this method changes resource file names to resulted files paths
23
+ #used to determine if resulted files exist on disk
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,58 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), "/bundle_filter")
2
+ require 'bundle_filter/resource_packager'
3
+ require 'base_filter'
4
+ require 'digest/md5'
5
+ module WebResourceBundler::Filters::BundleFilter
6
+ class Filter < WebResourceBundler::Filters::BaseFilter
7
+
8
+ def initialize(settings, file_manager)
9
+ super(settings, file_manager)
10
+ @packager = ResourcePackager.new(@settings, @file_manager)
11
+ end
12
+
13
+ def apply!(block_data)
14
+ new_files = []
15
+ unless block_data.styles.empty?
16
+ new_css_filename = css_bundle_filepath(block_data.styles)
17
+ new_css_content = @packager.bundle_files(block_data.styles)
18
+ new_css_file = WebResourceBundler::ResourceFile.new_css_file(new_css_filename, new_css_content)
19
+ new_files << new_css_file
20
+ end
21
+ unless block_data.scripts.empty?
22
+ new_js_filename = js_bundle_filepath(block_data.scripts)
23
+ new_js_content = @packager.bundle_files(block_data.scripts)
24
+ new_js_file = WebResourceBundler::ResourceFile.new_js_file(new_js_filename, new_js_content)
25
+ new_files << new_js_file
26
+ end
27
+ block_data.files = new_files
28
+ block_data
29
+ end
30
+
31
+ def get_md5(files)
32
+ items = [(files.map {|f| f.path }).sort]
33
+ items += @settings.md5_additional_data if @settings.md5_additional_data
34
+ Digest::MD5.hexdigest(items.flatten.join('|'))
35
+ end
36
+
37
+ def bundle_filepath(type, files)
38
+ unless files.empty?
39
+ items = [type[:name] + '_' + get_md5(files)]
40
+ items += @settings.filename_additional_data if @settings.filename_additional_data
41
+ items << type[:ext]
42
+ return File.join(@settings.cache_dir, items.join('.'))
43
+ else
44
+ return nil
45
+ end
46
+ end
47
+
48
+ #just aliases to simplify code
49
+ def css_bundle_filepath(files)
50
+ bundle_filepath(WebResourceBundler::ResourceFileType::CSS, files)
51
+ end
52
+
53
+ def js_bundle_filepath(files)
54
+ bundle_filepath(WebResourceBundler::ResourceFileType::JS, files)
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ module WebResourceBundler::Filters::BundleFilter
2
+ class ResourcePackager
3
+ IMPORT_PTR = /\@import ['|"](.*?)['|"];/
4
+
5
+ def initialize(settings, file_manager)
6
+ @settings = settings
7
+ @file_manager = file_manager
8
+ end
9
+
10
+ #recursively iterates through all files and imported files
11
+ def bundle_files(files)
12
+ output = ""
13
+ files.select{|f| not f.content.empty? }.each do |file|
14
+ path = file.path
15
+ content = file.content
16
+ output << "/* --------- #{path} --------- */\n"
17
+ if file.type[:ext] == 'css'
18
+ imported_files = extract_imported_files!(content, path)
19
+ #getting imported (@import ...) files contents
20
+ imported_resource_files = []
21
+ imported_files.each do |imported_file|
22
+ imported_resource_files << WebResourceBundler::ResourceFile.new_css_file(imported_file, @file_manager.get_content(imported_file))
23
+ end
24
+ #bundling imported files
25
+ output << bundle_files(imported_resource_files) unless imported_resource_files.empty?
26
+ end
27
+ #adding ';' symbol in case javascript developer forget to do this
28
+ content << ';' if File.extname(path) == '.js'
29
+ output << content
30
+ output << "\n/* --------- END #{path} --------- */\n"
31
+ end
32
+ output
33
+ end
34
+
35
+ #finds all imported files in css
36
+ def extract_imported_files!(content, base_file_path)
37
+ imported_files = []
38
+ content.gsub!(IMPORT_PTR) do |result|
39
+ imported_file = IMPORT_PTR.match(result)[1]
40
+ if imported_file
41
+ imported_files << File.join(File.dirname(base_file_path), imported_file)
42
+ end
43
+ result = ""
44
+ end
45
+ return imported_files
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ module WebResourceBundler::Filters::CdnFilter
2
+ class Filter < WebResourceBundler::Filters::BaseFilter
3
+ def initialize(settings, file_manager)
4
+ super(settings, file_manager)
5
+ end
6
+
7
+ def apply!(block_data)
8
+ block_data.styles.each do |file|
9
+ rewrite_content_urls!(file.path, file.content) unless file.content.empty?
10
+ file.path = new_filepath(file.path)
11
+ end
12
+ block_data
13
+ end
14
+
15
+ def new_filepath(path)
16
+ File.join(@settings.cache_dir, 'cdn_' + File.basename(path))
17
+ end
18
+
19
+ #insures that image linked to one particular host
20
+ def host_for_image(image_url)
21
+ #hosts are different depending on protocol
22
+ if @settings.protocol == 'https'
23
+ hosts = @settings.https_hosts
24
+ else
25
+ hosts = @settings.http_hosts
26
+ end
27
+ #getting host based on image url hash
28
+ host_index = image_url.hash % hosts.size
29
+ hosts[host_index]
30
+ end
31
+
32
+ def rewrite_content_urls!(file_path, content)
33
+ content.gsub!(/url\s*\(['|"]?([^\)'"]+)['|"]?\)/) do |s|
34
+ matched_url = $1
35
+ #we shouldn't change url value for base64 encoded images
36
+ if not (/base64/.match(s) or /mhtml/.match(s)) and matched_url.match(/\.(jpg|gif|png|jpeg|bmp)/)
37
+ #using CssUrlRewriter method to get image url
38
+ url = WebResourceBundler::CssUrlRewriter.rewrite_relative_path(file_path, matched_url)
39
+ host = host_for_image(url)
40
+ s = "url('#{File.join(host, url)}')"
41
+ else
42
+ s
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,56 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ require 'image_encode_filter/image_data'
3
+ require 'image_encode_filter/css_generator'
4
+ module WebResourceBundler::Filters::ImageEncodeFilter
5
+ class Filter < WebResourceBundler::Filters::BaseFilter
6
+ FILE_PREFIX = 'base64_'
7
+ IE_FILE_PREFIX = 'base64_ie_'
8
+ MHTML_FILE_PREFIX = 'mhtml_'
9
+ def initialize(settings, file_manager)
10
+ super settings, file_manager
11
+ @generator = CssGenerator.new(@settings, @file_manager)
12
+ end
13
+
14
+ def set_settings(settings)
15
+ super settings
16
+ @generator.set_settings(settings)
17
+ end
18
+
19
+ def apply!(block_data)
20
+ added_files = []
21
+ block_data.styles.each do |file|
22
+ #creating new css file with content for IE
23
+ ie_css_file = WebResourceBundler::ResourceFile.new_ie_css_file(encoded_filepath_for_ie(file.path), file.content.dup)
24
+ #creating new mhtml file with images encoded in base64
25
+ mhtml_file = WebResourceBundler::ResourceFile.new_mhtml_file(mhtml_filepath(file.path), "")
26
+ file.path = encoded_filepath(file.path)
27
+ unless file.content.empty?
28
+ @generator.encode_images!(file.content)
29
+ #getting images to construct mhtml file
30
+ images = @generator.encode_images_for_ie!(ie_css_file.content, mhtml_file.path)
31
+ mhtml_file.content = @generator.construct_mhtml_content(images)
32
+ end
33
+ added_files << ie_css_file
34
+ added_files << mhtml_file
35
+ end
36
+ block_data.files += added_files
37
+ block_data
38
+ end
39
+
40
+ #path of a new file with images encoded
41
+ def encoded_filepath(base_file_path)
42
+ File.join(@settings.cache_dir, FILE_PREFIX + File.basename(base_file_path))
43
+ end
44
+
45
+ #path of a new file for IE with images encoded
46
+ def encoded_filepath_for_ie(base_file_path)
47
+ File.join(@settings.cache_dir, IE_FILE_PREFIX + File.basename(base_file_path))
48
+ end
49
+
50
+ #filepath of mhtml file for IE
51
+ def mhtml_filepath(base_file_path)
52
+ File.join(@settings.cache_dir, MHTML_FILE_PREFIX + File.basename(base_file_path))
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,85 @@
1
+ module WebResourceBundler
2
+ module Filters
3
+ module ImageEncodeFilter
4
+ class CssGenerator
5
+ TAGS = ['background-image', 'background']
6
+ SEPARATOR = 'A_SEPARATOR'
7
+ PATTERN = /((#{TAGS.join('|')})\s*:[^\(]*)url\(\s*['|"]([^\)]*)['|"]\s*\)/
8
+
9
+
10
+ def initialize(settings, file_manager)
11
+ @settings = settings
12
+ @file_manager = file_manager
13
+ end
14
+
15
+ def set_settings(settings)
16
+ @settings.set(settings)
17
+ end
18
+
19
+ #construct mhtml head of css file with definition of image data in base64
20
+ def construct_mhtml_content(images)
21
+ result = ""
22
+ unless images.empty?
23
+ result << "/* \n"
24
+ result << 'Content-Type: multipart/related; boundary="' << SEPARATOR << '"' << "\n\n"
25
+ #each image found in css should be defined in header with base64 encoded content
26
+ images.each_key do |key|
27
+ result += images[key].construct_mhtml_image_data('--' + SEPARATOR)
28
+ end
29
+ result << "\n" << '--' << SEPARATOR << '--' << "\n"
30
+ result << "*/"
31
+ end
32
+ result
33
+ end
34
+
35
+ #creates mhtml link to use in css tags instead of image url
36
+ def construct_mhtml_link(filepath)
37
+ "#{@settings.protocol}://#{File.join(@settings.domain, filepath)}"
38
+ end
39
+
40
+ #iterates through all tags found in css
41
+ #if image exist and has proper size - it should be encoded
42
+ #each tag with this kind of an image is replaced with new one (mhtml link for IE and base64 code for another browser
43
+ #returns images hash - in case generator can build proper IE css header with base64 images encoded
44
+ def encode_images_basic!(content)
45
+ images = {}
46
+ new_content = content.gsub!(PATTERN) do |s|
47
+ tag, url = $1, $3
48
+ #this constructor will write in log if image doesn't exist
49
+ data = ImageData.new(url, @settings.resource_dir)
50
+ if !url.empty? and data.exist and data.size <= @settings.max_image_size and block_given?
51
+ #using image url as key to prevent one image be encoded many times
52
+ images[data.url] = data unless images[data.path]
53
+ #changing string using provided block
54
+ s = yield(data, tag) if block_given?
55
+ else
56
+ #returning the same string because user failed with image path - such image non existent
57
+ s
58
+ end
59
+ end
60
+ images
61
+ end
62
+
63
+ #generates css file for IE with encoded images using mhtml in cache dir
64
+ #mhtml_filepath - path to file with images encoded in base64
65
+ def encode_images_for_ie!(content, mhtml_filepath)
66
+ #creating new css content with images encoded in base64
67
+ images = encode_images_basic!(content) do |image_data, tag|
68
+ "*#{tag}url(mhtml:#{construct_mhtml_link(mhtml_filepath)}!#{image_data.id})"
69
+ end
70
+ images
71
+ end
72
+
73
+ #generates css file with encoded images in cache dir
74
+ def encode_images!(content)
75
+ #encoding images in content
76
+ images = encode_images_basic!(content) do |image_data, tag|
77
+ "#{tag}url('data:image/#{image_data.extension};base64,#{image_data.encoded}')"
78
+ end
79
+ images
80
+ end
81
+
82
+ end
83
+ end
84
+ end
85
+ end