nanoc-cachebuster 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format documentation --color -r spec/spec_helper.rb
data/HISTORY.md ADDED
@@ -0,0 +1,5 @@
1
+ # 0.0.1
2
+
3
+ * No more re-calculation of fingerprints, just use the routed filename.
4
+ * Refactored into separate strategies.
5
+ * First, direct extraction.
data/LICENSE ADDED
@@ -0,0 +1 @@
1
+
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ A simple Ruby gem that enhances Nanoc with cache-busting capabilities.
2
+
3
+ Description
4
+ ===========
5
+
6
+ Your website should use far-future expires headers on static assets, to make
7
+ the best use of client-side caching. But when a file is cached, updates won't
8
+ get picked up. Cache busting is the practice of making the filename of a
9
+ cached asset unique to its content, so it can be cached without having to
10
+ worry about future changes.
11
+
12
+ This gem adds a filter and some helper methods to Nanoc, the static site
13
+ generator, to simplify the process of making asset filenames unique. It helps
14
+ you output fingerprinted filenames, and refer to them from your source files.
15
+
16
+ More information
17
+ ----------------
18
+
19
+ Find out more about Nanoc by Denis Defreyne at http://nanoc.stoneship.org.
20
+
21
+ Installation
22
+ ============
23
+
24
+ As an extension to Nanoc, you need to have that installed and working before
25
+ you can add this gem. When your Nanoc project is up and running, simply
26
+ install this gem:
27
+
28
+ $ gem install nanoc-cachebuster
29
+
30
+ Then load it via your project Gemfile or in `./lib/default.rb`:
31
+
32
+ require 'nanoc3/cachebuster'
33
+
34
+ Usage
35
+ =====
36
+
37
+ This gem provides a Nanoc filter you can use to rewrite references to static
38
+ assets in your source files. These will be picked up automatically.
39
+
40
+ So, when you include a stylesheet:
41
+
42
+ <link rel="stylesheet" href="styles.css">
43
+
44
+ This filter will change that on compilation to:
45
+
46
+ <link rel="stylesheet" href="styles-cb7a4bb98ef.css">
47
+
48
+ The adjusted filename changes every time the file itself changes, so you don't
49
+ want to code that by hand in your Rules file. Instead, use the helper methods
50
+ provided. First, include the helpers in your ./lib/default.rb:
51
+
52
+ include Nanoc3::Helpers::CacheBusting
53
+
54
+ Then you can use `#should_cachebust?` and `#cachebusting_hash` in your routing
55
+ rules to determine whether an item needs cachebusting, and get the fingerprint
56
+ for it. So you can do something like:
57
+
58
+ route 'styles' do
59
+ fp = cachebust?(item) ? fingerprint(item[:filename]) : ''
60
+ item.identifier.chop + fp + '.' + item[:extension]
61
+ end
62
+
63
+ Development
64
+ ===========
65
+
66
+ Changes
67
+ -------
68
+
69
+ See HISTORY.md for the full changelog.
70
+
71
+ Dependencies
72
+ ------------
73
+
74
+ nanoc-cachebuster obviously depends on Nanoc, but has no further dependencies.
75
+ To test it you will need Rspec.
76
+
77
+ Credits
78
+ =======
79
+
80
+ * **Author**: Arjan van der Gaag <arjan@arjanvandergaag.nl>
81
+ * **License**: MIT License (same as Ruby, see LICENSE)
data/Rakefile ADDED
@@ -0,0 +1,94 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
2
+ require 'nanoc3/cachebuster/version'
3
+
4
+ task :build do
5
+ sh 'gem build nanoc-cachebuster.gemspec'
6
+ end
7
+
8
+ desc 'Install the gem locally'
9
+ task :install => :build do
10
+ sh "gem install nanoc-cachebuster-#{Nanoc3::Cachebuster::VERSION}.gem"
11
+ end
12
+
13
+ task :tag do
14
+ sh "git tag -a #{Nanoc3::Cachebuster::VERSION}"
15
+ end
16
+
17
+ task :push do
18
+ sh 'git push origin master'
19
+ sh 'git push --tags'
20
+ end
21
+
22
+ task :log do
23
+ changes = `git log --oneline $(git describe --abbrev=0 2>/dev/null)..HEAD`
24
+ abort 'Nothing to do' if changes.empty?
25
+
26
+ changes.gsub!(/^\w+/, '*')
27
+ path = File.expand_path('../HISTORY.md', __FILE__)
28
+
29
+ original_content = File.read(path)
30
+ addition = "# #{Nanoc3::Cachebuster::VERSION}\n\n#{changes}"
31
+ puts addition
32
+
33
+ File.open(path, 'w') do |f|
34
+ f.write "#{addition}\n#{original_content}"
35
+ end
36
+ end
37
+
38
+ desc 'Tag the code, push upstream, build and push the gem'
39
+ task :release => [:install, :push] do
40
+ sh "gem push nanoc-cachebuster-#{Nanoc3::Cachebuster::VERSION}"
41
+ end
42
+
43
+ desc 'Print current version number'
44
+ task :version do
45
+ puts Nanoc3::Cachebuster::VERSION
46
+ end
47
+
48
+ class Version
49
+ def initialize(version_string)
50
+ @major, @minor, @patch = version_string.split('.').map { |s| s.to_i }
51
+ end
52
+
53
+ def bump(part)
54
+ case part
55
+ when :major then @major, @minor, @patch = @major + 1, 0, 0
56
+ when :minor then @minor, @patch = @minor + 1, 0
57
+ when :patch then @patch += 1
58
+ end
59
+ self
60
+ end
61
+
62
+ def to_s
63
+ [@major, @minor, @patch].join('.')
64
+ end
65
+
66
+ def write
67
+ file = File.expand_path('../lib/nanoc3/cachebuster/version.rb', __FILE__)
68
+ original_contents = File.read(file)
69
+ File.open(file, 'w') do |f|
70
+ f.write original_contents.gsub(/VERSION = ('|")\d+\.\d+\.\d+\1/, "VERSION = '#{to_s}'")
71
+ end
72
+ puts to_s
73
+ to_s
74
+ end
75
+ end
76
+
77
+ namespace :version do
78
+ namespace :bump do
79
+ desc 'Bump a major version'
80
+ task :major do
81
+ Version.new(Nanoc3::Cachebuster::VERSION).bump(:major).write
82
+ end
83
+
84
+ desc 'Bump a minor version'
85
+ task :minor do
86
+ Version.new(Nanoc3::Cachebuster::VERSION).bump(:minor).write
87
+ end
88
+
89
+ desc 'Bump a patch version'
90
+ task :patch do
91
+ Version.new(Nanoc3::Cachebuster::VERSION).bump(:patch).write
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,148 @@
1
+ module Nanoc3
2
+ module Cachebuster
3
+ # The Strategy is a way to deal with an input file. The Cache busting filter
4
+ # will use a strategy to process all references. You may want to use different
5
+ # strategies for different file types.
6
+ #
7
+ # @abstract
8
+ class Strategy
9
+
10
+ @subclasses = {}
11
+
12
+ def self.inherited(subclass)
13
+ @subclasses[subclass.to_s.split('::').last.downcase.to_sym] = subclass
14
+ end
15
+
16
+ def self.for(kind, site, item)
17
+ klass = @subclasses[kind]
18
+ raise Nanoc3::Cachebuster::NoSuchStrategy.new "No strategy found for #{kind}" unless klass
19
+ klass.new(site, item)
20
+ end
21
+
22
+ # The current site. We need a reference to that in a strategy,
23
+ # so we can browse through all its items.
24
+ #
25
+ # This might very well have been just the site#items array, but for
26
+ # future portability we might as well carry the entire site object
27
+ # over.
28
+ #
29
+ # @return <Nanoc3::Site>
30
+ attr_reader :site
31
+
32
+ # The Nanoc item we are currently filtering.
33
+ #
34
+ # @return <Nanoc3::Item>
35
+ attr_reader :current_item
36
+
37
+ def initialize(site, current_item)
38
+ @site, @current_item = site, current_item
39
+ end
40
+
41
+ # Abstract method that subclasses (actual strategies) should
42
+ # implement.
43
+ #
44
+ # @abstract
45
+ def apply
46
+ raise Exception, 'Must be implemented in a subclass'
47
+ end
48
+
49
+ protected
50
+
51
+ # Try to find the source path of a referenced file.
52
+ #
53
+ # This will use Nanoc's routing rules to try and find an item whose output
54
+ # path matches the path given, which is a source reference. It returns
55
+ # the path to the content file if a match is found.
56
+ #
57
+ # As an example, when we use the input file "assets/styles.scss" for our
58
+ # stylesheet, then we refer to that file in our HTML as "assets/styles.css".
59
+ # Given the output filename, this method will return the input filename.
60
+ #
61
+ # @raises NoSuchSourceFile when no match is found
62
+ # @param <String> path is the reference to an asset file from another source
63
+ # file, such as '/assets/styles.css'
64
+ # @return <String> the path to the content file for the referenced file,
65
+ # such as '/assets/styles.scss'
66
+ def output_filename(input_path)
67
+ path = absolutize(input_path)
68
+
69
+ matching_item = site.items.find do |i|
70
+ next unless i.path # some items don't have an output path. Ignore those.
71
+ i.path.sub(/#{Nanoc3::Cachebuster::CACHEBUSTER_PREFIX}[a-zA-Z0-9]{9}(?=\.)/o, '') == path
72
+ end
73
+
74
+ # Raise an exception to indicate we should leave this reference alone
75
+ unless matching_item
76
+ raise Nanoc3::Cachebuster::NoSuchSourceFile, 'No source file found matching ' + input_path
77
+ end
78
+
79
+ # Make sure to keep or remove the first slash, as the input path
80
+ # does.
81
+ matching_item.path.tap do |p|
82
+ p.sub!(/^\//, '') unless input_path =~ /^\//
83
+ end
84
+ end
85
+
86
+ # Get the absolute path to a file, whereby absolute means relative to the root.
87
+ #
88
+ # We use the relative-to-root path to detect if our site contains an item
89
+ # that will be output to that location.
90
+ #
91
+ # @example Using an absolute input path in 'assets/styles.css'
92
+ # absolutize('/assets/logo.png') # => '/assets/logo.png'
93
+ # @example Using a relative input path in 'assets/styles.css'
94
+ # absolutize('logo.png') # => '/assets/logo.png'
95
+ #
96
+ # @param <String> path is the path of the file that is referred to in
97
+ # an input file, such as a stylesheet or HTML page.
98
+ # @return <String> path to the same file as the input path but relative
99
+ # to the site root.
100
+ def absolutize(path)
101
+ return path if path =~ /^\//
102
+ File.join(File.dirname(current_item[:content_filename]), path).sub(/^content/, '')
103
+ end
104
+ end
105
+
106
+ # The Css strategy looks for CSS-style external references that use the
107
+ # url() syntax. This will typically cover any @import statements and
108
+ # references to images.
109
+ class Css < Strategy
110
+ REGEX = /
111
+ url\( # Start with the literal url(
112
+ ('|"|) # Then either a single, double or no quote at all
113
+ (
114
+ ([^'")]+) # The file basename, and below the extension
115
+ \.(#{Nanoc3::Cachebuster::FILETYPES_TO_FINGERPRINT.join('|')})
116
+ )
117
+ \1 # Repeat the same quote as at the start
118
+ \) # And cose the url()
119
+ /ix
120
+
121
+ def apply(m, quote, filename, basename, extension)
122
+ m.sub(filename, output_filename(filename))
123
+ end
124
+ end
125
+
126
+ # The Html strategy looks for HTML-style attributes in the item source code,
127
+ # picking up the values of href and src attributes. This will typically cover
128
+ # links, stylesheets, images and javascripts.
129
+ class Html < Strategy
130
+ REGEX = /
131
+ (href|src) # Look for either an href="" or src="" attribute
132
+ = # ...followed by an =
133
+ ("|'|) # Then either a single, double or no quote at all
134
+ ( # Capture the entire reference
135
+ [^'"]+ # Anything but something that would close the attribute
136
+ # And then the extension:
137
+ (\.(?:#{Nanoc3::Cachebuster::FILETYPES_TO_FINGERPRINT.join('|')}))
138
+ )
139
+ \2 # Repeat the opening quote
140
+ /ix
141
+
142
+ def apply(m, attribute, quote, filename, extension)
143
+ %Q{#{attribute}=#{quote}#{output_filename(filename)}#{quote}}
144
+ end
145
+ end
146
+ end
147
+ end
148
+
@@ -0,0 +1,5 @@
1
+ module Nanoc3
2
+ module Cachebuster
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,44 @@
1
+ require 'nanoc3'
2
+ require 'digest'
3
+
4
+ module Nanoc3
5
+ module Cachebuster
6
+ autoload :VERSION, 'cachebuster/version'
7
+
8
+ # List of file extensions that the routing system should regard
9
+ # as needing a fingerprint. These are input file extensions, so
10
+ # we also include the extensions used by popular preprocessors.
11
+ FILETYPES_TO_FINGERPRINT = %w[css js scss sass less coffee html htm png jpg jpeg gif]
12
+
13
+ # List of file extensions that should be considered css. This is used
14
+ # to determine what filtering strategy to use when none is explicitly
15
+ # set.
16
+ FILETYPES_CONSIDERED_CSS = %w[css js scss sass less]
17
+
18
+ # Value prepended to the file fingerprint, to identify it as a cache buster.
19
+ CACHEBUSTER_PREFIX = '-cb'
20
+
21
+ # Custom exception that might be raised by the rewriting strategies when
22
+ # there can be no source file found for the reference that it found that
23
+ # might need rewriting.
24
+ #
25
+ # This exception should never bubble up from the filter.
26
+ NoSuchSourceFile = Class.new(Exception)
27
+
28
+ # Custom exception that will be raised when trying to use a filtering
29
+ # strategy that does not exist. This will bubble up to the end user.
30
+ NoSuchStrategy = Class.new(Exception)
31
+
32
+ def self.should_apply_fingerprint_to_file?(item)
33
+ FILETYPES_TO_FINGERPRINT.include? item[:extension]
34
+ end
35
+
36
+ def self.fingerprint_file(filename, length = 8)
37
+ CACHEBUSTER_PREFIX + Digest::MD5.hexdigest(File.read(filename))[0..length.to_i]
38
+ end
39
+ end
40
+
41
+ require File.expand_path('../filters', __FILE__)
42
+ require File.expand_path('../helpers', __FILE__)
43
+ require File.expand_path('../cachebuster/strategy', __FILE__)
44
+ end
@@ -0,0 +1,33 @@
1
+ module Nanoc3
2
+ module Filters
3
+ class CacheBuster < Nanoc3::Filter
4
+ identifier :cache_buster
5
+
6
+ def run(content, options = {})
7
+ kind = options[:strategy] || (stylesheet? ? :css : :html)
8
+ strategy = Nanoc3::Cachebuster::Strategy.for(kind , site, item)
9
+ content.gsub(strategy.class::REGEX) do |m|
10
+ begin
11
+ strategy.apply m, $1, $2, $3, $4
12
+ rescue Nanoc3::Cachebuster::NoSuchSourceFile
13
+ m
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # See if the current item is a stylesheet.
21
+ #
22
+ # This is a simple check for filetypes, but you can override what strategy to use
23
+ # with the filter options. This provides a default.
24
+ #
25
+ # @see Nanoc3::Cachebuster::FILETYPES_CONSIDERED_CSS
26
+ # @return <Bool>
27
+ def stylesheet?
28
+ Nanoc3::Cachebuster::FILETYPES_CONSIDERED_CSS.include?(item[:extension].to_s)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,4 @@
1
+ module Nanoc3::Filters
2
+ autoload 'CacheBuster', 'nanoc3/filters/cache_buster'
3
+ Nanoc3::Filter.register '::Nanoc3::Filters::CacheBuster', :cache_buster
4
+ end
@@ -0,0 +1,32 @@
1
+ module Nanoc3
2
+ module Helpers
3
+ module CacheBusting
4
+
5
+ # Test if we want to filter the output filename for a given item.
6
+ # This is logic used in the Rules file, but doesn't belong there.
7
+ #
8
+ # @example Determining whether to rewrite an output filename
9
+ # # in your Rules file
10
+ # route '/assets/*' do
11
+ # hash = cachebust?(item) ? cachebusting_hash(item) : ''
12
+ # item.identifier + hash + '.' + item[:extension]
13
+ # end
14
+ #
15
+ # @param <Item> item is the item to test
16
+ # @return <Boolean>
17
+ def cachebust?(item)
18
+ Nanoc3::Cachebuster.should_apply_fingerprint_to_file?(item)
19
+ end
20
+
21
+ # Get a unique fingerprint for a file's content. This currently uses
22
+ # an MD5 hash of the file contents.
23
+ #
24
+ # @todo Also allow passing in an item rather than a path
25
+ # @param <String> filename is the path to the file to fingerprint.
26
+ # @return <String> file fingerprint
27
+ def fingerprint(filename)
28
+ Nanoc3::Cachebuster.fingerprint_file(filename)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module Nanoc3::Helpers
2
+ autoload 'CacheBusting', 'nanoc3/helpers/cache_busting'
3
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'nanoc3/cachebuster/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'nanoc-cachebuster'
7
+ s.version = Nanoc3::Cachebuster::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Arjan van der Gaag']
10
+ s.email = ['arjan@arjanvandergaag.nl']
11
+ s.homepage = 'http://github.com/avdgaag/nanoc_cachebuster'
12
+ s.summary = %q{Adds filters and helpers for cache busting to Nanoc}
13
+ s.description = <<-EOS
14
+ Your website should use far-future expires headers on static assets, to make
15
+ the best use of client-side caching. But when a file is cached, updates won't
16
+ get picked up. Cache busting is the practice of making the filename of a
17
+ cached asset unique to its content, so it can be cached without having to
18
+ worry about future changes.
19
+
20
+ This gem adds a filter and some helper methods to Nanoc, the static site
21
+ generator, to simplify the process of making asset filenames unique. It helps
22
+ you output fingerprinted filenames, and refer to them from your source files.
23
+
24
+ It works on images, javascripts and stylesheets. It is extracted from the
25
+ nanoc-template project at http://github.com/avdgaag/nanoc-template.
26
+ EOS
27
+
28
+ s.rubyforge_project = 'nanoc-cachebuster'
29
+
30
+ s.files = `git ls-files`.split("\n")
31
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
32
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
33
+ s.require_paths = ['lib']
34
+ end
@@ -0,0 +1,247 @@
1
+ require 'ostruct'
2
+
3
+ class MockItem
4
+ attr_reader :path, :content
5
+
6
+ def self.css_file(content = 'example content')
7
+ new '/styles-cb123456789.css', content, { :extension => 'css', :content_filename => 'content/styles.css' }
8
+ end
9
+
10
+ def self.html_file(content = 'example content')
11
+ new '/output_file.html', content, { :extension => 'html', :content_filename => 'content/input_file.html' }
12
+ end
13
+
14
+ def self.image_file(input = '/foo.png', output = '/foo-cb123456789.png')
15
+ new output, 'hello, world', { :extension => 'png', :content_filename => input }
16
+ end
17
+
18
+ def self.image_file_routed_somewhere_else
19
+ image_file '/foo.png', '/folder/foo-cb123456789.png'
20
+ end
21
+
22
+ def self.image_file_unfiltered
23
+ image_file '/foo.png', '/foo.png'
24
+ end
25
+
26
+ def initialize(path, content, attributes = {})
27
+ @path, @content, @attributes = path, content, attributes
28
+ end
29
+
30
+ def identifier
31
+ File.basename(@path)
32
+ end
33
+
34
+ def [](k)
35
+ @attributes[k]
36
+ end
37
+ end
38
+
39
+ describe Nanoc3::Filters::CacheBuster do
40
+ before(:each) do
41
+ Digest::MD5.stub!(:hexdigest).and_return('123456789')
42
+ end
43
+
44
+ let(:subject) { Nanoc3::Filters::CacheBuster.new context }
45
+ let(:content) { item.content }
46
+ let(:item) { MockItem.css_file }
47
+ let(:target) { MockItem.image_file }
48
+ let(:items) { [item, target] }
49
+ let(:site) { OpenStruct.new({ :items => items }) }
50
+ let(:context) do
51
+ {
52
+ :site => site,
53
+ :item => item,
54
+ :content => content,
55
+ :items => items
56
+ }
57
+ end
58
+
59
+ describe 'filter interface' do
60
+ it { should be_kind_of(Nanoc3::Filter) }
61
+ it { should respond_to(:run) }
62
+
63
+ it 'should accept a string and an options Hash' do
64
+ lambda { subject.run('foo', {}) }.should_not raise_error(ArgumentError)
65
+ end
66
+ end
67
+
68
+ def self.it_should_filter(replacements = {})
69
+ replacements.each do |original, busted|
70
+ it 'should add cache buster to reference' do
71
+ context[:content] = original
72
+ subject.run(original).should == busted
73
+ end
74
+ end
75
+ end
76
+
77
+ def self.it_should_not_filter(str)
78
+ it 'should not change the reference' do
79
+ context[:content] = str
80
+ subject.run(str).should == str
81
+ end
82
+ end
83
+
84
+ describe 'filtering CSS' do
85
+ let(:item) { MockItem.css_file }
86
+
87
+ describe 'when the file exists' do
88
+ before(:each) do
89
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
90
+ end
91
+
92
+ describe 'without quotes' do
93
+ it_should_filter %Q{background: url(foo.png);} => 'background: url(foo-cb123456789.png);'
94
+ end
95
+
96
+ describe 'with single quotes' do
97
+ it_should_filter %Q{background: url('foo.png');} => %Q{background: url('foo-cb123456789.png');}
98
+ end
99
+
100
+ describe 'with double quotes' do
101
+ it_should_filter %Q{background: url("foo.png");} => %Q{background: url("foo-cb123456789.png");}
102
+ end
103
+ end
104
+
105
+ describe 'when using an absolute path' do
106
+ let(:target) { MockItem.image_file '/foo.png', '/images/foo-cb123456789.png' }
107
+
108
+ before(:each) do
109
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
110
+ end
111
+
112
+ it_should_filter %Q{background: url("/images/foo.png");} => %Q{background: url("/images/foo-cb123456789.png");}
113
+ end
114
+
115
+ describe 'when using a relative path' do
116
+ let(:target) { MockItem.image_file '/foo.png', '/../images/foo-cb123456789.png' }
117
+
118
+ before(:each) do
119
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
120
+ end
121
+
122
+ it_should_filter %Q{background: url("../images/foo.png");} => %Q{background: url("../images/foo-cb123456789.png");}
123
+ end
124
+
125
+ describe 'when the file does not exist' do
126
+ let(:target) { MockItem.image_file_routed_somewhere_else }
127
+
128
+ it_should_not_filter %Q{background: url(foo.png);}
129
+ end
130
+
131
+ describe 'when the file is not cache busted' do
132
+ let(:target) { MockItem.image_file_unfiltered }
133
+
134
+ it_should_not_filter %Q{background: url(foo.png);}
135
+ end
136
+ end
137
+
138
+ describe 'filtering HTML' do
139
+ describe 'on the href attribute' do
140
+ let(:item) { MockItem.html_file '<link href="foo.png">' }
141
+
142
+ describe 'when the file exists' do
143
+ before(:each) do
144
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
145
+ end
146
+
147
+ describe 'without quotes' do
148
+ it_should_filter %Q{<link href=foo.png>} => %Q{<link href=foo-cb123456789.png>}
149
+ end
150
+
151
+ describe 'with single quotes' do
152
+ it_should_filter %Q{<link href='foo.png'>} => %Q{<link href='foo-cb123456789.png'>}
153
+ end
154
+
155
+ describe 'with double quotes' do
156
+ it_should_filter %Q{<link href="foo.png">} => %Q{<link href="foo-cb123456789.png">}
157
+ end
158
+ end
159
+
160
+ describe 'when using an absolute path' do
161
+ let(:target) { MockItem.image_file '/foo.png', '/images/foo-cb123456789.png' }
162
+
163
+ before(:each) do
164
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
165
+ end
166
+
167
+ it_should_filter %Q{<link href="/images/foo.png">} => %Q{<link href="/images/foo-cb123456789.png">}
168
+ end
169
+
170
+ describe 'when using a relative path' do
171
+ let(:target) { MockItem.image_file '/foo.png', '/../images/foo-cb123456789.png' }
172
+
173
+ before(:each) do
174
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
175
+ end
176
+
177
+ it_should_filter %Q{<link href="../images/foo.png">} => %Q{<link href="../images/foo-cb123456789.png">}
178
+ end
179
+
180
+ describe 'when the file does not exist' do
181
+ let(:target) { MockItem.image_file_routed_somewhere_else }
182
+
183
+ it_should_not_filter '<link href="foo.png">'
184
+ end
185
+
186
+ describe 'when the file is not cache busted' do
187
+ let(:target) { MockItem.image_file_unfiltered }
188
+
189
+ it_should_not_filter '<link href="foo.png">'
190
+ end
191
+ end
192
+
193
+ describe 'on the src attribute' do
194
+ let(:item) { MockItem.html_file '<img src="foo.png">' }
195
+
196
+ describe 'when the file exists' do
197
+ before(:each) do
198
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
199
+ end
200
+
201
+ describe 'without quotes' do
202
+ it_should_filter %Q{<img src=foo.png>} => %Q{<img src=foo-cb123456789.png>}
203
+ end
204
+
205
+ describe 'with single quotes' do
206
+ it_should_filter %Q{<img src='foo.png'>} => %Q{<img src='foo-cb123456789.png'>}
207
+ end
208
+
209
+ describe 'with double quotes' do
210
+ it_should_filter %Q{<img src="foo.png">} => %Q{<img src="foo-cb123456789.png">}
211
+ end
212
+ end
213
+
214
+ describe 'when using an absolute path' do
215
+ let(:target) { MockItem.image_file '/foo.png', '/images/foo-cb123456789.png' }
216
+
217
+ before(:each) do
218
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
219
+ end
220
+
221
+ it_should_filter %Q{<img src="/images/foo.png">} => %Q{<img src="/images/foo-cb123456789.png">}
222
+ end
223
+
224
+ describe 'when using a relative path' do
225
+ let(:target) { MockItem.image_file '/foo.png', '/../images/foo-cb123456789.png' }
226
+
227
+ before(:each) do
228
+ File.stub!(:read).with(File.join(Dir.pwd, 'content', 'foo.png')).and_return(context[:content])
229
+ end
230
+
231
+ it_should_filter %Q{<img src="../images/foo.png">} => %Q{<img src="../images/foo-cb123456789.png">}
232
+ end
233
+
234
+ describe 'when the file does not exist' do
235
+ let(:target) { MockItem.image_file_routed_somewhere_else }
236
+
237
+ it_should_not_filter '<img src="foo.png">'
238
+ end
239
+
240
+ describe 'when the file is not cache busted' do
241
+ let(:target) { MockItem.image_file_unfiltered }
242
+
243
+ it_should_not_filter '<img src="foo.png">'
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,22 @@
1
+ describe Nanoc3::Helpers::CacheBusting do
2
+ let(:subject) do
3
+ o = Object.new
4
+ o.extend Nanoc3::Helpers::CacheBusting
5
+ end
6
+
7
+ describe '#should_cachebust?' do
8
+ %w{png jpg jpeg gif css js scss sass less coffee html htm}.each do |extension|
9
+ it "should add fingerprint to #{extension} files" do
10
+ subject.cachebust?({ :extension => extension }).should be_true
11
+ end
12
+ end
13
+ end
14
+
15
+ describe '#fingerprint' do
16
+ it 'should calculate a checksum of the source file' do
17
+ File.should_receive(:read).with('foo').and_return('baz')
18
+ Digest::MD5.should_receive(:hexdigest).with('baz').and_return('bar')
19
+ subject.fingerprint('foo').should == '-cbbar'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,2 @@
1
+ require 'nanoc3/cachebuster'
2
+
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nanoc-cachebuster
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Arjan van der Gaag
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-21 00:00:00 Z
14
+ dependencies: []
15
+
16
+ description: |
17
+ Your website should use far-future expires headers on static assets, to make
18
+ the best use of client-side caching. But when a file is cached, updates won't
19
+ get picked up. Cache busting is the practice of making the filename of a
20
+ cached asset unique to its content, so it can be cached without having to
21
+ worry about future changes.
22
+
23
+ This gem adds a filter and some helper methods to Nanoc, the static site
24
+ generator, to simplify the process of making asset filenames unique. It helps
25
+ you output fingerprinted filenames, and refer to them from your source files.
26
+
27
+ It works on images, javascripts and stylesheets. It is extracted from the
28
+ nanoc-template project at http://github.com/avdgaag/nanoc-template.
29
+
30
+ email:
31
+ - arjan@arjanvandergaag.nl
32
+ executables: []
33
+
34
+ extensions: []
35
+
36
+ extra_rdoc_files: []
37
+
38
+ files:
39
+ - .gitignore
40
+ - .rspec
41
+ - HISTORY.md
42
+ - LICENSE
43
+ - README.md
44
+ - Rakefile
45
+ - lib/nanoc3/cachebuster.rb
46
+ - lib/nanoc3/cachebuster/strategy.rb
47
+ - lib/nanoc3/cachebuster/version.rb
48
+ - lib/nanoc3/filters.rb
49
+ - lib/nanoc3/filters/cache_buster.rb
50
+ - lib/nanoc3/helpers.rb
51
+ - lib/nanoc3/helpers/cache_busting.rb
52
+ - nanoc-cachebuster.gemspec
53
+ - spec/nanoc3/filters/cache_buster_spec.rb
54
+ - spec/nanoc3/helpers/cache_busting_spec.rb
55
+ - spec/spec_helper.rb
56
+ homepage: http://github.com/avdgaag/nanoc_cachebuster
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options: []
61
+
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ requirements: []
77
+
78
+ rubyforge_project: nanoc-cachebuster
79
+ rubygems_version: 1.7.2
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Adds filters and helpers for cache busting to Nanoc
83
+ test_files:
84
+ - spec/nanoc3/filters/cache_buster_spec.rb
85
+ - spec/nanoc3/helpers/cache_busting_spec.rb
86
+ - spec/spec_helper.rb