apropos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -0,0 +1,69 @@
|
|
1
|
+
module Apropos
|
2
|
+
# A collection of methods mixed into Sass::Script::Functions to allow
|
3
|
+
# Apropos to be used from Sass files. The primary method is
|
4
|
+
# `image-variants`, which generates the actual CSS rules. Configuration
|
5
|
+
# directly from the Sass file is possible with the `add-dpi-image-variant`
|
6
|
+
# and `add-breakpoint-image-variant` methods, although the limitations of
|
7
|
+
# Sass syntax require that the output of these functions be assigned to a
|
8
|
+
# dummy variable.
|
9
|
+
module SassFunctions
|
10
|
+
def self.sass_function_exist?(meth)
|
11
|
+
Sass::Script::Functions.instance_methods.include? meth
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(mod)
|
15
|
+
::Sass::Script::Functions.declare :image_variants, []
|
16
|
+
::Sass::Script::Functions.declare :add_dpi_image_variant, []
|
17
|
+
::Sass::Script::Functions.declare :add_breakpoint_image_variant, []
|
18
|
+
::Sass::Script::Functions.declare :nth_polyfill, [:list, :index]
|
19
|
+
unless sass_function_exist? :str_index
|
20
|
+
::Sass::Script::Functions.declare :str_index, [:string, :substring]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.value(val)
|
25
|
+
val.respond_to?(:value) ? val.value : val
|
26
|
+
end
|
27
|
+
|
28
|
+
def image_variants(path)
|
29
|
+
assert_type path, :String
|
30
|
+
out = ::Apropos.image_variant_rules(path.value)
|
31
|
+
::Apropos.convert_to_sass_value(out)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_dpi_image_variant(id, query, sort=0)
|
35
|
+
sort = ::Apropos::SassFunctions.value(sort)
|
36
|
+
::Apropos.add_dpi_image_variant(id.value, query.value, sort)
|
37
|
+
::Sass::Script::Bool.new(false)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_breakpoint_image_variant(id, query, sort=0)
|
41
|
+
sort = ::Apropos::SassFunctions.value(sort)
|
42
|
+
::Apropos.add_breakpoint_image_variant(id.value, query.value, sort)
|
43
|
+
::Sass::Script::Bool.new(false)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Can be replaced with stock `nth` once dca1498 makes it into a Sass release
|
47
|
+
# http://git.io/eGNOKA
|
48
|
+
def nth_polyfill(list, index)
|
49
|
+
index = index.value
|
50
|
+
list = list.value
|
51
|
+
index = list.length + index + 1 if index < 0
|
52
|
+
list[index - 1]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Polyfill for `str-index` function from master branch of Sass.
|
56
|
+
# Implementation taken from:
|
57
|
+
# https://github.com/nex3/sass/blob/master/lib/sass/script/functions.rb
|
58
|
+
# Using Sass::Script::Number rather than Sass::Script::Value::Number for
|
59
|
+
# backwards compatibility, however.
|
60
|
+
unless sass_function_exist? :str_index
|
61
|
+
def str_index(string, substring)
|
62
|
+
assert_type string, :String, :string
|
63
|
+
assert_type substring, :String, :substring
|
64
|
+
index = string.value.index(substring.value) || -1
|
65
|
+
::Sass::Script::Number.new(index + 1)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/apropos/set.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Apropos
|
2
|
+
# A Set generates a list of Variants from a base image path. Any file in the
|
3
|
+
# same directory as the base image with the pattern "basename.*.extension" is
|
4
|
+
# considered to be a potential Variant, though not all generated Variants are
|
5
|
+
# guaranteed to be valid.
|
6
|
+
class Set
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
def initialize(path, basedir)
|
10
|
+
@path = path
|
11
|
+
@basedir = Pathname.new(basedir)
|
12
|
+
end
|
13
|
+
|
14
|
+
def variants
|
15
|
+
variant_paths.map do |code_fragment, path|
|
16
|
+
Variant.new(code_fragment, path)
|
17
|
+
end.sort
|
18
|
+
end
|
19
|
+
|
20
|
+
def variant_paths
|
21
|
+
paths = {}
|
22
|
+
Dir.glob(@basedir.join(variant_path_glob)).each do |path|
|
23
|
+
key = code_fragment(path)
|
24
|
+
paths[key] = remove_basedir(path)
|
25
|
+
end
|
26
|
+
paths
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_basedir(path)
|
30
|
+
path.sub(basedir_re, '')
|
31
|
+
end
|
32
|
+
|
33
|
+
def basedir_re
|
34
|
+
@basedir_re ||= Regexp.new("^.*#{Regexp.escape(@basedir.to_s)}/")
|
35
|
+
end
|
36
|
+
|
37
|
+
def code_fragment(path)
|
38
|
+
start = File.join(File.dirname(path), basename)
|
39
|
+
path[(start.length + 1)...(path.length - extname.length)]
|
40
|
+
end
|
41
|
+
|
42
|
+
def variant_path_glob
|
43
|
+
Pathname.new(dirname).join("#{basename}#{SEPARATOR}*#{extname}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def dirname
|
47
|
+
@dirname ||= File.dirname(@path)
|
48
|
+
end
|
49
|
+
|
50
|
+
def basename
|
51
|
+
@basename ||= File.basename(@path, extname)
|
52
|
+
end
|
53
|
+
|
54
|
+
def extname
|
55
|
+
@extname ||= File.extname(@path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Apropos
|
2
|
+
# A Variant represents a single image file that should be displayed instead
|
3
|
+
# of the base image in one or more conditions. These conditions are parsed
|
4
|
+
# from the codes in the provided code fragment. If none of the available
|
5
|
+
# parsers can understand the Variant's codes, then the Variant is not
|
6
|
+
# considered valid.
|
7
|
+
#
|
8
|
+
# A valid Variant can generate a CSS rule from its matching conditions, and
|
9
|
+
# can be compared to other Variants based on the aggregate sort values of its
|
10
|
+
# matching conditions.
|
11
|
+
class Variant
|
12
|
+
attr_reader :path
|
13
|
+
|
14
|
+
def initialize(code_fragment, path)
|
15
|
+
@code_fragment = code_fragment
|
16
|
+
@path = path
|
17
|
+
end
|
18
|
+
|
19
|
+
def codes
|
20
|
+
@_codes ||= @code_fragment.split(SEPARATOR)
|
21
|
+
end
|
22
|
+
|
23
|
+
def conditions
|
24
|
+
@_conditions ||= codes.map do |code|
|
25
|
+
ExtensionParser.each_parser.inject(nil) do |_, parser|
|
26
|
+
query_or_selector = parser.match(code)
|
27
|
+
break query_or_selector if query_or_selector
|
28
|
+
end
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
def conditions_by_type
|
33
|
+
@_conditions_by_type ||= {}.tap do |combination|
|
34
|
+
conditions.each do |condition|
|
35
|
+
combination[condition.type] = if combination[condition.type]
|
36
|
+
combination[condition.type].combine(condition)
|
37
|
+
else
|
38
|
+
condition
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid?
|
45
|
+
!conditions.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def rule
|
49
|
+
sorted_selector_types = conditions_by_type.keys.sort
|
50
|
+
condition_css = sorted_selector_types.map do |rule_type|
|
51
|
+
conditions_by_type[rule_type].to_css
|
52
|
+
end
|
53
|
+
key = sorted_selector_types.join('+')
|
54
|
+
[key] + condition_css + [path]
|
55
|
+
end
|
56
|
+
|
57
|
+
def aggregate_sort_value
|
58
|
+
conditions.inject(0) do |total, query_or_selector|
|
59
|
+
total + query_or_selector.sort_value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def <=>(other)
|
64
|
+
aggregate_sort_value <=> other.aggregate_sort_value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative "../spec_helper.rb"
|
2
|
+
|
3
|
+
describe Apropos::ExtensionParser do
|
4
|
+
before :each do
|
5
|
+
described_class.parsers.clear
|
6
|
+
end
|
7
|
+
|
8
|
+
describe ".parsers" do
|
9
|
+
it "keeps track of variant parsers" do
|
10
|
+
parser = described_class.add_parser('2x') do
|
11
|
+
end
|
12
|
+
described_class.parsers['2x'].should == parser
|
13
|
+
described_class.parsers.count.should == 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".add_parser" do
|
18
|
+
it "overrides previously defined parsers with the same extension" do
|
19
|
+
old_parser = described_class.add_parser('2x')
|
20
|
+
new_parser = described_class.add_parser('2x')
|
21
|
+
described_class.parsers['2x'].should == new_parser
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe ".each_parser" do
|
26
|
+
it "yields each parser to the block" do
|
27
|
+
described_class.add_parser('2x')
|
28
|
+
described_class.add_parser('medium')
|
29
|
+
described_class.add_parser('fr')
|
30
|
+
vals = []
|
31
|
+
described_class.each_parser do |parser|
|
32
|
+
vals << parser.pattern
|
33
|
+
end
|
34
|
+
vals.should == %w[2x medium fr]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#match" do
|
39
|
+
let(:locale_pattern) { /^([a-z]{2})$/ }
|
40
|
+
|
41
|
+
it "calls the block when the extension matches" do
|
42
|
+
lastmatch = nil
|
43
|
+
parser = described_class.new(locale_pattern) do |match|
|
44
|
+
lastmatch = match
|
45
|
+
end
|
46
|
+
parser.match('fr')
|
47
|
+
lastmatch[1].should == 'fr'
|
48
|
+
end
|
49
|
+
|
50
|
+
it "doesn't call the block when there is no match" do
|
51
|
+
expect {
|
52
|
+
parser = described_class.new(/^fr$/) do |match|
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
parser.match('en').should be_nil
|
56
|
+
}.to_not raise_error
|
57
|
+
end
|
58
|
+
|
59
|
+
it "allows the block to return a nil value" do
|
60
|
+
parser = described_class.new(locale_pattern) do |match|
|
61
|
+
if match[1] == 'fr'
|
62
|
+
true
|
63
|
+
else
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
parser.match('fr').should == true
|
68
|
+
parser.match('en').should be_nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require_relative "../spec_helper.rb"
|
2
|
+
|
3
|
+
describe Apropos do
|
4
|
+
def stub_files(*files)
|
5
|
+
Dir.stub(:glob).with(Pathname.new("/project/images/foo.*.jpg")).and_return(files)
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:rules) { Apropos.image_variant_rules("foo.jpg") }
|
9
|
+
|
10
|
+
before :each do
|
11
|
+
Compass.configuration.stub(:project_path).and_return('/project')
|
12
|
+
Compass.configuration.stub(:images_dir).and_return('images')
|
13
|
+
end
|
14
|
+
|
15
|
+
describe ".add_class_image_variant" do
|
16
|
+
after :each do
|
17
|
+
Apropos.clear_image_variants
|
18
|
+
end
|
19
|
+
|
20
|
+
it "adds a simple class variant" do
|
21
|
+
Apropos.add_class_image_variant('alt', 'alternate')
|
22
|
+
stub_files("/foo.alt.jpg")
|
23
|
+
rules.should == [
|
24
|
+
["class", ".alternate", "/foo.alt.jpg"]
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "adds multiple classes" do
|
29
|
+
Apropos.add_class_image_variant('alt', ['alternate', 'alt'])
|
30
|
+
stub_files("/foo.alt.jpg")
|
31
|
+
rules.should == [
|
32
|
+
["class", ".alternate, .alt", "/foo.alt.jpg"]
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
it "respects sort order" do
|
37
|
+
Apropos.add_class_image_variant('alt', 'alternate', 1)
|
38
|
+
Apropos.add_class_image_variant('b', 'blue', 0)
|
39
|
+
stub_files("/foo.alt.jpg", "/foo.b.jpg")
|
40
|
+
rules.should == [
|
41
|
+
["class", ".blue", "/foo.b.jpg"],
|
42
|
+
["class", ".alternate", "/foo.alt.jpg"]
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
it "uses a custom block to generate class names" do
|
47
|
+
Apropos.add_class_image_variant(/^[a-z]{2}$/) do |match|
|
48
|
+
if match[0] == 'en'
|
49
|
+
"lang-en"
|
50
|
+
elsif match[0] == 'fr'
|
51
|
+
["lang-fr", "country-FR"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
stub_files('/foo.en.jpg', '/foo.fr.jpg', '/foo.de.jpg')
|
55
|
+
rules.should == [
|
56
|
+
["class", ".lang-en", "/foo.en.jpg"],
|
57
|
+
[ "class", ".lang-fr, .country-FR", "/foo.fr.jpg"]
|
58
|
+
]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe ".image_variant_rules" do
|
63
|
+
before :all do
|
64
|
+
Apropos.add_dpi_image_variant('2x', "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)", 0.5)
|
65
|
+
Apropos.add_breakpoint_image_variant('medium', 'min-width: 768px', 1)
|
66
|
+
Apropos.add_breakpoint_image_variant('large', 'min-width: 1024px', 2)
|
67
|
+
Apropos::ExtensionParser.add_parser(/^([a-z]{2}(?:-(ca))?)$/) do |match|
|
68
|
+
if match[2]
|
69
|
+
Apropos::ClassList.new([".locale-fr-CA"], 3)
|
70
|
+
elsif match[1]
|
71
|
+
if match[1] == 'fr'
|
72
|
+
Apropos::ClassList.new([".lang-fr"], 2)
|
73
|
+
elsif match[1] == 'ca'
|
74
|
+
Apropos::ClassList.new([".country-CA"], 1)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
after :all do
|
83
|
+
Apropos.clear_image_variants
|
84
|
+
end
|
85
|
+
|
86
|
+
it "ignores invalid variants" do
|
87
|
+
stub_files("/foo.1x.jpg", "/foo.de.jpg", "/foo.ca.jpg")
|
88
|
+
rules.should == [
|
89
|
+
["class", ".country-CA", "/foo.ca.jpg"]
|
90
|
+
]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "generates a locale class rule for localized variant" do
|
94
|
+
stub_files("/foo.ca.jpg")
|
95
|
+
rules.should == [
|
96
|
+
["class", ".country-CA", "/foo.ca.jpg"]
|
97
|
+
]
|
98
|
+
end
|
99
|
+
|
100
|
+
it "generates a media query rule for breakpoint variant" do
|
101
|
+
stub_files("/foo.medium.jpg")
|
102
|
+
rules.should == [
|
103
|
+
["media", "(min-width: 768px)", "/foo.medium.jpg"]
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
107
|
+
it "generates a media query rule for retina variant" do
|
108
|
+
stub_files("/foo.2x.jpg")
|
109
|
+
rules.should == [
|
110
|
+
["media", "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)", "/foo.2x.jpg"]
|
111
|
+
]
|
112
|
+
end
|
113
|
+
|
114
|
+
it "generates a combined rule for retina + localized variant" do
|
115
|
+
stub_files("/foo.2x.ca.jpg")
|
116
|
+
rules.should == [
|
117
|
+
["class+media", ".country-CA", "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)", "/foo.2x.ca.jpg"]
|
118
|
+
]
|
119
|
+
end
|
120
|
+
|
121
|
+
it "generates multiple rules for multiple variants" do
|
122
|
+
stub_files("/foo.2x.ca.jpg", "/foo.medium.2x.fr.jpg")
|
123
|
+
rules.should == [
|
124
|
+
["class+media", ".country-CA", "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)", "/foo.2x.ca.jpg"],
|
125
|
+
["class+media", ".lang-fr", "(min-width: 768px) and (-webkit-min-device-pixel-ratio: 2), (min-width: 768px) and (min-resolution: 192dpi)", "/foo.medium.2x.fr.jpg"]
|
126
|
+
]
|
127
|
+
end
|
128
|
+
|
129
|
+
it "sorts breakpoint rules" do
|
130
|
+
stub_files("/foo.large.jpg", "/foo.medium.jpg")
|
131
|
+
rules.should == [
|
132
|
+
["media", "(min-width: 768px)", "/foo.medium.jpg"],
|
133
|
+
["media", "(min-width: 1024px)", "/foo.large.jpg"]
|
134
|
+
]
|
135
|
+
end
|
136
|
+
|
137
|
+
it "sorts retina rules after non-retina rules" do
|
138
|
+
stub_files("/foo.2x.large.jpg", "/foo.large.jpg")
|
139
|
+
rules.should == [
|
140
|
+
["media", "(min-width: 1024px)", "/foo.large.jpg"],
|
141
|
+
["media", "(-webkit-min-device-pixel-ratio: 2) and (min-width: 1024px), (min-resolution: 192dpi) and (min-width: 1024px)", "/foo.2x.large.jpg"]
|
142
|
+
]
|
143
|
+
end
|
144
|
+
|
145
|
+
it "sorts breakpoints within retina rules" do
|
146
|
+
stub_files("/foo.2x.large.jpg", "/foo.2x.medium.jpg", "/foo.2x.jpg")
|
147
|
+
rules.should == [
|
148
|
+
["media", "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)", "/foo.2x.jpg"],
|
149
|
+
["media", "(-webkit-min-device-pixel-ratio: 2) and (min-width: 768px), (min-resolution: 192dpi) and (min-width: 768px)", "/foo.2x.medium.jpg"],
|
150
|
+
["media", "(-webkit-min-device-pixel-ratio: 2) and (min-width: 1024px), (min-resolution: 192dpi) and (min-width: 1024px)", "/foo.2x.large.jpg"]
|
151
|
+
]
|
152
|
+
end
|
153
|
+
|
154
|
+
it "sorts locale rules: country < lang < locale" do
|
155
|
+
stub_files("/foo.fr.jpg", "/foo.fr-ca.jpg", "/foo.ca.jpg")
|
156
|
+
rules.should == [
|
157
|
+
["class", ".country-CA", "/foo.ca.jpg"],
|
158
|
+
["class", ".lang-fr", "/foo.fr.jpg"],
|
159
|
+
["class", ".locale-fr-CA", "/foo.fr-ca.jpg"]
|
160
|
+
]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe ".convert_to_sass_value" do
|
165
|
+
it "converts strings to sass strings" do
|
166
|
+
val = Apropos.convert_to_sass_value("foo")
|
167
|
+
val.value.should == "foo"
|
168
|
+
val.class.should == Sass::Script::String
|
169
|
+
end
|
170
|
+
|
171
|
+
it "converts arrays to sass lists" do
|
172
|
+
original_value = ["foo", "bar"]
|
173
|
+
val = Apropos.convert_to_sass_value(original_value)
|
174
|
+
val.class.should == Sass::Script::List
|
175
|
+
val.value.map(&:value).should == original_value
|
176
|
+
val.separator.should == :space
|
177
|
+
end
|
178
|
+
|
179
|
+
it "raises an exception on other input types" do
|
180
|
+
expect {
|
181
|
+
Apropos.convert_to_sass_value(3)
|
182
|
+
}.to raise_exception
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|