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 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: []