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