web_resource_bundler 0.0.13

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