ress 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm gemset use ress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ress.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Matthew Robertson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,151 @@
1
+ # Ress
2
+
3
+ Ress implements a set of best practices that allow you to progressively
4
+ enhance you Rails application to improve the user experience on mobile
5
+ devices.
6
+
7
+ ## Background
8
+
9
+ Ress is an extension of the [devicejs](https://github.com/borismus/device.js)
10
+ library written by [Boris Smus](http://smus.com/). It adds a back end for
11
+ adapting server responses based on client side feature detection. Ress allows
12
+ you to specify alternate versions of your website, along with media queries
13
+ for which devices should be redirected to which version.
14
+
15
+ ## How it Works
16
+
17
+ ### HTML Annotations
18
+
19
+ When you register alternate mobile versions of your website, Ress adds annotations
20
+ to the `<head>` of your document that describes where these pages are located and
21
+ which devices should be redirected to them.
22
+
23
+ For example, a typical alternate version for a mobile site might include a tag
24
+ like this:
25
+
26
+ ```html
27
+ <link rel="alternate" media="only screen and (max-width: 640px)" href="http://m.example.com/page-1" >
28
+ ```
29
+
30
+ The mobile version of the page would then have a link pointing back the canonical
31
+ version:
32
+
33
+ ```html
34
+ <link rel="canonical" href="http://www.example.com/page-1" >
35
+ ```
36
+
37
+ These annotations conform to SEO best practices for mobile optimized websites
38
+ as documented by Google
39
+ [here](https://developers.google.com/webmasters/smartphone-sites/details).
40
+
41
+ ### Feature Detection
42
+
43
+ semantic, media query-based device detection
44
+
45
+ Device.js will read all of the version links in your markup, and
46
+ redirect you to the appropriate URL that serves the correct version of
47
+ your webapp.
48
+
49
+ When a request comes into your site, ress runs a some javascript if there is an
50
+ alternate version available that matches the client. If there is, the user is
51
+ redirected to that site.
52
+
53
+ ### Server Side Components
54
+
55
+ Ress allows you to customize how your Rails application responds to requests in
56
+ two ways:
57
+
58
+ 1. It adds controller and helper methods to detect which version of your site has
59
+ been requested. This is useful for small tweeks in html or behaviour, eg:
60
+
61
+ ```erb
62
+ <% if mobile_request? %>
63
+ <%= image_tag 'low-res.png' %>
64
+ <% else %>
65
+ <%= image_tag 'high-res.png' %>
66
+ <% end %>
67
+ ```
68
+
69
+ 2. It prepends a view path for each alternate version of your site, so that you can
70
+ override which templates or partials are rendered for certain requests. For example if
71
+ you want to render a different html form for creating users on the mobile version of your
72
+ site you could create `app/mobile_views/users/_form.html.erb` and Ress would have Rails
73
+ select that template over `app/views/users/_form.html.erb` when a request comes in to the
74
+ mobile version.
75
+
76
+
77
+ ## Installation
78
+
79
+ Add this line to your application's Gemfile:
80
+
81
+ gem 'ress'
82
+
83
+ And then execute:
84
+
85
+ $ bundle install
86
+
87
+ Run the installation generator:
88
+
89
+ $ rails g ress:install
90
+
91
+ ## Usage
92
+
93
+ ### Version override
94
+
95
+ You can manually override the detector and load a particular version of
96
+ the site by passing in the `device` GET parameter with the ID of the
97
+ version you'd like to load. This will look up the `link` tag based on
98
+ the specified ID and load that version. For example, if you are on
99
+ desktop but want the tablet version, visiting
100
+ `http://foo.com/?version=tablet` will redirect to the tablet version at
101
+ `http://tablet.foo.com`.
102
+
103
+ Relatedly, you can prevent redirection completely, by specifying the
104
+ `force=1` GET parameter. For example, if you are on desktop and know the
105
+ URL of the tablet site, you can load `http://tablet.foo.com/?force=1`.
106
+
107
+ ```html
108
+ <!-- Include a way to manually switch between device types -->
109
+ <footer>
110
+ <ul>
111
+ <li><a href="?device=desktop">Desktop</a></li>
112
+ <li><a href="?device=tablet">Tablet</a></li>
113
+ <li><a href="?device=phone">Phone</a></li>
114
+ </ul>
115
+ </footer>
116
+ ```
117
+
118
+ ### Dependencies
119
+
120
+ TODO: Modernizr
121
+
122
+
123
+ ## Performance considerations
124
+
125
+ The javascript included by Ress does some checks and will use client-side
126
+ redirection to point users to the right version of your webapp. Client-side
127
+ redirection can have a performance overhead (though I haven't measured it).
128
+ If you find this is true, you can keep your DOM the same, still using the
129
+ SEO-friendly `<link rel="alternate">` tags, but simply remove the
130
+ device.js script and do your own server-side UA-based pushing.
131
+
132
+ ## Browser support
133
+
134
+ Device.js should work in all browsers that support
135
+ `document.querySelectorAll`. Notably, this excludes IE7. If you want it
136
+ to work in IE7 and below, please include a [polyfill](https://gist.github.com/2724353).
137
+
138
+ ## Contributing
139
+
140
+ The goal of Ress is to provide a SEO-compatible best practice and
141
+ starting point for reliable cross-device, cross-browser redirection.
142
+
143
+ Given how many browsers and devices we have these days, there are bound
144
+ to be bugs. If you find them, please report them and (ideally) fix them
145
+ in a pull request.
146
+
147
+ 1. Fork it
148
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
149
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
150
+ 4. Push to the branch (`git push origin my-new-feature`)
151
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ module Ress
2
+ module Generators
3
+ class InstallGenerator < ::Rails::Generators::Base
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ desc "Creates a Ress initializer for your application."
7
+
8
+ def copy_initializer
9
+ template "ress.rb", "config/initializers/ress.rb"
10
+ end
11
+
12
+ def show_readme
13
+ readme "README" if behavior == :invoke
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ ===============================================================================
2
+
3
+ There is some setup you must do manually if you haven't yet:
4
+
5
+ 1. Include the Javascript files in your application.js:
6
+
7
+ //= require ress
8
+
9
+ 2. Call Ress in the head of your layout to render out the version annotations.
10
+ For example:
11
+
12
+ <head>
13
+ <title>My App</title>
14
+ <%= stylesheet_link_tag "application", :media => "all" %>
15
+ <%= ress_link_tags %>
16
+ <%= csrf_meta_tags %>
17
+ </head>
18
+
19
+ ===============================================================================
@@ -0,0 +1,4 @@
1
+ Ress.configure do |config|
2
+
3
+
4
+ end
@@ -0,0 +1,33 @@
1
+ require "action_view"
2
+
3
+ require "ress/version"
4
+ require "ress/alternate_version"
5
+ require "ress/canonical_version"
6
+ require "ress/category_collection"
7
+ require "ress/controller_additions"
8
+ require "ress/view_helpers"
9
+
10
+ if defined? Rails
11
+ require "ress/engine"
12
+ end
13
+
14
+ module Ress
15
+ extend self
16
+
17
+ def category_collection
18
+ @categories ||= CategoryCollection.new
19
+ end
20
+
21
+ def canonical_version
22
+ category_collection.canonical_version
23
+ end
24
+
25
+ def alternate_versions
26
+ category_collection.alternate_versions
27
+ end
28
+
29
+ def configure
30
+ yield(category_collection)
31
+ end
32
+
33
+ end
@@ -0,0 +1,40 @@
1
+ module Ress
2
+
3
+ class AlternateVersion
4
+
5
+ attr_reader :name, :subdomain, :media_query, :view_path
6
+
7
+ def initialize(name, media_query, options = {})
8
+ @name = name
9
+ @media_query = media_query
10
+ @subdomain = options.fetch(:subdomain, name)
11
+ @view_path = options.fetch(:view_path, default_view_path)
12
+ end
13
+
14
+ def matches?(subdomain)
15
+ self.subdomain == subdomain.split('.').first
16
+ end
17
+
18
+ # Create a tag of this format:
19
+ # `<link rel="alternate" href="http://foo.com" id="desktop" media="only screen and (touch-enabled: 0)">`
20
+ def link_tag(protocol, base_url, view)
21
+ view.tag :link, :rel => 'alternate', :href => href(protocol, base_url), :id => id, :media => media_query
22
+ end
23
+
24
+ private
25
+
26
+ def default_view_path
27
+ File.join('app', "#{name}_views")
28
+ end
29
+
30
+ def href(protocol, base_url)
31
+ "#{protocol}#{subdomain}.#{base_url}"
32
+ end
33
+
34
+ def id
35
+ name
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,38 @@
1
+ module Ress
2
+
3
+ class CanonicalVersion
4
+
5
+ attr_reader :subdomain
6
+
7
+ def initialize(options = {})
8
+ @subdomain = options.fetch(:subdomain, false)
9
+ end
10
+
11
+ def matches?(subdomain)
12
+ if self.subdomain
13
+ self.subdomain == subdomain
14
+ else
15
+ subdomain.empty?
16
+ end
17
+ end
18
+
19
+ # Create a tag of this format:
20
+ # `<link rel="canonical" href="http://www.example.com/page-1" >`
21
+ def link_tag(protocol, fullpath, subdomain, view)
22
+ view.tag :link, :rel => 'canonical', :href => href(protocol, fullpath, subdomain)
23
+ end
24
+
25
+ private
26
+
27
+ def href(protocol, fullpath, subdomain)
28
+ fullpath = fullpath[(subdomain.length + 1)..-1] unless subdomain.empty?
29
+ if self.subdomain
30
+ "#{protocol}#{self.subdomain}.#{fullpath}"
31
+ else
32
+ "#{protocol}#{fullpath}"
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,23 @@
1
+ module Ress
2
+
3
+ class CategoryCollection
4
+
5
+ attr_reader :canonical_version, :alternate_versions
6
+
7
+ def initialize
8
+ @alternate_versions = []
9
+ @canonical_version = CanonicalVersion.new
10
+ end
11
+
12
+ def set_canonical(options = {})
13
+ @canonical_version = CanonicalVersion.new(options)
14
+ end
15
+
16
+ def add_alternate(options)
17
+ version = AlternateVersion.new(options.delete(:name), options.delete(:media_type), options)
18
+ alternate_versions << version
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,32 @@
1
+ module Ress
2
+
3
+ # This module is automatically included into all controllers.
4
+ module ControllerAdditions
5
+ module ClassMethods
6
+ # class methods go here
7
+ end
8
+
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ base.helper_method :canonical_request?
12
+ base.before_filter :prepend_category_view_path
13
+ end
14
+
15
+ def prepend_category_view_path
16
+ Ress.alternate_versions.each do |cat|
17
+ prepend_view_path(cat.view_path) if cat.matches?(request.subdomain)
18
+ end
19
+ end
20
+
21
+ def canonical_request?
22
+ Ress.canonical_version.matches?(request.subdomain)
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ if defined? ActionController::Base
29
+ ActionController::Base.class_eval do
30
+ include Ress::ControllerAdditions
31
+ end
32
+ end
@@ -0,0 +1,6 @@
1
+ module Ress
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Ress
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,21 @@
1
+ module Ress
2
+
3
+ module ViewHelpers
4
+
5
+ def ress_link_tags
6
+ path = "#{request.host_with_port}#{request.fullpath}"
7
+ if canonical_request?
8
+ Ress.alternate_versions.map do |category|
9
+ category.link_tag(request.protocol, path, self)
10
+ end.join.html_safe
11
+ else
12
+ Ress.canonical_version.link_tag(request.protocol, path, request.subdomain, self)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+
21
+ ActionView::Base.send :include, Ress::ViewHelpers
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ress/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "ress"
8
+ gem.version = Ress::VERSION
9
+ gem.authors = ["Matthew Robertson"]
10
+ gem.email = ["matthewrobertson03@gmail.com"]
11
+ gem.description = %q{Progressively enhance the mobile user experience of your Rails application.}
12
+ gem.summary = %q{Progressively enhance the mobile user experience of your Rails application.}
13
+ gem.homepage = "https://github.com/matthewrobertson/ress"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency("actionpack", "~> 3.0")
21
+
22
+ gem.add_development_dependency("rspec")
23
+ end
@@ -0,0 +1,80 @@
1
+ require_relative '../../lib/ress/alternate_version'
2
+
3
+ describe Ress::AlternateVersion do
4
+
5
+ describe '#subdomain' do
6
+
7
+ it 'defaults to the name of the category' do
8
+ category = Ress::AlternateVersion.new('mobile', 'some media query')
9
+ category.subdomain.should == 'mobile'
10
+ end
11
+
12
+ it 'can be overridden by an optional parameter' do
13
+ category = Ress::AlternateVersion.new('mobile', 'some media query', :subdomain => 'foo')
14
+ category.subdomain.should == 'foo'
15
+ end
16
+
17
+ end
18
+
19
+ describe '#view_path' do
20
+
21
+ it 'defaults to app/[NAME]_views' do
22
+ category = Ress::AlternateVersion.new('mobile', 'some media query')
23
+ category.view_path.should == 'app/mobile_views'
24
+ end
25
+
26
+ it 'can be overridden by an optional parameter' do
27
+ category = Ress::AlternateVersion.new('mobile', 'some media query', :view_path => 'foo')
28
+ category.view_path.should == 'foo'
29
+ end
30
+
31
+ end
32
+
33
+ describe '#matches?' do
34
+
35
+ let(:category) { Ress::AlternateVersion.new('mobile', 'some media query', :subdomain => 'foo') }
36
+
37
+ it 'returns true if the subdomain matches' do
38
+ category.matches?('foo').should be_true
39
+ end
40
+
41
+ it 'returns false if the subdomain does not match' do
42
+ category.matches?('bar').should be_false
43
+ end
44
+
45
+ context 'when there are multiple subdomains' do
46
+
47
+ it 'returns true if the first subdomain matches' do
48
+ category.matches?('foo.bar').should be_true
49
+ end
50
+
51
+ it 'returns false if the first subdomain does not match' do
52
+ category.matches?('bar.baz').should be_false
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
59
+ describe '#link_tag' do
60
+
61
+ let(:category) { Ress::AlternateVersion.new('mobile', 'some media query') }
62
+ let(:view) { stub('view') }
63
+
64
+ def link_tag(base_url, protocol, view)
65
+ view.tag :link, :rel => 'alternate', :href => href(base_url, protocol), :id => id, :media => media_query
66
+ end
67
+
68
+ it 'passes args to the view tag helper' do
69
+ view.should_receive(:tag).with(:link, {
70
+ :rel => "alternate",
71
+ :href => "http://mobile.foo.com",
72
+ :id => "mobile",
73
+ :media => "some media query"
74
+ }).and_return('<link>')
75
+ category.link_tag('http://', 'foo.com', view).should == '<link>'
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,75 @@
1
+ require_relative '../../lib/ress/canonical_version'
2
+
3
+ describe Ress::CanonicalVersion do
4
+
5
+ describe '#subdomain' do
6
+
7
+ it 'defaults to false' do
8
+ category = Ress::CanonicalVersion.new
9
+ category.subdomain.should == false
10
+ end
11
+
12
+ it 'can be overridden by an optional parameter' do
13
+ category = Ress::CanonicalVersion.new(:subdomain => 'foo')
14
+ category.subdomain.should == 'foo'
15
+ end
16
+
17
+ end
18
+
19
+ describe '#matches?' do
20
+
21
+ context 'with no canonical subdomain' do
22
+ let(:version) { Ress::CanonicalVersion.new }
23
+
24
+ it 'returns true if there is no subdomain' do
25
+ version.matches?('').should be_true
26
+ end
27
+
28
+ it 'returns false if there is a subdomain' do
29
+ version.matches?('blah').should be_false
30
+ end
31
+
32
+ end
33
+
34
+ context 'with a canonical subdomain' do
35
+
36
+ let(:version) { Ress::CanonicalVersion.new(:subdomain => 'foo') }
37
+
38
+ it 'returns true if the subdomain matches the configured' do
39
+ version.matches?('foo').should be_true
40
+ end
41
+
42
+ it 'returns false if the subdomain does not match' do
43
+ version.matches?('bar').should be_false
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ describe '#link_tag' do
51
+
52
+ let(:view) { stub('view') }
53
+
54
+
55
+ it 'constructs the correct url when there is no canonical subdomain ' do
56
+ version = Ress::CanonicalVersion.new
57
+ view.should_receive(:tag).with(:link, {
58
+ :rel => "canonical",
59
+ :href => "http://foo.com"
60
+ }).and_return('<link>')
61
+ version.link_tag('http://', 'm.foo.com', 'm', view).should == '<link>'
62
+ end
63
+
64
+ it 'constructs the correct url when there is a canonical subdomain ' do
65
+ version = Ress::CanonicalVersion.new(:subdomain => 'secure')
66
+ view.should_receive(:tag).with(:link, {
67
+ :rel => "canonical",
68
+ :href => "http://secure.foo.com"
69
+ }).and_return('<link>')
70
+ version.link_tag('http://', 'm.secure.foo.com', 'm.secure', view).should == '<link>'
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../../lib/ress'
2
+
3
+ describe Ress::CategoryCollection do
4
+
5
+ let(:collection) { Ress::CategoryCollection.new }
6
+
7
+ describe '#add_alternate' do
8
+
9
+ it 'adds an item to the alternate_versions' do
10
+ expect {
11
+ collection.add_alternate({:name => 'stuff', :media_query => 'foo'})
12
+ }.to change { collection.alternate_versions.size }.by(1)
13
+ end
14
+
15
+ it 'sets the name and media_query of the alternate_version' do
16
+ collection.add_alternate({:name => 'stuff', :media_query => 'foo'})
17
+ collection.alternate_versions.last.name.should == 'stuff'
18
+ end
19
+
20
+ it 'passes options to the alternate_version' do
21
+ collection.add_alternate({:name => 'stuff', :media_query => 'foo', :subdomain => 'm'})
22
+ collection.alternate_versions.last.subdomain.should == 'm'
23
+ end
24
+
25
+ end
26
+
27
+ describe 'canonical_version' do
28
+
29
+ it 'defaults to a canonical_version with no subdomain' do
30
+ collection.canonical_version.subdomain.should be_false
31
+ end
32
+
33
+ it 'can be altered through set_canonical' do
34
+ collection.set_canonical :subdomain => 'foo'
35
+ collection.canonical_version.subdomain.should == 'foo'
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,72 @@
1
+ require_relative '../../lib/ress'
2
+
3
+ class ActionControllerStub
4
+
5
+ attr_accessor :request
6
+
7
+ def self.before_filter(action)
8
+ @@action = action
9
+ end
10
+
11
+ def self.action
12
+ @@action
13
+ end
14
+
15
+ def self.helper_method(*splat) ; end
16
+
17
+ end
18
+
19
+ ActionControllerStub.class_eval do
20
+ include Ress::ControllerAdditions
21
+ end
22
+
23
+ describe Ress::ControllerAdditions do
24
+
25
+ it 'adds a before_filter to all actions when it is included' do
26
+ ActionControllerStub.action.should == :prepend_category_view_path
27
+ end
28
+
29
+ describe '#prepend_category_view_path' do
30
+
31
+ let(:controller) { ActionControllerStub.new }
32
+
33
+ before do
34
+ request = stub('request', :subdomain => 'foo')
35
+ controller.request = request
36
+ end
37
+
38
+ it 'prepends view paths of matching alternate_versions' do
39
+ category = stub('category', :matches? => true, :view_path => 'foo/bar')
40
+ Ress.stub(:alternate_versions => [category])
41
+
42
+ controller.should_receive(:prepend_view_path).with('foo/bar')
43
+ controller.prepend_category_view_path
44
+ end
45
+
46
+ it 'does not prepend view paths of alternate_versions that dont match' do
47
+ category = stub('category', :matches? => false, :view_path => 'foo/bar')
48
+ Ress.stub(:alternate_versions => [category])
49
+
50
+ controller.should_not_receive(:prepend_view_path)
51
+ controller.prepend_category_view_path
52
+ end
53
+
54
+ end
55
+
56
+ describe '#canonical_request?' do
57
+
58
+ let(:controller) { ActionControllerStub.new }
59
+
60
+ before do
61
+ request = stub(:subdomain => 'foo')
62
+ controller.stub(:request => request)
63
+ end
64
+
65
+ it 'returns true if the request subdomain matches the canonical' do
66
+ Ress.canonical_version.stub(:matches? => true)
67
+ controller.canonical_request?.should be_true
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,49 @@
1
+ require 'rspec'
2
+ require_relative '../../lib/ress'
3
+
4
+ describe ActionView::Base do
5
+
6
+ describe '#ress_link_tags' do
7
+
8
+ let(:view) { ActionView::Base.new }
9
+ let(:request) { stub('request', :protocol => 'http://', :host_with_port => 'x.foo.com', :fullpath => '/bar', :subdomain => 'x') }
10
+ let(:category) { Ress::AlternateVersion.new('m', 'stuff') }
11
+
12
+ before do
13
+ view.stub(:request => request)
14
+ end
15
+
16
+ context 'alternate request' do
17
+
18
+ before { view.stub(:canonical_request? => false) }
19
+
20
+ it 'returns the canonical link' do
21
+ view.ress_link_tags.should == "<link href=\"http://foo.com/bar\" rel=\"canonical\" />"
22
+ end
23
+
24
+ end
25
+
26
+ context 'canonical request' do
27
+
28
+ let(:request) { stub('request', :protocol => 'http://', :host_with_port => 'foo.com', :fullpath => '/bar', :subdomain => '') }
29
+ before { view.stub(:canonical_request? => true) }
30
+
31
+ it 'returns an empty string if there are no registered categories' do
32
+ view.ress_link_tags.should == ''
33
+ end
34
+
35
+ it 'generates the link tags when there is one category' do
36
+
37
+ Ress.configure do |r|
38
+ r.add_alternate :name => 'm', :media_type => 'stuff'
39
+ end
40
+
41
+ view.ress_link_tags.should ==
42
+ "<link href=\"http://m.foo.com/bar\" id=\"m\" media=\"stuff\" rel=\"alternate\" />"
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../lib/ress'
2
+
3
+ describe Ress do
4
+
5
+ describe '.configure' do
6
+
7
+ it 'yields the default category collection' do
8
+ Ress.configure { |r| r.should be_a(Ress::CategoryCollection) }
9
+ end
10
+
11
+ end
12
+
13
+ describe '.canonical_version' do
14
+
15
+ it 'returns a Ress::CanonicalVersion' do
16
+ Ress.canonical_version.should be_a(Ress::CanonicalVersion)
17
+ end
18
+
19
+ it 'can be altered through Ress.configure' do
20
+ Ress.configure { |r| r.set_canonical :subdomain => 'foo' }
21
+ Ress.canonical_version.subdomain.should == 'foo'
22
+ end
23
+
24
+ end
25
+
26
+ describe '.alternate_versions' do
27
+
28
+ it 'can be altered through Ress.configure' do
29
+ expect {
30
+ Ress.configure { |r| r.add_alternate :name => 'foo' }
31
+ }.to change {
32
+ Ress.alternate_versions.length
33
+ }.by(1)
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,4 @@
1
+ /* Modernizr 2.5.3 (Custom Build) | MIT & BSD
2
+ * Build: http://modernizr.com/download/#-touch-mq-teststyles-prefixes
3
+ */
4
+ ;window.Modernizr=function(a,b,c){function w(a){i.cssText=a}function x(a,b){return w(l.join(a+";")+(b||""))}function y(a,b){return typeof a===b}function z(a,b){return!!~(""+a).indexOf(b)}function A(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:y(f,"function")?f.bind(d||b):f}return!1}var d="2.5.3",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l=" -webkit- -moz- -o- -ms- ".split(" "),m={},n={},o={},p=[],q=p.slice,r,s=function(a,c,d,e){var h,i,j,k=b.createElement("div"),l=b.body,m=l?l:b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:g+(d+1),k.appendChild(j);return h=["&#173;","<style>",a,"</style>"].join(""),k.id=g,(l?k:m).innerHTML+=h,m.appendChild(k),l||(m.style.background="",f.appendChild(m)),i=c(k,a),l?k.parentNode.removeChild(k):m.parentNode.removeChild(m),!!i},t=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return s("@media "+b+" { #"+g+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},u={}.hasOwnProperty,v;!y(u,"undefined")&&!y(u.call,"undefined")?v=function(a,b){return u.call(a,b)}:v=function(a,b){return b in a&&y(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e});var B=function(c,d){var f=c.join(""),g=d.length;s(f,function(c,d){var f=b.styleSheets[b.styleSheets.length-1],h=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"",i=c.childNodes,j={};while(g--)j[i[g].id]=i[g];e.touch="ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch||(j.touch&&j.touch.offsetTop)===9},g,d)}([,["@media (",l.join("touch-enabled),("),g,")","{#touch{top:9px;position:absolute}}"].join("")],[,"touch"]);m.touch=function(){return e.touch};for(var C in m)v(m,C)&&(r=C.toLowerCase(),e[r]=m[C](),p.push((e[r]?"":"no-")+r));return w(""),h=j=null,e._version=d,e._prefixes=l,e.mq=t,e.testStyles=s,e}(this,this.document);
@@ -0,0 +1,168 @@
1
+ /**
2
+ * This file has been adapted from device.js:
3
+ * https://github.com/borismus/device.js
4
+ */
5
+ (function(exports) {
6
+
7
+ var MQ_TOUCH = /\(touch-enabled: (.*?)\)/;
8
+ var FORCE_COOKIE = 'force-cannonical';
9
+
10
+ /**
11
+ * Class responsible for deciding which version of the application to
12
+ * load.
13
+ */
14
+ function VersionManager() {
15
+ // Get a list of all versions.
16
+ this.versions = this.getVersions();
17
+ }
18
+
19
+ /**
20
+ * Parse all of the <link> elements in the <head>.
21
+ */
22
+ VersionManager.prototype.getVersions = function() {
23
+ var versions = [];
24
+ // Get all of the link rel alternate elements from the head.
25
+ var links = document.querySelectorAll('head link[rel="alternate"]');
26
+ // For each link element, get href, media and id.
27
+ for (var i = 0; i < links.length; i++) {
28
+ var href = links[i].getAttribute('href');
29
+ var media = links[i].getAttribute('media');
30
+ var id = links[i].getAttribute('id');
31
+ versions.push(new Version(href, media, id));
32
+ }
33
+ // Return an array of Version objects.
34
+ return versions;
35
+ };
36
+
37
+ /**
38
+ * Device which version to load.
39
+ */
40
+ VersionManager.prototype.redirectIfNeeded = function() {
41
+ if(this.shouldNotRedirect()) {
42
+ return;
43
+ }
44
+ var version = this.findVersion(Version.prototype.matches);
45
+ if (version) {
46
+ version.redirect();
47
+ }
48
+ };
49
+
50
+ VersionManager.prototype.shouldNotRedirect = function() {
51
+ return this.versions.length === 0 ||
52
+ this.readCookie(FORCE_COOKIE) === 'true';
53
+ };
54
+
55
+ VersionManager.prototype.readCookie = function() {
56
+ var nameEQ = name + "=";
57
+ var ca = document.cookie.split(';');
58
+ for(var i=0;i < ca.length;i++) {
59
+ var c = ca[i];
60
+ while (c.charAt(0)==' ') c = c.substring(1,c.length);
61
+ if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length);
62
+ }
63
+ return null;
64
+ };
65
+
66
+ VersionManager.prototype.findVersion = function(criteria) {
67
+ // Go through configured versions.
68
+ for (var i = 0; i < this.versions.length; i++) {
69
+ var v = this.versions[i];
70
+ // Check if this version matches based on criteria.
71
+ if (criteria.call(v)) {
72
+ return v;
73
+ }
74
+ }
75
+ return null;
76
+ };
77
+
78
+ function Version(href, mediaQuery, id) {
79
+ this.mediaQuery = mediaQuery;
80
+ this.url = href;
81
+ this.id = id;
82
+ }
83
+
84
+ /**
85
+ * Check if this version matches the current one.
86
+ */
87
+ Version.prototype.matches = function() {
88
+ // Apply a polyfill for the touch-enabled media query (not currently
89
+ // standardized, only implemented in Firefox: http://goo.gl/LrmIa)
90
+ var mqParser = new MQParser(this.mediaQuery);
91
+ return mqParser.evaluate();
92
+ };
93
+
94
+ /**
95
+ * Redirect to the current version.
96
+ */
97
+ Version.prototype.redirect = function() {
98
+ // prevent infinite redirect loops
99
+ if(window.location.href != this.url) {
100
+ window.location.href = this.url;
101
+ }
102
+ };
103
+
104
+
105
+ function MQParser(mq) {
106
+ this.mq = mq;
107
+ this.segments = [];
108
+ this.standardSegments = [];
109
+ this.specialSegments = [];
110
+
111
+ this.parse();
112
+ }
113
+
114
+ MQParser.prototype.parse = function() {
115
+ // Split the Media Query into segments separated by 'and'.
116
+ this.segments = this.mq.split(/\s*and\s*/);
117
+ // Look for segments that contain touch checks.
118
+ for (var i = 0; i < this.segments.length; i++) {
119
+ var seg = this.segments[i];
120
+ // TODO: replace this check with something that checks generally for
121
+ // unknown MQ properties.
122
+ var match = seg.match(MQ_TOUCH);
123
+ if (match) {
124
+ this.specialSegments.push(seg);
125
+ } else {
126
+ // If there's no touch MQ, we're dealing with something standard.
127
+ this.standardSegments.push(seg);
128
+ }
129
+ }
130
+ };
131
+
132
+ /**
133
+ * Check if touch support matches the media query.
134
+ */
135
+ MQParser.prototype.evaluateTouch = function() {
136
+ var out = true;
137
+ for (var i = 0; i < this.specialSegments.length; i++) {
138
+ var match = this.specialSegments[i].match(MQ_TOUCH);
139
+ var touchValue = match[1];
140
+ if (touchValue !== "0" && touchValue !== "1") {
141
+ console.error('Invalid value for "touch-enabled" media query.');
142
+ }
143
+ var touchExpected = parseInt(touchValue, 10) === 1 ? true : false;
144
+ out = out && (touchExpected == Modernizr.touch);
145
+ }
146
+ return out;
147
+ };
148
+
149
+ /**
150
+ * Returns the valid media query (without touch stuff).
151
+ */
152
+ MQParser.prototype.getMediaQuery = function() {
153
+ return this.standardSegments.join(' and ');
154
+ };
155
+
156
+ /**
157
+ * Evaluates the media query with matchMedia.
158
+ */
159
+ MQParser.prototype.evaluate = function() {
160
+ return Modernizr.mq(this.getMediaQuery()) &&
161
+ this.evaluateTouch();
162
+
163
+ };
164
+
165
+ var vermgr = new VersionManager();
166
+ vermgr.redirectIfNeeded();
167
+
168
+ })(window);
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ress
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Matthew Robertson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ none: false
21
+ name: actionpack
22
+ type: :runtime
23
+ prerelease: false
24
+ requirement: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ version: '3.0'
29
+ none: false
30
+ - !ruby/object:Gem::Dependency
31
+ version_requirements: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ! '>='
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ none: false
37
+ name: rspec
38
+ type: :development
39
+ prerelease: false
40
+ requirement: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ none: false
46
+ description: Progressively enhance the mobile user experience of your Rails application.
47
+ email:
48
+ - matthewrobertson03@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - .rvmrc
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - lib/generators/ress/install_generator.rb
60
+ - lib/generators/ress/templates/README
61
+ - lib/generators/ress/templates/ress.rb
62
+ - lib/ress.rb
63
+ - lib/ress/alternate_version.rb
64
+ - lib/ress/canonical_version.rb
65
+ - lib/ress/category_collection.rb
66
+ - lib/ress/controller_additions.rb
67
+ - lib/ress/engine.rb
68
+ - lib/ress/version.rb
69
+ - lib/ress/view_helpers.rb
70
+ - ress.gemspec
71
+ - spec/ress/alternate_version_spec.rb
72
+ - spec/ress/canonical_version_spec.rb
73
+ - spec/ress/category_collection_spec.rb
74
+ - spec/ress/controller_additions_spec.rb
75
+ - spec/ress/view_helpers_spec.rb
76
+ - spec/ress_spec.rb
77
+ - vendor/assets/javascripts/modernizr.js
78
+ - vendor/assets/javascripts/ress.js
79
+ homepage: https://github.com/matthewrobertson/ress
80
+ licenses: []
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ none: false
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ none: false
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 1.8.24
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Progressively enhance the mobile user experience of your Rails application.
103
+ test_files:
104
+ - spec/ress/alternate_version_spec.rb
105
+ - spec/ress/canonical_version_spec.rb
106
+ - spec/ress/category_collection_spec.rb
107
+ - spec/ress/controller_additions_spec.rb
108
+ - spec/ress/view_helpers_spec.rb
109
+ - spec/ress_spec.rb