antw-kin 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +11 -0
- data/VERSION.yml +1 -1
- data/lib/kin.rb +4 -1
- data/lib/kin/masthead.rb +113 -60
- data/lib/kin/nav.rb +29 -4
- data/lib/kin/nav/builder.rb +4 -2
- data/lib/kin/nav/formatters.rb +187 -92
- data/lib/kin/nav/helper_mixin.rb +1 -1
- data/lib/kin/sprites.rb +100 -0
- data/lib/kin/sprites/image_generator.rb +75 -0
- data/lib/kin/sprites/rake_runner.rb +179 -0
- data/lib/kin/sprites/sass_generator.rb +81 -0
- data/lib/kin/tasks/sprites.rb +34 -0
- data/spec/fixture/app/views/nav_specs/has_right_formatter.html.haml +9 -0
- data/spec/fixture/app/views/nav_specs/subnav_formatter.html.haml +7 -0
- data/spec/fixture/config/sprites.different.yml +11 -0
- data/spec/fixture/config/sprites.yml +9 -0
- data/spec/fixture/public/images/sprites/src/one.png +0 -0
- data/spec/fixture/public/images/sprites/src/three.png +0 -0
- data/spec/fixture/public/images/sprites/src/two.png +0 -0
- data/spec/masthead_spec.rb +1 -77
- data/spec/nav_spec.rb +32 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/sprites_spec.rb +349 -0
- metadata +16 -4
- data/lib/kin/merbtasks.rb +0 -0
- data/spec/fixture/app/views/masthead_specs/no_border.html.haml +0 -2
data/lib/kin/nav/helper_mixin.rb
CHANGED
@@ -44,7 +44,7 @@ module Kin
|
|
44
44
|
# @api public
|
45
45
|
#
|
46
46
|
def display_navigation(name, options = {})
|
47
|
-
(options[:formatter] ||
|
47
|
+
(options[:formatter] || Kin::Nav.get(name).formatter).new(
|
48
48
|
Kin::Nav.get(name), self, options
|
49
49
|
).to_html
|
50
50
|
end
|
data/lib/kin/sprites.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'rmagick'
|
2
|
+
|
3
|
+
module Kin
|
4
|
+
##
|
5
|
+
# Takes 16x16 pixel icons and smushes them all together in to one sprite.
|
6
|
+
# Not auto-loaded, so remember to require 'kin/sprites' if you need it.
|
7
|
+
#
|
8
|
+
module Sprites
|
9
|
+
SpriteError = Class.new(StandardError)
|
10
|
+
|
11
|
+
##
|
12
|
+
# A simple class which represents a list of icons (wrapper around an
|
13
|
+
# array).
|
14
|
+
#
|
15
|
+
class IconSet
|
16
|
+
include Enumerable
|
17
|
+
|
18
|
+
##
|
19
|
+
# Creates a new IconSet instance.
|
20
|
+
#
|
21
|
+
# @param [Array<String>] icons
|
22
|
+
# An array of source icon files.
|
23
|
+
#
|
24
|
+
def initialize(icons)
|
25
|
+
@icons = icons.uniq
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Returns the vertical offset of a given icon in a sprite.
|
30
|
+
#
|
31
|
+
# @param [String]
|
32
|
+
# The name of an icon which appears in the list.
|
33
|
+
#
|
34
|
+
# @return [Integer]
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# list = IconSet.new(%w( one two three ))
|
38
|
+
# list.location_of('one') # => 0
|
39
|
+
# list.location_of('two') # => 40
|
40
|
+
# list.location_of('three') # => 80
|
41
|
+
#
|
42
|
+
# @raise [ArgumentError]
|
43
|
+
# Raises an ArgumentError if the given icon does not appear in the
|
44
|
+
# list.
|
45
|
+
#
|
46
|
+
def location_of(icon)
|
47
|
+
unless @icons.include?(icon)
|
48
|
+
raise ArgumentError, "Icon does not appear in the list: #{icon}"
|
49
|
+
end
|
50
|
+
|
51
|
+
@icons.index(icon) * 40
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Loops through, and yields, each icon in turn.
|
56
|
+
#
|
57
|
+
# @yield [String] The icon name.
|
58
|
+
#
|
59
|
+
def each
|
60
|
+
@icons.each { |i| yield i }
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns the number of icons in this set.
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
#
|
68
|
+
def length
|
69
|
+
@icons.length
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Returns a slice of the set. Note: This returns a Set, not an IconSet.
|
74
|
+
#
|
75
|
+
# @param [Numeric, Range] slice
|
76
|
+
# The element or range of elements to return.
|
77
|
+
#
|
78
|
+
# @return [Set]
|
79
|
+
#
|
80
|
+
def [](*slice)
|
81
|
+
@icons[*slice]
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Returns a hash for the icon list. Assuming the list contains the same
|
86
|
+
# icons, and in the same order, the hash will always be the same.
|
87
|
+
#
|
88
|
+
# @return [String]
|
89
|
+
#
|
90
|
+
def hash
|
91
|
+
Digest::SHA256.hexdigest(@icons.join("|"))
|
92
|
+
end
|
93
|
+
end # IconSet
|
94
|
+
end # Sprites
|
95
|
+
end # Kin
|
96
|
+
|
97
|
+
sprites = File.expand_path(File.join(File.dirname(__FILE__), 'sprites'))
|
98
|
+
|
99
|
+
require File.join(sprites, 'image_generator')
|
100
|
+
require File.join(sprites, 'sass_generator')
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Kin
|
2
|
+
module Sprites
|
3
|
+
##
|
4
|
+
# Responsible for taking source icon files and creating a sprite.
|
5
|
+
#
|
6
|
+
class ImageGenerator
|
7
|
+
IconNotReadable = Class.new(SpriteError)
|
8
|
+
TargetNotWriteable = Class.new(SpriteError)
|
9
|
+
|
10
|
+
##
|
11
|
+
# Creates a new Generator instance.
|
12
|
+
#
|
13
|
+
# @param [IconSet] set
|
14
|
+
# An IconSet instance.
|
15
|
+
# @param [String] source_dir
|
16
|
+
# A path to the directory in which the source files reside.
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
#
|
20
|
+
def initialize(set, source_dir)
|
21
|
+
@set, @source_dir = set, source_dir
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Uses RMagick to create a transparent PNG containing the individual
|
26
|
+
# icons as a sprite.
|
27
|
+
#
|
28
|
+
# If a file already exists at the given path it will be overwritten.
|
29
|
+
#
|
30
|
+
# @param [String] path
|
31
|
+
# The path at which to save the sprite.
|
32
|
+
#
|
33
|
+
# @raise [Kin::Sprites::ImageGenerator::IconNotReadable]
|
34
|
+
# Raised when an icon could not be found.
|
35
|
+
#
|
36
|
+
# @raise [Kin::Sprites::ImageGenerator::TargetNotWriteable]
|
37
|
+
# Raised when RMagick couldn't save the image because the target path
|
38
|
+
# was not writeable.
|
39
|
+
#
|
40
|
+
# @raise [Magick::ImageMagickError]
|
41
|
+
# Raised if some other error occured with RMagick.
|
42
|
+
#
|
43
|
+
# @api public
|
44
|
+
#
|
45
|
+
def save(path)
|
46
|
+
list = @set.inject(Magick::ImageList.new) do |m, icon|
|
47
|
+
m << Magick::Image.read(File.join(@source_dir, "#{icon}.png"))[0]
|
48
|
+
end
|
49
|
+
|
50
|
+
# RMagick uses instance_eval, @set isn't available in the block below.
|
51
|
+
set_length = @set.length
|
52
|
+
|
53
|
+
montage = list.montage do
|
54
|
+
# Transparent background.
|
55
|
+
self.background_color = '#FFF0'
|
56
|
+
# Each icon is 16x16 with 12 pixels of top & bottom padding = 16x40.
|
57
|
+
self.geometry = Magick::Geometry.new(16, 16, 0, 12)
|
58
|
+
self.tile = Magick::Geometry.new(1, set_length)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Remove the blank space from both the top and bottom of the image.
|
62
|
+
montage.crop!(0, 12, 0, (40 * set_length) - 24)
|
63
|
+
montage.write("PNG32:#{path}")
|
64
|
+
rescue Magick::ImageMagickError => ex
|
65
|
+
# Nicely rescue and re-raise if the target directory wasn't writable,
|
66
|
+
# or if one of the icon files could not be opened.
|
67
|
+
case ex.message
|
68
|
+
when /Permission denied/ then raise TargetNotWriteable, ex.message
|
69
|
+
when /unable to open/ then raise IconNotReadable, ex.message
|
70
|
+
else raise ex
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end # ImageGenerator
|
74
|
+
end # Sprites
|
75
|
+
end # Kin
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
|
3
|
+
module Kin
|
4
|
+
module Sprites
|
5
|
+
##
|
6
|
+
# A Rake helper which generates all the sprites for a given application.
|
7
|
+
#
|
8
|
+
# RakeRunner expects a sprites.yml config file which defines the sprites
|
9
|
+
# and the icons in each sprite. The contents of each sprites are hashed
|
10
|
+
# and cached in a .hashes file (the the same directory as the saved
|
11
|
+
# sprites) so that re-running the task won't result in them being
|
12
|
+
# re-generated. See README for examples.
|
13
|
+
#
|
14
|
+
# Not auto-loaded by requiring either 'kin' or 'kin/sprites'; if you want
|
15
|
+
# this Rake helper you need to require it explicitly.
|
16
|
+
#
|
17
|
+
class RakeRunner
|
18
|
+
TEMPLATE = File.read(__FILE__).split(/^__END__/)[1]
|
19
|
+
|
20
|
+
SetInfo = Struct.new(:name, :set, :skipped)
|
21
|
+
|
22
|
+
##
|
23
|
+
# Creates a new Generator instance.
|
24
|
+
#
|
25
|
+
# @param [String] sprites_yml
|
26
|
+
# The path to the sprites.yml file which defines the sprites.
|
27
|
+
# @param [String] sprites_dir
|
28
|
+
# The path to sprites directory, in which the generated sprites will
|
29
|
+
# be saved. The source icons will be assumed to be in a src/
|
30
|
+
# subdirectory.
|
31
|
+
# @param [String] sass_partial
|
32
|
+
# The path where the SASS partial should be saved.
|
33
|
+
#
|
34
|
+
# @raise [SpriteError]
|
35
|
+
# Raised if the sprites.yml file wasn't at the specified path.
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
#
|
39
|
+
def initialize(sprites_yml, sprites_dir, sass_partial)
|
40
|
+
@sprites_yml = sprites_yml
|
41
|
+
@sprites_dir = sprites_dir
|
42
|
+
@sass_partial = sass_partial
|
43
|
+
@source_dir = File.join(@sprites_dir, 'src')
|
44
|
+
@dot_hashes = File.join(@sprites_dir, '.hashes')
|
45
|
+
|
46
|
+
unless File.exists?(@sprites_yml)
|
47
|
+
raise SpriteError,
|
48
|
+
"sprites.yml not found; expected to be at: #{@sprites_yml}"
|
49
|
+
end
|
50
|
+
|
51
|
+
@sets = YAML.load(File.read(@sprites_yml)).map do |name, icons|
|
52
|
+
SetInfo.new(name, Kin::Sprites::IconSet.new(icons), false)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sort the sets in alphanumeric order such that SASS partials don't
|
56
|
+
# radically change when regenerated (since Ruby 1.8's hash is
|
57
|
+
# unordered)
|
58
|
+
@sets.sort! { |left, right| left.name <=> right.name }
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Generates the sprite images, and creates a _sprites.sass partial.
|
63
|
+
#
|
64
|
+
# @param [Boolean] force
|
65
|
+
# Regenerate sprite images even if the existing image contains the
|
66
|
+
# exact same icons as the new one will.
|
67
|
+
#
|
68
|
+
# @api public
|
69
|
+
#
|
70
|
+
def generate!(force = false)
|
71
|
+
@sets.each do |set|
|
72
|
+
generate_sprite!(set, force)
|
73
|
+
end
|
74
|
+
|
75
|
+
if @sets.any? { |set| not set.skipped }
|
76
|
+
generate_sass!
|
77
|
+
dump_hashes!
|
78
|
+
end
|
79
|
+
|
80
|
+
$stdout.puts
|
81
|
+
$stdout.puts("All done.")
|
82
|
+
end
|
83
|
+
|
84
|
+
private # ==============================================================
|
85
|
+
|
86
|
+
##
|
87
|
+
# Generates a single sprite.
|
88
|
+
#
|
89
|
+
# @api private
|
90
|
+
#
|
91
|
+
def generate_sprite!(setinfo, force)
|
92
|
+
if requires_regenerating?(setinfo) or force
|
93
|
+
gen = Kin::Sprites::ImageGenerator.new(setinfo.set, @source_dir)
|
94
|
+
gen.save(File.join(@sprites_dir, "#{setinfo.name}.png"))
|
95
|
+
$stdout.puts("Regenerated #{setinfo.name.inspect} sprite.")
|
96
|
+
else
|
97
|
+
# Existing sprite file is identical, and the user doesn't want to
|
98
|
+
# force re-creating the file.
|
99
|
+
setinfo.skipped = true
|
100
|
+
$stdout.puts("Ignoring #{setinfo.name.inspect} sprite (unchanged).")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Generates the SASS partial for all sprites.
|
106
|
+
#
|
107
|
+
# @api private
|
108
|
+
#
|
109
|
+
def generate_sass!
|
110
|
+
sass = @sets.map do |setinfo|
|
111
|
+
set_sass = "// #{setinfo.name} sprite "
|
112
|
+
set_sass += "=" * (80 - 13 - setinfo.name.length)
|
113
|
+
set_sass += Kin::Sprites::SassGenerator.new(
|
114
|
+
setinfo.set, setinfo.name).to_sass('/images/sprites')
|
115
|
+
end.join("\n")
|
116
|
+
|
117
|
+
FileUtils.mkdir_p(File.dirname(@sass_partial))
|
118
|
+
|
119
|
+
File.open(@sass_partial, 'w') do |f|
|
120
|
+
f.puts(TEMPLATE.sub(/^SASS/, sass))
|
121
|
+
end
|
122
|
+
|
123
|
+
$stdout.puts('Saved SASS partial.')
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Creates a fresh .hashes file containing hashes for the currently
|
128
|
+
# loaded sets.
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
#
|
132
|
+
def dump_hashes!
|
133
|
+
hashes = @sets.inject({}) do |hashes, setinfo|
|
134
|
+
hashes[setinfo.name] = setinfo.set.hash ; hashes
|
135
|
+
end
|
136
|
+
|
137
|
+
File.open(@dot_hashes, 'w') do |f|
|
138
|
+
f.puts(YAML.dump(hashes))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Returns if a sprite needs to be regenerated.
|
144
|
+
#
|
145
|
+
# @api private
|
146
|
+
#
|
147
|
+
def requires_regenerating?(setinfo)
|
148
|
+
hash_for(setinfo.name) != setinfo.set.hash ||
|
149
|
+
(! File.exists?(File.join(@sprites_dir, "#{setinfo.name}.png")))
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Returns the hash checksum for the current sprite file, if it exists.
|
154
|
+
#
|
155
|
+
# @param [String] name
|
156
|
+
# The name of the sprite to check.
|
157
|
+
#
|
158
|
+
# @return [String, nil]
|
159
|
+
# Returns a string if a hash was available, otherwise nil.
|
160
|
+
#
|
161
|
+
# @api private
|
162
|
+
#
|
163
|
+
def hash_for(name)
|
164
|
+
@_sprite_hashes ||= begin
|
165
|
+
File.exists?(@dot_hashes) ? YAML.load(File.read(@dot_hashes)) : {}
|
166
|
+
end
|
167
|
+
|
168
|
+
@_sprite_hashes[name]
|
169
|
+
end
|
170
|
+
end # RakeTaskRunner
|
171
|
+
end # Sprites
|
172
|
+
end # Kin
|
173
|
+
|
174
|
+
__END__
|
175
|
+
// The mixins in this file are automatically generated by the `sprites` rake
|
176
|
+
// task provided in the Kin gem. Don't edit this file directly; rather you
|
177
|
+
// should change config/sprites.yaml and then run `rake sprites`.
|
178
|
+
|
179
|
+
SASS
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Kin
|
2
|
+
module Sprites
|
3
|
+
##
|
4
|
+
# Converts an IconSet to a SASS partial.
|
5
|
+
#
|
6
|
+
class SassGenerator
|
7
|
+
TEMPLATE = File.read(__FILE__).split(/^__END__/)[1]
|
8
|
+
|
9
|
+
##
|
10
|
+
# Creates a new Generator instance.
|
11
|
+
#
|
12
|
+
# @param [IconSet] set
|
13
|
+
# An IconSet instance.
|
14
|
+
# @param [String] name
|
15
|
+
# The name of this sprite.
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
#
|
19
|
+
def initialize(set, name)
|
20
|
+
@set, @name = set, name
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Converts the icon set to a SASS partial.
|
25
|
+
#
|
26
|
+
# @param [String] dir
|
27
|
+
# The web directory in which the sprite files are located (no trailing
|
28
|
+
# space please).
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
#
|
32
|
+
def to_sass(dir)
|
33
|
+
# Holds all the if/else statements.
|
34
|
+
conditions = []
|
35
|
+
conditions << if_statement(@set[0], true)
|
36
|
+
@set[1..-1].each { |icon| conditions << if_statement(icon) }
|
37
|
+
|
38
|
+
# Permits supplying an offset, rather than an icon name.
|
39
|
+
conditions << ' @else'
|
40
|
+
conditions << ' !pos = !icon'
|
41
|
+
conditions << ''
|
42
|
+
|
43
|
+
# Put the data into the template.
|
44
|
+
template = TEMPLATE.dup
|
45
|
+
template.gsub!(/NAME/, @name)
|
46
|
+
template.gsub!(/DIR/, dir)
|
47
|
+
template.gsub!(/CONDITIONS/, conditions.join("\n"))
|
48
|
+
template
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
##
|
54
|
+
# Creates an if statement for the given icon.
|
55
|
+
#
|
56
|
+
# @param [String] icon
|
57
|
+
# The name of an icons
|
58
|
+
# @param [Boolean] is_first
|
59
|
+
# Is this the first icon to appear in the if statement?
|
60
|
+
#
|
61
|
+
def if_statement(icon, is_first = false)
|
62
|
+
else_modifier = is_first ? '' : 'else '
|
63
|
+
|
64
|
+
%[ @#{else_modifier}if !icon == "#{icon}"\n] +
|
65
|
+
%[ !pos = -#{@set.location_of(icon)}px]
|
66
|
+
end
|
67
|
+
end # SassGenerator
|
68
|
+
end # Sprites
|
69
|
+
end # Kin
|
70
|
+
|
71
|
+
__END__
|
72
|
+
|
73
|
+
=NAME-icon(!icon, !hpos = 0px, !voff = 0px)
|
74
|
+
!pos = !voff
|
75
|
+
CONDITIONS
|
76
|
+
:background = "url(DIR/NAME.png)" !hpos !pos "no-repeat"
|
77
|
+
|
78
|
+
=NAME-icon-pos(!icon, !hpos = 0px, !voff = 0px)
|
79
|
+
!pos = !voff
|
80
|
+
CONDITIONS
|
81
|
+
:background-position = !hpos !pos
|