slimmer 0.8.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/Rakefile +15 -0
- data/lib/slimmer.rb +314 -0
- data/lib/slimmer/template.rb +15 -0
- data/lib/tasks/slimmer.rake +15 -0
- metadata +62 -0
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "rake/gempackagetask"
|
5
|
+
|
6
|
+
spec = Gem::Specification.load('slimmer.gemspec')
|
7
|
+
|
8
|
+
Rake::GemPackageTask.new(spec) do
|
9
|
+
end
|
10
|
+
|
11
|
+
Rake::RDocTask.new do |rd|
|
12
|
+
rd.rdoc_files.include("lib/**/*.rb")
|
13
|
+
rd.rdoc_dir = "rdoc"
|
14
|
+
end
|
15
|
+
|
data/lib/slimmer.rb
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'slimmer/template'
|
4
|
+
require 'erb'
|
5
|
+
|
6
|
+
module Slimmer
|
7
|
+
|
8
|
+
TEMPLATE_HEADER = 'X-Slimmer-Template'
|
9
|
+
|
10
|
+
class App
|
11
|
+
|
12
|
+
def initialize(app,options = {})
|
13
|
+
@app = app
|
14
|
+
@skin = Skin.new(options[:asset_host], options[:template_path])
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
status,env2,body = @app.call(env)
|
19
|
+
rewrite_response(env,[status,env2,body])
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_success(request,body)
|
23
|
+
@skin.success(request, body)
|
24
|
+
end
|
25
|
+
|
26
|
+
def admin(request,body)
|
27
|
+
@skin.admin(request,body)
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_error(request,status, body)
|
31
|
+
@skin.error(request, '500')
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_404(request,body)
|
35
|
+
@skin.error(request, '404')
|
36
|
+
end
|
37
|
+
|
38
|
+
def s(body)
|
39
|
+
return body.to_s unless body.respond_to?(:each)
|
40
|
+
b = ""
|
41
|
+
body.each {|a| b << a }
|
42
|
+
b
|
43
|
+
end
|
44
|
+
|
45
|
+
def rewrite_response(env,triplet)
|
46
|
+
status, headers, app_body = triplet
|
47
|
+
source_request = Rack::Request.new(env)
|
48
|
+
request = Rack::Request.new(headers)
|
49
|
+
if headers['Content-Type'] =~ /text\/html/ || headers['content-type'] =~ /text\/html/
|
50
|
+
case status.to_i
|
51
|
+
when 200
|
52
|
+
if headers[TEMPLATE_HEADER] == 'admin' || source_request.path =~ /^\/admin(\/|$)/
|
53
|
+
rewritten_body = admin(request,s(app_body))
|
54
|
+
else
|
55
|
+
rewritten_body = on_success(request,s(app_body))
|
56
|
+
end
|
57
|
+
when 301, 302, 304
|
58
|
+
rewritten_body = app_body
|
59
|
+
when 404
|
60
|
+
rewritten_body = on_404(request,s(app_body))
|
61
|
+
else
|
62
|
+
rewritten_body = on_error(request,status, s(app_body))
|
63
|
+
end
|
64
|
+
else
|
65
|
+
rewritten_body = app_body
|
66
|
+
end
|
67
|
+
rewritten_body = [rewritten_body] unless rewritten_body.respond_to?(:each)
|
68
|
+
[status, filter_headers(headers), rewritten_body]
|
69
|
+
end
|
70
|
+
|
71
|
+
def filter_headers(header_hash)
|
72
|
+
valid_keys = ['vary', 'set-cookie', 'location', 'content-type', 'expires', 'cache-control']
|
73
|
+
header_hash.keys.each do |key|
|
74
|
+
header_hash.delete(key) unless valid_keys.include?(key.downcase)
|
75
|
+
end
|
76
|
+
header_hash
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class UrlRewriter
|
81
|
+
|
82
|
+
def initialize(request)
|
83
|
+
@request = request
|
84
|
+
end
|
85
|
+
|
86
|
+
def filter(src,dest)
|
87
|
+
rewrite_document src
|
88
|
+
end
|
89
|
+
|
90
|
+
def rewrite_document(doc)
|
91
|
+
rewrite_nodes doc.css('body img'),'src'
|
92
|
+
rewrite_nodes doc.css('script'),'src'
|
93
|
+
rewrite_nodes doc.css('link'),'href'
|
94
|
+
end
|
95
|
+
|
96
|
+
def rewrite_nodes(nodes,attr)
|
97
|
+
nodes.each do |node|
|
98
|
+
next unless node.attr(attr)
|
99
|
+
node_uri = URI.parse(node.attr(attr))
|
100
|
+
node.attribute(attr).value = rewrite_url(node_uri).to_s
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def rewrite_url(uri)
|
105
|
+
unless uri.absolute?
|
106
|
+
uri.scheme = @request.scheme
|
107
|
+
if @request.host =~ /:/
|
108
|
+
host,port = @request.host.split(":")
|
109
|
+
uri.host = host
|
110
|
+
uri.port = port
|
111
|
+
else
|
112
|
+
uri.host = @request.host
|
113
|
+
uri.port = @request.port
|
114
|
+
end
|
115
|
+
end
|
116
|
+
uri
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class TitleInserter
|
121
|
+
def filter(src,dest)
|
122
|
+
title = src.at_css('head title')
|
123
|
+
head = dest.at_xpath('/html/head')
|
124
|
+
if head && title
|
125
|
+
insert_title(title,head)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def insert_title(title, head)
|
130
|
+
if head.at_css('title').nil?
|
131
|
+
head.first_element_child.nil? ? head << title : head.first_element_child.before(title)
|
132
|
+
else
|
133
|
+
head.at_css('title').replace(title)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class SectionInserter
|
139
|
+
def filter(src,dest)
|
140
|
+
meta_name = dest.at_css('meta[name="x-section-name"]')
|
141
|
+
meta_link = dest.at_css('meta[name="x-section-link"]')
|
142
|
+
list = dest.at_css('nav[role=navigation] ol')
|
143
|
+
|
144
|
+
if meta_name && meta_link && list
|
145
|
+
link_node = Nokogiri::XML::Node.new('a', dest)
|
146
|
+
link_node['href'] = meta_link['content']
|
147
|
+
link_node.content = meta_name['content']
|
148
|
+
|
149
|
+
list_item = Nokogiri::XML::Node.new('li', dest)
|
150
|
+
list_item.add_child(link_node)
|
151
|
+
|
152
|
+
list.first_element_child.after(list_item)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class TagMover
|
158
|
+
def filter(src,dest)
|
159
|
+
move_tags(src, dest, 'script', :must_have => ['src'])
|
160
|
+
move_tags(src, dest, 'link', :must_have => ['href'])
|
161
|
+
move_tags(src, dest, 'meta', :must_have => ['name', 'content'], :keys => ['name', 'content', 'http-equiv'])
|
162
|
+
end
|
163
|
+
|
164
|
+
def include_tag?(node, min_attrs)
|
165
|
+
min_attrs.inject(true) { |all_okay, attr_name| all_okay && node.has_attribute?(attr_name) }
|
166
|
+
end
|
167
|
+
|
168
|
+
def tag_fingerprint(node, attrs)
|
169
|
+
attrs.collect do |attr_name|
|
170
|
+
node.has_attribute?(attr_name) ? node.attr(attr_name) : nil
|
171
|
+
end.compact.sort
|
172
|
+
end
|
173
|
+
|
174
|
+
def move_tags(src, dest, type, opts)
|
175
|
+
comparison_attrs = opts[:keys] || opts[:must_have]
|
176
|
+
min_attrs = opts[:must_have]
|
177
|
+
already_there = dest.css(type).map { |node|
|
178
|
+
tag_fingerprint(node, comparison_attrs)
|
179
|
+
}.compact
|
180
|
+
|
181
|
+
src.css(type).each do |node|
|
182
|
+
if include_tag?(node, min_attrs) && !already_there.include?(tag_fingerprint(node, comparison_attrs))
|
183
|
+
node.remove
|
184
|
+
dest.at_xpath('/html/head') << node
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class BodyInserter
|
191
|
+
def initialize(path='#wrapper')
|
192
|
+
@path = path
|
193
|
+
end
|
194
|
+
|
195
|
+
def filter(src,dest)
|
196
|
+
body = src.fragment(src.at_css(@path))
|
197
|
+
dest.at_css(@path).replace(body)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
class BodyClassCopier
|
202
|
+
def filter(src, dest)
|
203
|
+
src_body_tag = src.at_css("body")
|
204
|
+
dest_body_tag = dest.at_css('body')
|
205
|
+
if src_body_tag.has_attribute?("class")
|
206
|
+
combinded_classes = dest_body_tag.attr('class').to_s.split(/ +/)
|
207
|
+
combinded_classes << src_body_tag.attr('class').to_s.split(/ +/)
|
208
|
+
dest_body_tag.set_attribute("class", combinded_classes.join(' '))
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class AdminTitleInserter
|
214
|
+
def filter(src,dest)
|
215
|
+
title = src.at_css('#site-title')
|
216
|
+
head = dest.at_css('.gds-header h2')
|
217
|
+
if head && title
|
218
|
+
head.content = title.content
|
219
|
+
title.remove
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class FooterRemover
|
225
|
+
def filter(src,dest)
|
226
|
+
footer = src.at_css("#footer")
|
227
|
+
footer.remove if footer
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class Skin
|
232
|
+
|
233
|
+
def initialize(asset_host = nil, template_path = nil)
|
234
|
+
@asset_host = asset_host
|
235
|
+
@template_path = template_path
|
236
|
+
@template = {}
|
237
|
+
end
|
238
|
+
|
239
|
+
def template(template_name)
|
240
|
+
if templates_are_local?
|
241
|
+
load_template(template_name)
|
242
|
+
else
|
243
|
+
@template[template_name] ||= load_template(template_name)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def load_template(template_name)
|
248
|
+
source = open("#{template_path}/#{template_name}.html.erb", "r:UTF-8").read
|
249
|
+
ERB.new(source).result binding
|
250
|
+
end
|
251
|
+
|
252
|
+
def template_path
|
253
|
+
@template_path || (@asset_host.to_s + '/templates')
|
254
|
+
end
|
255
|
+
|
256
|
+
def templates_are_local?
|
257
|
+
File.exists? template_path
|
258
|
+
end
|
259
|
+
|
260
|
+
def unparse_esi(doc)
|
261
|
+
## HTML doesn't really have namespaces, and nokogiri's
|
262
|
+
## default behaviour is to strip the namespace, but to
|
263
|
+
## leave the tag name intact. Ugly hack here to reverse
|
264
|
+
## that for ESI includes
|
265
|
+
doc.gsub("<include","<esi:include").gsub(/><\/(esi:)?include>/, ' />')
|
266
|
+
end
|
267
|
+
|
268
|
+
def error(request, template_name)
|
269
|
+
processors = [
|
270
|
+
TitleInserter.new()
|
271
|
+
]
|
272
|
+
self.process(processors,"<html></html>",template(template_name))
|
273
|
+
end
|
274
|
+
|
275
|
+
def process(processors,body,template)
|
276
|
+
src = Nokogiri::HTML.parse(body.to_s)
|
277
|
+
dest = Nokogiri::HTML.parse(template)
|
278
|
+
|
279
|
+
processors.each do |p|
|
280
|
+
p.filter(src,dest)
|
281
|
+
end
|
282
|
+
|
283
|
+
return unparse_esi(dest.to_html)
|
284
|
+
end
|
285
|
+
|
286
|
+
def admin(request,body)
|
287
|
+
processors = [
|
288
|
+
TitleInserter.new(),
|
289
|
+
TagMover.new(),
|
290
|
+
AdminTitleInserter.new,
|
291
|
+
FooterRemover.new,
|
292
|
+
BodyInserter.new(),
|
293
|
+
BodyClassCopier.new
|
294
|
+
]
|
295
|
+
self.process(processors,body,template('admin'))
|
296
|
+
end
|
297
|
+
|
298
|
+
def success(request,body)
|
299
|
+
|
300
|
+
processors = [
|
301
|
+
TitleInserter.new(),
|
302
|
+
TagMover.new(),
|
303
|
+
BodyInserter.new(),
|
304
|
+
BodyClassCopier.new,
|
305
|
+
SectionInserter.new()
|
306
|
+
]
|
307
|
+
|
308
|
+
template_name = request.env.has_key?(TEMPLATE_HEADER) ? request.env[TEMPLATE_HEADER] : 'wrapper'
|
309
|
+
self.process(processors,body,template(template_name))
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
|
314
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Slimmer
|
2
|
+
module Template
|
3
|
+
def self.included into
|
4
|
+
into.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def slimmer_template template_name
|
9
|
+
after_filter do
|
10
|
+
response.headers[Slimmer::TEMPLATE_HEADER] = template_name.to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
namespace :slimmer do
|
4
|
+
desc "Symlink from public directory to static directory"
|
5
|
+
task :link do
|
6
|
+
path_to_static = "../static/public"
|
7
|
+
path_to_public = "public"
|
8
|
+
commands = ["cd #{path_to_public}"]
|
9
|
+
dirs_to_link = Dir.glob("../static/public/*") {|f|
|
10
|
+
commands << "ln -s #{path_to_static}/#{f}"
|
11
|
+
}
|
12
|
+
commands << ["cd .."]
|
13
|
+
run commands.join(" && ")
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slimmer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ben Griffiths
|
9
|
+
- James Stewart
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-11-01 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: nokogiri
|
17
|
+
requirement: &70264965293060 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70264965293060
|
26
|
+
description: Rack middleware for skinning pages using a specific template
|
27
|
+
email:
|
28
|
+
- bengriffiths@gmail.com
|
29
|
+
- james.stewart@digital.cabinet-office.gov.uk
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/slimmer/template.rb
|
35
|
+
- lib/slimmer.rb
|
36
|
+
- lib/tasks/slimmer.rake
|
37
|
+
- Rakefile
|
38
|
+
homepage: http://github.com/alphagov/slimmer
|
39
|
+
licenses: []
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project: slimmer
|
58
|
+
rubygems_version: 1.8.10
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: Thinner than the skinner
|
62
|
+
test_files: []
|