nanoc-cachebuster 0.1.0

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/.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