ress 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +151 -0
- data/Rakefile +1 -0
- data/lib/generators/ress/install_generator.rb +17 -0
- data/lib/generators/ress/templates/README +19 -0
- data/lib/generators/ress/templates/ress.rb +4 -0
- data/lib/ress.rb +33 -0
- data/lib/ress/alternate_version.rb +40 -0
- data/lib/ress/canonical_version.rb +38 -0
- data/lib/ress/category_collection.rb +23 -0
- data/lib/ress/controller_additions.rb +32 -0
- data/lib/ress/engine.rb +6 -0
- data/lib/ress/version.rb +3 -0
- data/lib/ress/view_helpers.rb +21 -0
- data/ress.gemspec +23 -0
- data/spec/ress/alternate_version_spec.rb +80 -0
- data/spec/ress/canonical_version_spec.rb +75 -0
- data/spec/ress/category_collection_spec.rb +40 -0
- data/spec/ress/controller_additions_spec.rb +72 -0
- data/spec/ress/view_helpers_spec.rb +49 -0
- data/spec/ress_spec.rb +38 -0
- data/vendor/assets/javascripts/modernizr.js +4 -0
- data/vendor/assets/javascripts/ress.js +168 -0
- metadata +109 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm gemset use ress
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
+
===============================================================================
|
data/lib/ress.rb
ADDED
@@ -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
|
data/lib/ress/engine.rb
ADDED
data/lib/ress/version.rb
ADDED
@@ -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
|
data/ress.gemspec
ADDED
@@ -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
|
data/spec/ress_spec.rb
ADDED
@@ -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=["­","<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
|