antw-kin 0.3.2 → 0.3.3
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.
- 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
|