apropos 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +14 -0
- data/README.md +94 -0
- data/Rakefile +20 -0
- data/apropos.gemspec +28 -0
- data/doc-src/customization.md +56 -0
- data/lib/apropos.rb +16 -0
- data/lib/apropos/class_list.rb +26 -0
- data/lib/apropos/extension_parser.rb +35 -0
- data/lib/apropos/functions.rb +68 -0
- data/lib/apropos/media_query.rb +41 -0
- data/lib/apropos/sass_functions.rb +69 -0
- data/lib/apropos/set.rb +58 -0
- data/lib/apropos/variant.rb +67 -0
- data/lib/apropos/version.rb +3 -0
- data/spec/apropos/class_list_spec.rb +8 -0
- data/spec/apropos/extension_parser_spec.rb +71 -0
- data/spec/apropos/functions_spec.rb +185 -0
- data/spec/apropos/media_query_spec.rb +24 -0
- data/spec/apropos/set_spec.rb +34 -0
- data/spec/apropos/variant_spec.rb +74 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/stylesheets_spec.rb +114 -0
- data/stylesheets/_apropos.sass +3 -0
- data/stylesheets/apropos/_breakpoints.sass +6 -0
- data/stylesheets/apropos/_core.sass +40 -0
- data/stylesheets/apropos/_hidpi.sass +4 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 108512a68128ab79c042acc053dfdbaa25acbd0a
|
4
|
+
data.tar.gz: 992deab384d11673b20ae55c10e783f78c8d03d6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 87339533a21983bf2ef19b408f18864fe3ef345cf3319f39e0097b232b7c703b728e260eef746e3de5e23767812af5523f45f204aa21c7b1cf3c43e80e8ae253
|
7
|
+
data.tar.gz: 7d6f80d6f12b3d425c307582750e928df2e932534b6cfcfb8ad6dc7efadae6e0737387016393d159c5d1a8e1e31efacbb914aeffa3f726df927f38c7dd137a8b
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
Copyright 2013 Square Inc.
|
3
|
+
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
you may not use this file except in compliance with the License.
|
6
|
+
You may obtain a copy of the License at
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
See the License for the specific language governing permissions and
|
14
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Apropos
|
2
|
+
|
3
|
+
Apropos helps your site serve up the appropriate image for every visitor. Serving multiple versions of an image in responsive and/or localized web sites can be a chore, but Apropos simplifies and automates this task. Instead of manually writing a lot of CSS rules to swap different images, Apropos generates CSS for you based on a simple file naming convention.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'apropos'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install apropos
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Apropos depends on [Compass](http://compass-style.org/), so make sure you have that installed and configured in your project.
|
22
|
+
|
23
|
+
### Sample configuration
|
24
|
+
|
25
|
+
It's easy to get up and running with Apropos' basic configuration. Here's a sample stylesheet:
|
26
|
+
|
27
|
+
```sass
|
28
|
+
// Put this in a .sass (or .scss) file, such as application.css.sass
|
29
|
+
|
30
|
+
// Substitute with your own breakpoint names and sizes
|
31
|
+
$apropos-breakpoints: (medium, 768px), (large, 1024px)
|
32
|
+
@import "apropos"
|
33
|
+
|
34
|
+
.hero
|
35
|
+
// Use hero.jpg as the background of this element, and load any image
|
36
|
+
// variants that exist. If you use $generate-height: true, the function
|
37
|
+
// will also generate height definitions based on the height of each
|
38
|
+
// image (except dpi variants, since you want to display those at the
|
39
|
+
// original dimensions).
|
40
|
+
+apropos-bg-variants('hero.jpg', $generate-height: true)
|
41
|
+
|
42
|
+
// Customize other background styles
|
43
|
+
background-size: auto 100%
|
44
|
+
background-position: 50%
|
45
|
+
```
|
46
|
+
|
47
|
+
With that configuration set up, you can include any set of variants on your image with a simple file naming convention:
|
48
|
+
|
49
|
+
# File listing e.g. app/assets/images:
|
50
|
+
hero.jpg
|
51
|
+
hero.medium.jpg
|
52
|
+
hero.large.jpg
|
53
|
+
hero.2x.jpg
|
54
|
+
hero.2x.medium.jpg
|
55
|
+
hero.2x.large.jpg
|
56
|
+
|
57
|
+
In this example, `hero.jpg` would be your base image, most likely a mobile version. `hero.medium.jpg` would be swapped in at the 768px breakpoint, and `hero.large.jpg` would be swapped in at 1024px. On a high-dpi device, `hero.2x.jpg`, `hero.2x.medium.jpg`, and `hero.2x.large.jpg` would be used instead. Note that the order of the file extensions doesn't matter; `hero.2x.medium.jpg` and `hero.medium.2x.jpg` work exactly the same.
|
58
|
+
|
59
|
+
### Customization
|
60
|
+
|
61
|
+
You can customize Apropos' breakpoints as shown above, and you can also customize the definition of the "high dpi" variant:
|
62
|
+
|
63
|
+
```sass
|
64
|
+
// The default extension name is "2x", we're overriding to use "hidpi"
|
65
|
+
$apropos-hidpi-extension: "hidpi"
|
66
|
+
// The default ratio is 1.75 (or 168 dpi), but here we're overriding that
|
67
|
+
$apropos-hidpi-query: "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)"
|
68
|
+
@import "apropos"
|
69
|
+
```
|
70
|
+
|
71
|
+
If you want to do more advanced configuration like adding variants for localization, you can [customize Apropos in Ruby](doc-src/customization.md).
|
72
|
+
|
73
|
+
## Why use Apropos?
|
74
|
+
|
75
|
+
There are many tools and techniques for using responsive images. What makes Apropos different? A few key principles:
|
76
|
+
|
77
|
+
- Let the browser do what it does best. CSS rules are more efficient and reliable than a solution that relies on Javascript or setting cookies for each visitor.
|
78
|
+
- Avoid duplicate downloads. Almost all Javascript solutions, including polyfills for things like `srcset`, require unnecessary extra downloads, which CSS classes and media queries avoid.
|
79
|
+
- No server logic should be required. Rather than setting a cookie and serving up different assets based on the cookie, we should be able to push compiled CSS and images to a CDN and rely on the browser to request the right images.
|
80
|
+
- Take advantage of the "metadata" encoded in file names. We need to create separate assets for high-dpi devices, breakpoints, locales, etc anyway. We can lean on the filesystem with a simple naming convention rather than hand-coding a bunch of CSS.
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
1. Fork it
|
85
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
86
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
87
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
88
|
+
5. Create new Pull Request
|
89
|
+
|
90
|
+
Before any changes are merged to master, we need you to sign a very simple
|
91
|
+
[Individual Contributor Agreement](https://spreadsheets.google.com/a/squareup.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1)
|
92
|
+
(Google Form).
|
93
|
+
|
94
|
+
© 2013 Square, Inc.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'cane/rake_task'
|
10
|
+
|
11
|
+
desc "Run cane to check quality metrics"
|
12
|
+
Cane::RakeTask.new(:quality) do |cane|
|
13
|
+
cane.abc_max = 10
|
14
|
+
cane.style_glob = 'lib/**/*.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => :quality
|
18
|
+
rescue LoadError
|
19
|
+
warn "cane not available, quality task not provided."
|
20
|
+
end
|
data/apropos.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'apropos/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "apropos"
|
8
|
+
spec.version = Apropos::VERSION
|
9
|
+
spec.authors = ["Gabriel Gilder"]
|
10
|
+
spec.email = ["gabriel@squareup.com"]
|
11
|
+
spec.description = %q{Apropos helps your site serve up the appropriate image for every visitor. Serving multiple versions of an image in responsive and/or localized web sites can be a chore, but Apropos simplifies and automates this task. Instead of manually writing a lot of CSS rules to swap different images, Apropos generates CSS for you based on a simple file naming convention.}
|
12
|
+
spec.summary = %q{Apropos helps your site serve up the appropriate image for every visitor.}
|
13
|
+
spec.homepage = "https://github.com/square/apropos"
|
14
|
+
spec.license = "Apache 2.0"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "compass"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec", "~> 2.13"
|
26
|
+
spec.add_development_dependency "cane"
|
27
|
+
spec.add_development_dependency "simplecov"
|
28
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Apropos
|
2
|
+
|
3
|
+
## Advanced Customization
|
4
|
+
|
5
|
+
If you want to go beyond breakpoint and resolution variants, you can use Apropos' Ruby interface to customize it for your app.
|
6
|
+
|
7
|
+
Your customization code should go in a initializer file or in your Compass config file.
|
8
|
+
|
9
|
+
### Example: localized images
|
10
|
+
|
11
|
+
This example creates a variant that will recognize images for different languages. We assume that your app adds a class such as "lang-en" to the body to indicate the language of the page. With this code, you could have a base file "image.jpg" and variants such as "image.fr.jpg" and "image.ja.jpg" for different languages.
|
12
|
+
|
13
|
+
And of course, this works in combination with other variants, so if you're using Apropos' hidpi and breakpoints you could have files like "image.medium.2x.fr.jpg" and all the proper rules would be generated.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
# This would be in your Compass config.rb or in a Rails initializer.
|
17
|
+
# You may need to add `require 'apropos'` depending on load order in your app.
|
18
|
+
|
19
|
+
SUPPORTED_LANGUAGES = ['en', 'fr', 'ja']
|
20
|
+
|
21
|
+
# Use a broad regex to match the file extension...
|
22
|
+
Apropos.add_class_image_variant(/^[a-z]{2}$/) do |match|
|
23
|
+
# ... but validate it against our app's supported languages
|
24
|
+
if SUPPORTED_LANGUAGES.include? match[0]
|
25
|
+
".lang-#{match[0]}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
### Example: country + language
|
31
|
+
|
32
|
+
Here's a more complex example where we recognize simple locale identifiers that encode country as well as language. We also recognize just the language code, or just the country code. This means you could have images like "image.ca.jpg", "image.fr-ca.jpg", "image.fr.jpg", etc...
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
SUPPORTED_COUNTRIES = ['us', 'ca', 'fr']
|
36
|
+
SUPPORTED_LANGUAGES = ['en', 'fr']
|
37
|
+
|
38
|
+
Apropos.add_class_image_variant(/^([a-z]{2})(-[a-z]{2})?$/) do |match|
|
39
|
+
lang_or_country = match[1]
|
40
|
+
# Strip off the dash
|
41
|
+
country = match[2][1..-1] if match[2]
|
42
|
+
if country
|
43
|
+
if SUPPORTED_COUNTRIES.include?(country) && SUPPORTED_LANGUAGES.include?(lang_or_country)
|
44
|
+
# Return a class like ".locale-fr-ca"
|
45
|
+
".locale-#{lang_or_country}-#{country}"
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# Determine if the two-letter code is a country, language, or both (like "fr")
|
49
|
+
classes = []
|
50
|
+
classes << ".lang-#{lang_or_country}" if SUPPORTED_LANGUAGES.include? lang_or_country
|
51
|
+
classes << ".country-#{lang_or_country}" if SUPPORTED_COUNTRIES.include? lang_or_country
|
52
|
+
# Return nil if the code is not a supported language or country
|
53
|
+
classes unless classes.empty?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
data/lib/apropos.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'compass'
|
2
|
+
|
3
|
+
module Apropos
|
4
|
+
SEPARATOR = '.'
|
5
|
+
STYLESHEETS_DIR = File.expand_path('../../stylesheets', __FILE__)
|
6
|
+
end
|
7
|
+
|
8
|
+
Dir.glob(File.expand_path('../apropos/*.rb', __FILE__), &method(:require))
|
9
|
+
|
10
|
+
module Sass::Script::Functions
|
11
|
+
include Apropos::SassFunctions
|
12
|
+
end
|
13
|
+
|
14
|
+
Compass::Frameworks.register('apropos', {
|
15
|
+
:stylesheets_directory => Apropos::STYLESHEETS_DIR
|
16
|
+
})
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Apropos
|
2
|
+
# ClassList wraps a list of CSS class selectors with several abilities:
|
3
|
+
# - Can be combined with other ClassLists
|
4
|
+
# - Can be compared to MediaQuery or ClassList objects via #sort_value, #type
|
5
|
+
# - Can be converted to CSS output
|
6
|
+
class ClassList
|
7
|
+
attr_reader :list, :sort_value
|
8
|
+
|
9
|
+
def initialize(list, sort_value=0)
|
10
|
+
@list = list
|
11
|
+
@sort_value = sort_value
|
12
|
+
end
|
13
|
+
|
14
|
+
def combine(other)
|
15
|
+
self.class.new(list + other.list)
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_css
|
19
|
+
list.join(', ')
|
20
|
+
end
|
21
|
+
|
22
|
+
def type
|
23
|
+
"class"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Apropos
|
2
|
+
# ExtensionParser manages registered variant parsers and provides a base
|
3
|
+
# class which new parsers subclass. Parsers are initialized with a pattern
|
4
|
+
# (String or Regexp) and a block that is called to generate ClassList or
|
5
|
+
# MediaQuery objects from the provided match data.
|
6
|
+
class ExtensionParser
|
7
|
+
@parsers = {}
|
8
|
+
|
9
|
+
def self.parsers
|
10
|
+
@parsers
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.add_parser(extension, &block)
|
14
|
+
@parsers[extension] = new(extension, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.each_parser(&block)
|
18
|
+
parsers.values.each(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :pattern
|
22
|
+
|
23
|
+
def initialize(pattern, &block)
|
24
|
+
@pattern = pattern
|
25
|
+
@match_block = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def match(extension)
|
29
|
+
matchdata = pattern.match(extension)
|
30
|
+
if matchdata
|
31
|
+
@match_block.call(matchdata)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# The Apropos module provides several functions for configuration and for
|
2
|
+
# supplying rules to the Sass functions. See the README for configuration
|
3
|
+
# examples.
|
4
|
+
#
|
5
|
+
# It also provides convenience functions used by the Sass functions.
|
6
|
+
module Apropos
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def image_variant_rules(path)
|
10
|
+
set = Set.new(path, images_dir)
|
11
|
+
set.variants.select(&:valid?).map do |variant|
|
12
|
+
variant.rule
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_dpi_image_variant(id, query, order=0)
|
17
|
+
ExtensionParser.add_parser(id) do |match|
|
18
|
+
MediaQuery.new(query, order)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_breakpoint_image_variant(id, query, order=0)
|
23
|
+
ExtensionParser.add_parser(id) do |match|
|
24
|
+
MediaQuery.new(query, order)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_class_image_variant(id, class_list=[], order=0, &block)
|
29
|
+
parser = if block_given?
|
30
|
+
lambda do |match|
|
31
|
+
result = block.call(match)
|
32
|
+
create_class_rule(result) if result
|
33
|
+
end
|
34
|
+
else
|
35
|
+
lambda do |match|
|
36
|
+
create_class_rule(class_list, order)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
ExtensionParser.add_parser(id, &parser)
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_class_rule(class_list, order=0)
|
44
|
+
list = Array(class_list).map {|name| name[0] == '.' ? name : ".#{name}"}
|
45
|
+
ClassList.new(list, order)
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear_image_variants
|
49
|
+
ExtensionParser.parsers.clear
|
50
|
+
end
|
51
|
+
|
52
|
+
def images_dir
|
53
|
+
config = Compass.configuration
|
54
|
+
Pathname.new(config.project_path).join(config.images_dir || '')
|
55
|
+
end
|
56
|
+
|
57
|
+
def convert_to_sass_value(val)
|
58
|
+
case val
|
59
|
+
when String
|
60
|
+
Sass::Script::String.new(val)
|
61
|
+
when Array
|
62
|
+
converted = val.map {|element| convert_to_sass_value(element) }
|
63
|
+
Sass::Script::List.new(converted, :space)
|
64
|
+
else
|
65
|
+
raise "convert_to_sass_value doesn't understand type #{val.class.inspect}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Apropos
|
2
|
+
# MediaQuery wraps a media query string with several features:
|
3
|
+
# - Parenthesizes queries when necessary
|
4
|
+
# - Can be combined with other MediaQuery objects
|
5
|
+
# - Can be compared to ClassList or MediaQuery objects via #type, #sort_value
|
6
|
+
# - Can be converted to CSS output
|
7
|
+
class MediaQuery
|
8
|
+
attr_reader :query_list, :sort_value
|
9
|
+
|
10
|
+
def initialize(query_string, sort_value=0)
|
11
|
+
@query_list = query_string.split(',').map { |q| parenthesize(q.strip) }
|
12
|
+
@sort_value = sort_value
|
13
|
+
end
|
14
|
+
|
15
|
+
def parenthesize(query)
|
16
|
+
unless query =~ /^\(.+\)$/
|
17
|
+
"(#{query})"
|
18
|
+
else
|
19
|
+
query
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def combine(other)
|
24
|
+
other_ql = other.query_list
|
25
|
+
combo = query_list.map do |q|
|
26
|
+
other_ql.map do |q2|
|
27
|
+
"#{q} and #{q2}"
|
28
|
+
end
|
29
|
+
end.flatten
|
30
|
+
self.class.new(combo.join(', '))
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_css
|
34
|
+
query_list.join(", ")
|
35
|
+
end
|
36
|
+
|
37
|
+
def type
|
38
|
+
"media"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|