nanoc-cachebuster 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/HISTORY.md +5 -0
- data/LICENSE +1 -0
- data/README.md +81 -0
- data/Rakefile +94 -0
- data/lib/nanoc3/cachebuster/strategy.rb +148 -0
- data/lib/nanoc3/cachebuster/version.rb +5 -0
- data/lib/nanoc3/cachebuster.rb +44 -0
- data/lib/nanoc3/filters/cache_buster.rb +33 -0
- data/lib/nanoc3/filters.rb +4 -0
- data/lib/nanoc3/helpers/cache_busting.rb +32 -0
- data/lib/nanoc3/helpers.rb +3 -0
- data/nanoc-cachebuster.gemspec +34 -0
- data/spec/nanoc3/filters/cache_buster_spec.rb +247 -0
- data/spec/nanoc3/helpers/cache_busting_spec.rb +22 -0
- data/spec/spec_helper.rb +2 -0
- metadata +86 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--format documentation --color -r spec/spec_helper.rb
|
data/HISTORY.md
ADDED
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,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,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,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
|
data/spec/spec_helper.rb
ADDED
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
|