slimmer 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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: []