slimmer 1.1.3 → 1.1.4beta2
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/lib/slimmer.rb +15 -336
- data/lib/slimmer/admin_title_inserter.rb +12 -0
- data/lib/slimmer/app.rb +84 -0
- data/lib/slimmer/body_class_copier.rb +13 -0
- data/lib/slimmer/body_inserter.rb +12 -0
- data/lib/slimmer/footer_remover.rb +8 -0
- data/lib/slimmer/header_context_inserter.rb +14 -0
- data/lib/slimmer/section_inserter.rb +23 -0
- data/lib/slimmer/skin.rb +96 -0
- data/lib/slimmer/tag_mover.rb +34 -0
- data/lib/slimmer/test.rb +22 -0
- data/lib/slimmer/title_inserter.rb +19 -0
- data/lib/slimmer/url_rewriter.rb +40 -0
- data/lib/slimmer/version.rb +1 -1
- data/test/fixtures/wrapper.html.erb +3 -1
- data/test/processors/header_context_inserter_test.rb +51 -0
- data/test/test_helper.rb +4 -0
- data/test/typical_usage_test.rb +22 -1
- metadata +33 -19
data/lib/slimmer.rb
CHANGED
@@ -1,346 +1,25 @@
|
|
1
|
-
require 'slimmer/template'
|
2
|
-
require 'slimmer/railtie' if defined?(Rails)
|
3
|
-
|
4
1
|
require 'nokogiri'
|
5
2
|
require 'erb'
|
6
3
|
require 'open-uri'
|
7
4
|
require 'plek'
|
8
5
|
|
9
6
|
module Slimmer
|
10
|
-
|
11
7
|
TEMPLATE_HEADER = 'X-Slimmer-Template'
|
12
8
|
SKIP_HEADER = 'X-Slimmer-Skip'
|
13
9
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def call(env)
|
31
|
-
response_array = @app.call(env)
|
32
|
-
if response_array[1][SKIP_HEADER]
|
33
|
-
response_array
|
34
|
-
else
|
35
|
-
rewrite_response(env, response_array)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def on_success(request,body)
|
40
|
-
@skin.success(request, body)
|
41
|
-
end
|
42
|
-
|
43
|
-
def admin(request,body)
|
44
|
-
@skin.admin(request,body)
|
45
|
-
end
|
46
|
-
|
47
|
-
def on_error(request, status, body)
|
48
|
-
@skin.error(request, '500', body)
|
49
|
-
end
|
50
|
-
|
51
|
-
def on_404(request,body)
|
52
|
-
@skin.error(request, '404', body)
|
53
|
-
end
|
54
|
-
|
55
|
-
def s(body)
|
56
|
-
return body.to_s unless body.respond_to?(:each)
|
57
|
-
b = ""
|
58
|
-
body.each {|a| b << a }
|
59
|
-
b
|
60
|
-
end
|
61
|
-
|
62
|
-
def rewrite_response(env,triplet)
|
63
|
-
status, headers, app_body = triplet
|
64
|
-
source_request = Rack::Request.new(env)
|
65
|
-
request = Rack::Request.new(headers)
|
66
|
-
if headers['Content-Type'] =~ /text\/html/ || headers['content-type'] =~ /text\/html/
|
67
|
-
case status.to_i
|
68
|
-
when 200
|
69
|
-
if headers[TEMPLATE_HEADER] == 'admin' || source_request.path =~ /^\/admin(\/|$)/
|
70
|
-
rewritten_body = admin(request,s(app_body))
|
71
|
-
else
|
72
|
-
rewritten_body = on_success(request,s(app_body))
|
73
|
-
end
|
74
|
-
when 301, 302, 304
|
75
|
-
rewritten_body = app_body
|
76
|
-
when 404
|
77
|
-
rewritten_body = on_404(request,s(app_body))
|
78
|
-
else
|
79
|
-
rewritten_body = on_error(request,status,s(app_body))
|
80
|
-
end
|
81
|
-
else
|
82
|
-
rewritten_body = app_body
|
83
|
-
end
|
84
|
-
rewritten_body = [rewritten_body] unless rewritten_body.respond_to?(:each)
|
85
|
-
[status, filter_headers(headers), rewritten_body]
|
86
|
-
end
|
87
|
-
|
88
|
-
def filter_headers(header_hash)
|
89
|
-
valid_keys = ['vary', 'set-cookie', 'location', 'content-type', 'expires', 'cache-control']
|
90
|
-
header_hash.keys.each do |key|
|
91
|
-
header_hash.delete(key) unless valid_keys.include?(key.downcase)
|
92
|
-
end
|
93
|
-
header_hash
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
class UrlRewriter
|
98
|
-
|
99
|
-
def initialize(request)
|
100
|
-
@request = request
|
101
|
-
end
|
102
|
-
|
103
|
-
def filter(src,dest)
|
104
|
-
rewrite_document src
|
105
|
-
end
|
106
|
-
|
107
|
-
def rewrite_document(doc)
|
108
|
-
rewrite_nodes doc.css('body img'),'src'
|
109
|
-
rewrite_nodes doc.css('script'),'src'
|
110
|
-
rewrite_nodes doc.css('link'),'href'
|
111
|
-
end
|
112
|
-
|
113
|
-
def rewrite_nodes(nodes,attr)
|
114
|
-
nodes.each do |node|
|
115
|
-
next unless node.attr(attr)
|
116
|
-
node_uri = URI.parse(node.attr(attr))
|
117
|
-
node.attribute(attr).value = rewrite_url(node_uri).to_s
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def rewrite_url(uri)
|
122
|
-
unless uri.absolute?
|
123
|
-
uri.scheme = @request.scheme
|
124
|
-
if @request.host =~ /:/
|
125
|
-
host,port = @request.host.split(":")
|
126
|
-
uri.host = host
|
127
|
-
uri.port = port
|
128
|
-
else
|
129
|
-
uri.host = @request.host
|
130
|
-
uri.port = @request.port
|
131
|
-
end
|
132
|
-
end
|
133
|
-
uri
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
class TitleInserter
|
138
|
-
def filter(src,dest)
|
139
|
-
title = src.at_css('head title')
|
140
|
-
head = dest.at_xpath('/html/head')
|
141
|
-
if head && title
|
142
|
-
insert_title(title,head)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
def insert_title(title, head)
|
147
|
-
if head.at_css('title').nil?
|
148
|
-
head.first_element_child.nil? ? head << title : head.first_element_child.before(title)
|
149
|
-
else
|
150
|
-
head.at_css('title').replace(title)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
class SectionInserter
|
156
|
-
def filter(src,dest)
|
157
|
-
meta_name = dest.at_css('meta[name="x-section-name"]')
|
158
|
-
meta_link = dest.at_css('meta[name="x-section-link"]')
|
159
|
-
list = dest.at_css('nav[role=navigation] ol')
|
160
|
-
|
161
|
-
# FIXME: Presumably this is meant to stop us adding a 'current section'
|
162
|
-
# link if we're missing navigation, or x-section-* meta tags.
|
163
|
-
# It doesn't work: #at_css will return a truthy object in any case.
|
164
|
-
if meta_name && meta_link && list
|
165
|
-
link_node = Nokogiri::XML::Node.new('a', dest)
|
166
|
-
link_node['href'] = meta_link['content']
|
167
|
-
link_node.content = meta_name['content']
|
168
|
-
|
169
|
-
list_item = Nokogiri::XML::Node.new('li', dest)
|
170
|
-
list_item.add_child(link_node)
|
171
|
-
|
172
|
-
list.first_element_child.after(list_item)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
class TagMover
|
178
|
-
def filter(src,dest)
|
179
|
-
move_tags(src, dest, 'script', :must_have => ['src'])
|
180
|
-
move_tags(src, dest, 'link', :must_have => ['href'])
|
181
|
-
move_tags(src, dest, 'meta', :must_have => ['name', 'content'], :keys => ['name', 'content', 'http-equiv'])
|
182
|
-
end
|
183
|
-
|
184
|
-
def include_tag?(node, min_attrs)
|
185
|
-
min_attrs.inject(true) { |all_okay, attr_name| all_okay && node.has_attribute?(attr_name) }
|
186
|
-
end
|
187
|
-
|
188
|
-
def tag_fingerprint(node, attrs)
|
189
|
-
attrs.collect do |attr_name|
|
190
|
-
node.has_attribute?(attr_name) ? node.attr(attr_name) : nil
|
191
|
-
end.compact.sort
|
192
|
-
end
|
193
|
-
|
194
|
-
def move_tags(src, dest, type, opts)
|
195
|
-
comparison_attrs = opts[:keys] || opts[:must_have]
|
196
|
-
min_attrs = opts[:must_have]
|
197
|
-
already_there = dest.css(type).map { |node|
|
198
|
-
tag_fingerprint(node, comparison_attrs)
|
199
|
-
}.compact
|
200
|
-
|
201
|
-
src.css(type).each do |node|
|
202
|
-
if include_tag?(node, min_attrs) && !already_there.include?(tag_fingerprint(node, comparison_attrs))
|
203
|
-
node.remove
|
204
|
-
dest.at_xpath('/html/head') << node
|
205
|
-
end
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
class BodyInserter
|
211
|
-
def initialize(path='#wrapper')
|
212
|
-
@path = path
|
213
|
-
end
|
214
|
-
|
215
|
-
def filter(src,dest)
|
216
|
-
body = src.fragment(src.at_css(@path))
|
217
|
-
dest.at_css(@path).replace(body)
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
class BodyClassCopier
|
222
|
-
def filter(src, dest)
|
223
|
-
src_body_tag = src.at_css("body")
|
224
|
-
dest_body_tag = dest.at_css('body')
|
225
|
-
if src_body_tag.has_attribute?("class")
|
226
|
-
combinded_classes = dest_body_tag.attr('class').to_s.split(/ +/)
|
227
|
-
combinded_classes << src_body_tag.attr('class').to_s.split(/ +/)
|
228
|
-
dest_body_tag.set_attribute("class", combinded_classes.join(' '))
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
class AdminTitleInserter
|
234
|
-
def filter(src,dest)
|
235
|
-
title = src.at_css('#site-title')
|
236
|
-
head = dest.at_css('.gds-header h2')
|
237
|
-
if head && title
|
238
|
-
head.content = title.content
|
239
|
-
title.remove
|
240
|
-
end
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
class FooterRemover
|
245
|
-
def filter(src,dest)
|
246
|
-
footer = src.at_css("#footer")
|
247
|
-
footer.remove if footer
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
class Skin
|
252
|
-
attr_accessor :use_cache
|
253
|
-
private :use_cache=, :use_cache
|
254
|
-
|
255
|
-
attr_accessor :templated_cache
|
256
|
-
private :templated_cache=, :templated_cache
|
257
|
-
|
258
|
-
attr_accessor :asset_host
|
259
|
-
private :asset_host=, :asset_host
|
260
|
-
|
261
|
-
# TODO: Extract the cache to something we can pass in instead of using
|
262
|
-
# true/false and an in-memory cache.
|
263
|
-
def initialize asset_host, use_cache = false
|
264
|
-
self.asset_host = asset_host
|
265
|
-
self.templated_cache = {}
|
266
|
-
self.use_cache = false
|
267
|
-
end
|
268
|
-
|
269
|
-
def template(template_name)
|
270
|
-
return cached_template(template_name) if template_cached? template_name
|
271
|
-
load_template template_name
|
272
|
-
end
|
273
|
-
|
274
|
-
def template_cached? name
|
275
|
-
!cached_template(name).nil?
|
276
|
-
end
|
277
|
-
|
278
|
-
def cached_template name
|
279
|
-
templated_cache[name]
|
280
|
-
end
|
281
|
-
|
282
|
-
def cache name, template
|
283
|
-
return unless use_cache
|
284
|
-
templated_cache[name] = template
|
285
|
-
end
|
286
|
-
|
287
|
-
def load_template template_name
|
288
|
-
url = template_url template_name
|
289
|
-
source = open(url, "r:UTF-8").read
|
290
|
-
template = ERB.new(source).result binding
|
291
|
-
cache template_name, template
|
292
|
-
template
|
293
|
-
end
|
294
|
-
|
295
|
-
def template_url template_name
|
296
|
-
host = asset_host.dup
|
297
|
-
host += '/' unless host =~ /\/$/
|
298
|
-
"#{host}templates/#{template_name}.html.erb"
|
299
|
-
end
|
300
|
-
|
301
|
-
def error(request, template_name, body)
|
302
|
-
processors = [
|
303
|
-
TitleInserter.new()
|
304
|
-
]
|
305
|
-
process(processors, body, template(template_name))
|
306
|
-
end
|
307
|
-
|
308
|
-
def process(processors,body,template)
|
309
|
-
src = Nokogiri::HTML.parse(body.to_s)
|
310
|
-
dest = Nokogiri::HTML.parse(template)
|
311
|
-
|
312
|
-
processors.each do |p|
|
313
|
-
p.filter(src,dest)
|
314
|
-
end
|
315
|
-
|
316
|
-
return dest.to_html
|
317
|
-
end
|
318
|
-
|
319
|
-
def admin(request,body)
|
320
|
-
processors = [
|
321
|
-
TitleInserter.new(),
|
322
|
-
TagMover.new(),
|
323
|
-
AdminTitleInserter.new,
|
324
|
-
FooterRemover.new,
|
325
|
-
BodyInserter.new(),
|
326
|
-
BodyClassCopier.new
|
327
|
-
]
|
328
|
-
process(processors,body,template('admin'))
|
329
|
-
end
|
330
|
-
|
331
|
-
def success(request,body)
|
332
|
-
processors = [
|
333
|
-
TitleInserter.new(),
|
334
|
-
TagMover.new(),
|
335
|
-
BodyInserter.new(),
|
336
|
-
BodyClassCopier.new,
|
337
|
-
SectionInserter.new()
|
338
|
-
]
|
339
|
-
|
340
|
-
template_name = request.env.has_key?(TEMPLATE_HEADER) ? request.env[TEMPLATE_HEADER] : 'wrapper'
|
341
|
-
process(processors,body,template(template_name))
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
345
|
-
|
10
|
+
autoload :Version, 'slimmer/version'
|
11
|
+
autoload :Railtie, 'slimmer/railtie'
|
12
|
+
autoload :Skin, 'slimmer/skin'
|
13
|
+
|
14
|
+
autoload :Template, 'slimmer/template'
|
15
|
+
autoload :App, 'slimmer/app'
|
16
|
+
autoload :TitleInserter, 'slimmer/title_inserter'
|
17
|
+
autoload :AdminTitleInserter, 'slimmer/admin_title_inserter'
|
18
|
+
autoload :SectionInserter, 'slimmer/section_inserter'
|
19
|
+
autoload :TagMover, 'slimmer/tag_mover'
|
20
|
+
autoload :FooterRemover, 'slimmer/footer_remover'
|
21
|
+
autoload :BodyInserter, 'slimmer/body_inserter'
|
22
|
+
autoload :BodyClassCopier, 'slimmer/body_class_copier'
|
23
|
+
autoload :UrlRewriter, 'slimmer/url_rewriter'
|
24
|
+
autoload :HeaderContextInserter, 'slimmer/header_context_inserter'
|
346
25
|
end
|
data/lib/slimmer/app.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class App
|
3
|
+
def initialize(app, *args, &block)
|
4
|
+
options = args.first || {}
|
5
|
+
@app = app
|
6
|
+
|
7
|
+
if options.key? :template_path
|
8
|
+
raise "Template path should not be used. Use asset_host instead."
|
9
|
+
end
|
10
|
+
|
11
|
+
unless options[:asset_host]
|
12
|
+
options[:asset_host] = Plek.current.find("assets")
|
13
|
+
end
|
14
|
+
|
15
|
+
@skin = Skin.new options[:asset_host], options[:cache_templates]
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
response_array = @app.call(env)
|
20
|
+
if response_array[1][SKIP_HEADER]
|
21
|
+
response_array
|
22
|
+
else
|
23
|
+
rewrite_response(env, response_array)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_success(request,body)
|
28
|
+
@skin.success(request, body)
|
29
|
+
end
|
30
|
+
|
31
|
+
def admin(request,body)
|
32
|
+
@skin.admin(request,body)
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_error(request, status, body)
|
36
|
+
@skin.error(request, '500', body)
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_404(request,body)
|
40
|
+
@skin.error(request, '404', body)
|
41
|
+
end
|
42
|
+
|
43
|
+
def s(body)
|
44
|
+
return body.to_s unless body.respond_to?(:each)
|
45
|
+
b = ""
|
46
|
+
body.each {|a| b << a }
|
47
|
+
b
|
48
|
+
end
|
49
|
+
|
50
|
+
def rewrite_response(env,triplet)
|
51
|
+
status, headers, app_body = triplet
|
52
|
+
source_request = Rack::Request.new(env)
|
53
|
+
request = Rack::Request.new(headers)
|
54
|
+
if headers['Content-Type'] =~ /text\/html/ || headers['content-type'] =~ /text\/html/
|
55
|
+
case status.to_i
|
56
|
+
when 200
|
57
|
+
if headers[TEMPLATE_HEADER] == 'admin' || source_request.path =~ /^\/admin(\/|$)/
|
58
|
+
rewritten_body = admin(request,s(app_body))
|
59
|
+
else
|
60
|
+
rewritten_body = on_success(request,s(app_body))
|
61
|
+
end
|
62
|
+
when 301, 302, 304
|
63
|
+
rewritten_body = app_body
|
64
|
+
when 404
|
65
|
+
rewritten_body = on_404(request,s(app_body))
|
66
|
+
else
|
67
|
+
rewritten_body = on_error(request,status,s(app_body))
|
68
|
+
end
|
69
|
+
else
|
70
|
+
rewritten_body = app_body
|
71
|
+
end
|
72
|
+
rewritten_body = [rewritten_body] unless rewritten_body.respond_to?(:each)
|
73
|
+
[status, filter_headers(headers), rewritten_body]
|
74
|
+
end
|
75
|
+
|
76
|
+
def filter_headers(header_hash)
|
77
|
+
valid_keys = ['vary', 'set-cookie', 'location', 'content-type', 'expires', 'cache-control']
|
78
|
+
header_hash.keys.each do |key|
|
79
|
+
header_hash.delete(key) unless valid_keys.include?(key.downcase)
|
80
|
+
end
|
81
|
+
header_hash
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class BodyClassCopier
|
3
|
+
def filter(src, dest)
|
4
|
+
src_body_tag = src.at_css("body")
|
5
|
+
dest_body_tag = dest.at_css('body')
|
6
|
+
if src_body_tag.has_attribute?("class")
|
7
|
+
combinded_classes = dest_body_tag.attr('class').to_s.split(/ +/)
|
8
|
+
combinded_classes << src_body_tag.attr('class').to_s.split(/ +/)
|
9
|
+
dest_body_tag.set_attribute("class", combinded_classes.join(' '))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class HeaderContextInserter
|
3
|
+
def initialize(path='.header-context')
|
4
|
+
@path = path
|
5
|
+
end
|
6
|
+
|
7
|
+
def filter(src,dest)
|
8
|
+
if dest.at_css(@path) && replacement = src.at_css(@path)
|
9
|
+
header_context = src.fragment(replacement)
|
10
|
+
dest.at_css(@path).replace(header_context)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class SectionInserter
|
3
|
+
def filter(src,dest)
|
4
|
+
meta_name = dest.at_css('meta[name="x-section-name"]')
|
5
|
+
meta_link = dest.at_css('meta[name="x-section-link"]')
|
6
|
+
list = dest.at_css('nav[role=navigation] ol')
|
7
|
+
|
8
|
+
# FIXME: Presumably this is meant to stop us adding a 'current section'
|
9
|
+
# link if we're missing navigation, or x-section-* meta tags.
|
10
|
+
# It doesn't work: #at_css will return a truthy object in any case.
|
11
|
+
if meta_name && meta_link && list
|
12
|
+
link_node = Nokogiri::XML::Node.new('a', dest)
|
13
|
+
link_node['href'] = meta_link['content']
|
14
|
+
link_node.content = meta_name['content']
|
15
|
+
|
16
|
+
list_item = Nokogiri::XML::Node.new('li', dest)
|
17
|
+
list_item.add_child(link_node)
|
18
|
+
|
19
|
+
list.first_element_child.after(list_item)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/slimmer/skin.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class Skin
|
3
|
+
attr_accessor :use_cache
|
4
|
+
private :use_cache=, :use_cache
|
5
|
+
|
6
|
+
attr_accessor :templated_cache
|
7
|
+
private :templated_cache=, :templated_cache
|
8
|
+
|
9
|
+
attr_accessor :asset_host
|
10
|
+
private :asset_host=, :asset_host
|
11
|
+
|
12
|
+
# TODO: Extract the cache to something we can pass in instead of using
|
13
|
+
# true/false and an in-memory cache.
|
14
|
+
def initialize asset_host, use_cache = false
|
15
|
+
self.asset_host = asset_host
|
16
|
+
self.templated_cache = {}
|
17
|
+
self.use_cache = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def template(template_name)
|
21
|
+
return cached_template(template_name) if template_cached? template_name
|
22
|
+
load_template template_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def template_cached? name
|
26
|
+
!cached_template(name).nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def cached_template name
|
30
|
+
templated_cache[name]
|
31
|
+
end
|
32
|
+
|
33
|
+
def cache name, template
|
34
|
+
return unless use_cache
|
35
|
+
templated_cache[name] = template
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_template template_name
|
39
|
+
url = template_url template_name
|
40
|
+
source = open(url, "r:UTF-8").read
|
41
|
+
template = ERB.new(source).result binding
|
42
|
+
cache template_name, template
|
43
|
+
template
|
44
|
+
end
|
45
|
+
|
46
|
+
def template_url template_name
|
47
|
+
host = asset_host.dup
|
48
|
+
host += '/' unless host =~ /\/$/
|
49
|
+
"#{host}templates/#{template_name}.html.erb"
|
50
|
+
end
|
51
|
+
|
52
|
+
def error(request, template_name, body)
|
53
|
+
processors = [
|
54
|
+
TitleInserter.new()
|
55
|
+
]
|
56
|
+
process(processors, body, template(template_name))
|
57
|
+
end
|
58
|
+
|
59
|
+
def process(processors,body,template)
|
60
|
+
src = Nokogiri::HTML.parse(body.to_s)
|
61
|
+
dest = Nokogiri::HTML.parse(template)
|
62
|
+
|
63
|
+
processors.each do |p|
|
64
|
+
p.filter(src,dest)
|
65
|
+
end
|
66
|
+
|
67
|
+
return dest.to_html
|
68
|
+
end
|
69
|
+
|
70
|
+
def admin(request,body)
|
71
|
+
processors = [
|
72
|
+
TitleInserter.new(),
|
73
|
+
TagMover.new(),
|
74
|
+
AdminTitleInserter.new,
|
75
|
+
FooterRemover.new,
|
76
|
+
BodyInserter.new(),
|
77
|
+
BodyClassCopier.new
|
78
|
+
]
|
79
|
+
process(processors,body,template('admin'))
|
80
|
+
end
|
81
|
+
|
82
|
+
def success(request,body)
|
83
|
+
processors = [
|
84
|
+
TitleInserter.new(),
|
85
|
+
TagMover.new(),
|
86
|
+
BodyInserter.new(),
|
87
|
+
BodyClassCopier.new,
|
88
|
+
HeaderContextInserter.new(),
|
89
|
+
SectionInserter.new()
|
90
|
+
]
|
91
|
+
|
92
|
+
template_name = request.env.has_key?(TEMPLATE_HEADER) ? request.env[TEMPLATE_HEADER] : 'wrapper'
|
93
|
+
process(processors,body,template(template_name))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class TagMover
|
3
|
+
def filter(src,dest)
|
4
|
+
move_tags(src, dest, 'script', :must_have => ['src'])
|
5
|
+
move_tags(src, dest, 'link', :must_have => ['href'])
|
6
|
+
move_tags(src, dest, 'meta', :must_have => ['name', 'content'], :keys => ['name', 'content', 'http-equiv'])
|
7
|
+
end
|
8
|
+
|
9
|
+
def include_tag?(node, min_attrs)
|
10
|
+
min_attrs.inject(true) { |all_okay, attr_name| all_okay && node.has_attribute?(attr_name) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def tag_fingerprint(node, attrs)
|
14
|
+
attrs.collect do |attr_name|
|
15
|
+
node.has_attribute?(attr_name) ? node.attr(attr_name) : nil
|
16
|
+
end.compact.sort
|
17
|
+
end
|
18
|
+
|
19
|
+
def move_tags(src, dest, type, opts)
|
20
|
+
comparison_attrs = opts[:keys] || opts[:must_have]
|
21
|
+
min_attrs = opts[:must_have]
|
22
|
+
already_there = dest.css(type).map { |node|
|
23
|
+
tag_fingerprint(node, comparison_attrs)
|
24
|
+
}.compact
|
25
|
+
|
26
|
+
src.css(type).each do |node|
|
27
|
+
if include_tag?(node, min_attrs) && !already_there.include?(tag_fingerprint(node, comparison_attrs))
|
28
|
+
node.remove
|
29
|
+
dest.at_xpath('/html/head') << node
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/slimmer/test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class Skin
|
3
|
+
def load_template name
|
4
|
+
%q{
|
5
|
+
<html>
|
6
|
+
<head>
|
7
|
+
<title>Test Template</title>
|
8
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/libs/jquery/jquery-1.6.2.min.js"></script><!-- no defer on jquery -->
|
9
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/libs/jquery/jquery-ui-1.8.16.custom.min.js" defer></script>
|
10
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/libs/jquery/plugins/jquery.base64.js" defer></script>
|
11
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/search.js" defer></script>
|
12
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/devolution.js" defer></script>
|
13
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/popup.js" defer></script>
|
14
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/feedback.js" defer></script>
|
15
|
+
<script src="http://static.dev.alphagov.co.uk/javascripts/customisation-settings.js" defer></script>
|
16
|
+
</head>
|
17
|
+
<body></body>
|
18
|
+
</html>
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class TitleInserter
|
3
|
+
def filter(src,dest)
|
4
|
+
title = src.at_css('head title')
|
5
|
+
head = dest.at_xpath('/html/head')
|
6
|
+
if head && title
|
7
|
+
insert_title(title,head)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def insert_title(title, head)
|
12
|
+
if head.at_css('title').nil?
|
13
|
+
head.first_element_child.nil? ? head << title : head.first_element_child.before(title)
|
14
|
+
else
|
15
|
+
head.at_css('title').replace(title)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Slimmer
|
2
|
+
class UrlRewriter
|
3
|
+
def initialize(request)
|
4
|
+
@request = request
|
5
|
+
end
|
6
|
+
|
7
|
+
def filter(src,dest)
|
8
|
+
rewrite_document src
|
9
|
+
end
|
10
|
+
|
11
|
+
def rewrite_document(doc)
|
12
|
+
rewrite_nodes doc.css('body img'),'src'
|
13
|
+
rewrite_nodes doc.css('script'),'src'
|
14
|
+
rewrite_nodes doc.css('link'),'href'
|
15
|
+
end
|
16
|
+
|
17
|
+
def rewrite_nodes(nodes,attr)
|
18
|
+
nodes.each do |node|
|
19
|
+
next unless node.attr(attr)
|
20
|
+
node_uri = URI.parse(node.attr(attr))
|
21
|
+
node.attribute(attr).value = rewrite_url(node_uri).to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def rewrite_url(uri)
|
26
|
+
unless uri.absolute?
|
27
|
+
uri.scheme = @request.scheme
|
28
|
+
if @request.host =~ /:/
|
29
|
+
host,port = @request.host.split(":")
|
30
|
+
uri.host = host
|
31
|
+
uri.port = port
|
32
|
+
else
|
33
|
+
uri.host = @request.host
|
34
|
+
uri.port = @request.port
|
35
|
+
end
|
36
|
+
end
|
37
|
+
uri
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/slimmer/version.rb
CHANGED
@@ -6,7 +6,9 @@
|
|
6
6
|
<body>
|
7
7
|
<div class="content">
|
8
8
|
<header><h1>I AM A TITLE</h1></header>
|
9
|
-
<
|
9
|
+
<div class="header-context">
|
10
|
+
<nav role="navigation"><ol><li>MySite</li></ol></nav>
|
11
|
+
</div>
|
10
12
|
<div id="wrapper" class="group">
|
11
13
|
</div>
|
12
14
|
</div>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class HeaderContextInserterTest < MiniTest::Unit::TestCase
|
4
|
+
def test_should_replace_contents_of_header_context_in_template
|
5
|
+
template = as_nokogiri %{
|
6
|
+
<html><body><div><div class="header-context"></div></div></body></html>
|
7
|
+
}
|
8
|
+
source = as_nokogiri %{
|
9
|
+
<html><body><nav></nav><div class="header-context"><p>this should be moved</p></div></body></html>
|
10
|
+
}
|
11
|
+
|
12
|
+
Slimmer::HeaderContextInserter.new.filter(source, template)
|
13
|
+
assert_in template, ".header-context", %{<p>this should be moved</p>}
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_should_replace_classes_of_header_context_in_template
|
17
|
+
template = as_nokogiri %{
|
18
|
+
<html><body><div><div class="header-context template-class"></div></div></body></html>
|
19
|
+
}
|
20
|
+
source = as_nokogiri %{
|
21
|
+
<html><body><nav></nav><div class="header-context app-class"><p>this should be moved</p></div></body></html>
|
22
|
+
}
|
23
|
+
|
24
|
+
Slimmer::HeaderContextInserter.new.filter(source, template)
|
25
|
+
assert_in template, ".header-context.app-class", %{<p>this should be moved</p>}
|
26
|
+
assert_not_in template, ".header-context.template-class"
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_should_do_nothing_if_no_header_context_was_provided
|
30
|
+
template = as_nokogiri %{
|
31
|
+
<html><body><div><div class="header-context">should not be removed</div></div></body></html>
|
32
|
+
}
|
33
|
+
source = as_nokogiri %{
|
34
|
+
<html><body><nav></nav></body></html>
|
35
|
+
}
|
36
|
+
|
37
|
+
Slimmer::HeaderContextInserter.new.filter(source, template)
|
38
|
+
assert_in template, ".header-context", %{should not be removed}
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_should_do_nothing_if_no_header_context_was_present_in_the_template
|
42
|
+
template = as_nokogiri %{
|
43
|
+
<html><body><div></div></body></html>
|
44
|
+
}
|
45
|
+
source = as_nokogiri %{
|
46
|
+
<html><body><div><div class="header-context">should be ignored</div></div></body></html>
|
47
|
+
}
|
48
|
+
|
49
|
+
Slimmer::HeaderContextInserter.new.filter(source, template) # should not raise
|
50
|
+
end
|
51
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -13,6 +13,10 @@ class MiniTest::Unit::TestCase
|
|
13
13
|
def assert_in(template, selector, content, message=nil)
|
14
14
|
assert_equal content, template.at_css(selector).inner_html.to_s, message
|
15
15
|
end
|
16
|
+
|
17
|
+
def assert_not_in(template, selector, message="didn't exist to find #{selector}")
|
18
|
+
refute template.at_css(selector), message
|
19
|
+
end
|
16
20
|
end
|
17
21
|
|
18
22
|
class SlimmerIntegrationTest < MiniTest::Unit::TestCase
|
data/test/typical_usage_test.rb
CHANGED
@@ -11,7 +11,6 @@ module TypicalUsage
|
|
11
11
|
end
|
12
12
|
|
13
13
|
class NormalResponseTest < SlimmerIntegrationTest
|
14
|
-
|
15
14
|
given_response 200, %{
|
16
15
|
<html>
|
17
16
|
<head><title>The title of the page</title>
|
@@ -56,6 +55,28 @@ module TypicalUsage
|
|
56
55
|
end
|
57
56
|
end
|
58
57
|
|
58
|
+
class HeaderContextResponseTest < SlimmerIntegrationTest
|
59
|
+
given_response 200, %{
|
60
|
+
<html>
|
61
|
+
<head><title>The title of the page</title>
|
62
|
+
<meta name="something" content="yes">
|
63
|
+
<meta name="x-section-name" content="This section">
|
64
|
+
<meta name="x-section-link" content="/this_section">
|
65
|
+
<script src="blah.js"></script>
|
66
|
+
<link href="app.css" rel="stylesheet" type="text/css">
|
67
|
+
</head>
|
68
|
+
<body class="body_class">
|
69
|
+
<div class="header-context custom-class">app-specific header context</div>
|
70
|
+
<div id="wrapper">The body of the page</div>
|
71
|
+
</body>
|
72
|
+
</html>
|
73
|
+
}
|
74
|
+
|
75
|
+
def test_should_replace_the_header_context_using_the_app_response
|
76
|
+
assert_rendered_in_template ".header-context.custom-class", "app-specific header context"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
59
80
|
class Error500ResponseTest < SlimmerIntegrationTest
|
60
81
|
include Rack::Test::Methods
|
61
82
|
|
metadata
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slimmer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
5
|
-
prerelease:
|
4
|
+
version: 1.1.4beta2
|
5
|
+
prerelease: 5
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Ben Griffiths
|
@@ -10,11 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2011-11-
|
13
|
+
date: 2011-11-29 00:00:00.000000000Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: nokogiri
|
17
|
-
requirement: &
|
17
|
+
requirement: &70348230280000 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ~>
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: 1.5.0
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *70348230280000
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rack
|
28
|
-
requirement: &
|
28
|
+
requirement: &70348230276740 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ~>
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: 1.3.5
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *70348230276740
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: plek
|
39
|
-
requirement: &
|
39
|
+
requirement: &70348230274580 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
42
42
|
- - ! '>='
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
version: 0.1.8
|
45
45
|
type: :runtime
|
46
46
|
prerelease: false
|
47
|
-
version_requirements: *
|
47
|
+
version_requirements: *70348230274580
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: rake
|
50
|
-
requirement: &
|
50
|
+
requirement: &70348230264040 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
53
53
|
- - ~>
|
@@ -55,10 +55,10 @@ dependencies:
|
|
55
55
|
version: 0.9.2.2
|
56
56
|
type: :development
|
57
57
|
prerelease: false
|
58
|
-
version_requirements: *
|
58
|
+
version_requirements: *70348230264040
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: rack-test
|
61
|
-
requirement: &
|
61
|
+
requirement: &70348230253520 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
64
64
|
- - ~>
|
@@ -66,10 +66,10 @@ dependencies:
|
|
66
66
|
version: 0.6.1
|
67
67
|
type: :development
|
68
68
|
prerelease: false
|
69
|
-
version_requirements: *
|
69
|
+
version_requirements: *70348230253520
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: mocha
|
72
|
-
requirement: &
|
72
|
+
requirement: &70348230212940 !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
75
|
- - ~>
|
@@ -77,10 +77,10 @@ dependencies:
|
|
77
77
|
version: 0.9.12
|
78
78
|
type: :development
|
79
79
|
prerelease: false
|
80
|
-
version_requirements: *
|
80
|
+
version_requirements: *70348230212940
|
81
81
|
- !ruby/object:Gem::Dependency
|
82
82
|
name: webmock
|
83
|
-
requirement: &
|
83
|
+
requirement: &70348230198760 !ruby/object:Gem::Requirement
|
84
84
|
none: false
|
85
85
|
requirements:
|
86
86
|
- - ~>
|
@@ -88,7 +88,7 @@ dependencies:
|
|
88
88
|
version: '1.7'
|
89
89
|
type: :development
|
90
90
|
prerelease: false
|
91
|
-
version_requirements: *
|
91
|
+
version_requirements: *70348230198760
|
92
92
|
description: Rack middleware for skinning pages using a specific template
|
93
93
|
email:
|
94
94
|
- bengriffiths@gmail.com
|
@@ -99,8 +99,20 @@ extra_rdoc_files: []
|
|
99
99
|
files:
|
100
100
|
- README.md
|
101
101
|
- CHANGELOG.md
|
102
|
+
- lib/slimmer/admin_title_inserter.rb
|
103
|
+
- lib/slimmer/app.rb
|
104
|
+
- lib/slimmer/body_class_copier.rb
|
105
|
+
- lib/slimmer/body_inserter.rb
|
106
|
+
- lib/slimmer/footer_remover.rb
|
107
|
+
- lib/slimmer/header_context_inserter.rb
|
102
108
|
- lib/slimmer/railtie.rb
|
109
|
+
- lib/slimmer/section_inserter.rb
|
110
|
+
- lib/slimmer/skin.rb
|
111
|
+
- lib/slimmer/tag_mover.rb
|
103
112
|
- lib/slimmer/template.rb
|
113
|
+
- lib/slimmer/test.rb
|
114
|
+
- lib/slimmer/title_inserter.rb
|
115
|
+
- lib/slimmer/url_rewriter.rb
|
104
116
|
- lib/slimmer/version.rb
|
105
117
|
- lib/slimmer.rb
|
106
118
|
- lib/tasks/slimmer.rake
|
@@ -109,6 +121,7 @@ files:
|
|
109
121
|
- test/fixtures/500.html.erb
|
110
122
|
- test/fixtures/wrapper.html.erb
|
111
123
|
- test/processors/body_inserter_test.rb
|
124
|
+
- test/processors/header_context_inserter_test.rb
|
112
125
|
- test/slimmer_test.rb
|
113
126
|
- test/test_helper.rb
|
114
127
|
- test/typical_usage_test.rb
|
@@ -127,9 +140,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
140
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
141
|
none: false
|
129
142
|
requirements:
|
130
|
-
- - ! '
|
143
|
+
- - ! '>'
|
131
144
|
- !ruby/object:Gem::Version
|
132
|
-
version:
|
145
|
+
version: 1.3.1
|
133
146
|
requirements: []
|
134
147
|
rubyforge_project: slimmer
|
135
148
|
rubygems_version: 1.8.10
|
@@ -141,6 +154,7 @@ test_files:
|
|
141
154
|
- test/fixtures/500.html.erb
|
142
155
|
- test/fixtures/wrapper.html.erb
|
143
156
|
- test/processors/body_inserter_test.rb
|
157
|
+
- test/processors/header_context_inserter_test.rb
|
144
158
|
- test/slimmer_test.rb
|
145
159
|
- test/test_helper.rb
|
146
160
|
- test/typical_usage_test.rb
|