/
extension.rb
462 lines (376 loc) · 17.4 KB
/
extension.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
require 'middleman-core'
################################################################################
# This extension provides Middleman the ability to build and serve multiple
# targets, and includes resource extensions and helpers to take advantage of
# this new feature.
# @author Jim Derry <balthisar@gmail.com>
################################################################################
class MiddlemanTargets < ::Middleman::Extension
############################################################
# Define the options that are to be set within `config.rb`
# as Middleman *application* (not extension) options.
############################################################
define_setting :target, 'default', 'The default target to process if not specified.'
define_setting :targets, { :default => { :features => {} } } , 'A hash that defines many characteristics of the target.'
define_setting :target_magic_images, true, 'Enable magic images for targets.'
define_setting :target_magic_word, 'all', 'The magic image prefix for image substitution.'
# @!group Middleman Configuration
# @!attribute [rw] config[:target]=
# Indicates the current target that is being built or served. When
# set in `config.rb` it indicates the default target if one is not
# specified on the command line.
# @return [Symbol] The target from `config[:targets]` that should
# be used as the default.
# @note This is a Middleman application level config option.
# @!attribute [rw] config[:targets]=
# A hash that defines all of the characteristics of your individual targets.
# The `build_dir`, 'http_prefix', and `features` keys in a target have special
# meanings; other keys can be added arbitrarily and helpers can fetch these
# for you. A best practice is to assign the same features to _all_ of your
# targets and toggle them `on` or `off` on a target-specific basis.
# @example You might define this in your `config.rb` like this:
# config[:targets] = {
# :free =>
# {
# :sample_key => 'People who use free versions don\'t drive profits.',
# :build_dir => 'build (%s)',
# :http_prefix => '/free',
# :features =>
# {
# :feature_advertise_pro => true,
# :insults_user => true,
# :grants_wishes => false,
# }
# },
#
# :pro =>
# {
# :sample_key => 'You are a valued contributor to our balance sheet!',
# :http_prefix => '/pro',
# :features =>
# {
# :feature_advertise_pro => false,
# :insults_user => false,
# :grants_wishes => true,
# }
# },
# }
# @return [Hash] The complete definition of your targets, their
# features, and other keys-value pairs that you wish to include.
# @note This is a Middleman application level config option.
# @!attribute [rw] config[:target_magic_images]=
# This option is used to enable or disable the target magic images feature.
# If it's `true` then the `image_tag` helper will attempt to substitute
# target-specific images instead of the specified image, if the specified
# image begins with `:target_magic_word`.
# @return [Boolean] Specify whether or not automatic target-specific
# image substitution should be enabled.
# @note This is a Middleman application level config option.
# @!attribute [rw] config[:target_magic_word]=
# Indicates the magic image prefix for image substitution with the
# `image_tag` helper when `:target_magic_images` is enabled. For example
# if you specify `all-image.png` and `pro-image.png` exists, then the
# latter will be used by the helper instead of the former.
# @return [String] Indicate the prefix that should indicate an image
# that should be substituted, such as `all`.
# @note This is a Middleman application level config option.
# @!attribute [rw] config[:build_dir]=
# Indicates where **Middleman** will put build output. This standard config
# value will be treated as a *prefix*; for example if the current target is
# `:pro` and this value is set to its default `build`, then the actual build
# directory will be `build (pro)/`.
#
# If the `build_dir` key is present for any of the `config[:targets]`, they
# will override this setting.
# @return [String] Indicate the build directory prefix that should be
# used for build output.
# @note This is a Middleman application level config option.
# @!attribute [rw] config[:http_prefix]=
# Default prefix for building paths. Used by HTML helpers.
#
# If the `http_prefix` key is present for any of the `config[:targets]`,
# they will override this setting.
# @return [String] Indicate the HTTP prefix that will be used by
# HTML helpers.
# @note This is a Middleman application level config option.
# @!endgroup
############################################################
# initialize
# @!visibility private
############################################################
def initialize(app, options_hash={}, &block)
super
app.config[:target] = app.config[:target].to_sym
end # initialize
############################################################
# after_configuration
# Handle the --target cli setting.
# @!visibility private
#############################################################
def after_configuration
app.config[:target] = app.config[:target].downcase.to_sym
requested_target = app.config[:target]
valid_targets = app.config[:targets].each_key.collect { |item| item.downcase}
if (build_dir = app.config[:targets][app.config[:target]][:build_dir])
app.config[:build_dir] = sprintf(build_dir, requested_target)
else
app.config[:build_dir] = "#{app.config[:build_dir]} (#{requested_target})"
end
if valid_targets.count < 1
say 'middleman-targets is activated but there are no targets specified in your', :red
say 'configuration file.', :red
exit 1
end
return if app.config[:exit_before_ready]
if valid_targets.include?(requested_target)
if app.config[:mode] == :server
say "Middleman will serve using target \"#{requested_target}\".", :blue
else
say "Middleman will build using target \"#{requested_target}\".", :blue
say "Build directory is \"#{app.config[:build_dir]}\".", :blue
if (http_prefix = app.config[:targets][app.config[:target]][:http_prefix])
app.config[:http_prefix] = http_prefix
say "Using http_prefix \"#{app.config[:http_prefix]}\".", :blue
end
end
else
if requested_target
say "The target \"#{requested_target}\" is invalid. Use one of these:", :red
else
say 'No target has been specified. Use one of these:', :red
end
valid_targets.each { |t| say " #{t}", :red }
exit 1
end
end # after_configuration
############################################################
# Sitemap manipulators.
# Add new methods to each resource.
# @visibility private
############################################################
def manipulate_resource_list(resources)
resources.each do |resource|
#--------------------------------------------------------
# Returns an array of valid features for a resource
# based on the current target, i.e., features that
# are true for the current target.
# @return [Array] Returns an array of features.
#--------------------------------------------------------
def resource.valid_features
@app.config[:targets][@app.config[:target]][:features].select { |k, v| v }.keys
end
#--------------------------------------------------------
# Determines if the resource is eligible for inclusion
# in the current target based on the front matter data
# `target` and `exclude` fields.
#
# * If **frontmatter:target** is used, the target or
# feature appears in the frontmatter, and
# * If **frontmatter:exclude** is used, the target or
# enabled feature does NOT appear in the
# frontmatter
#
# In general you won't use this resource method because
# resources will already be excluded before you have a
# chance to check them, and so any leftover resources
# will always return `true` for this method.
# @return [Boolean] Returns a value indicating whether
# or not this resource belongs in the current target.
#--------------------------------------------------------
def resource.targeted?
target_name = @app.config[:target]
( !self.data['target'] || (self.data['target'].include?(target_name) || (self.data['target'] & self.valid_features).count > 0) ) &&
( !self.data['exclude'] || !(self.data['exclude'].include?(target_name) || (self.data['exclude'] & self.valid_features).count > 0) )
end
#========================================================
# ignore un-targeted pages
# Here we have the chance to ignore resources that
# don't belong in this build based on front matter
# options.
#========================================================
resource.ignore! unless resource.targeted?
#========================================================
# ignore non-target images
# Here we have the chance to ignore images from other
# targets if :target_magic_images is enabled.
#========================================================
if @app.config[:target_magic_images] && resource.content_type && resource.content_type.start_with?('image/')
targets = @app.config[:targets].keys
targets.delete(@app.config[:target])
keep = true
targets.each { |prefix| keep = keep && File.basename(resource.path) !~ /^#{prefix}\-/i }
unless keep
resource.ignore!
say " Ignoring #{resource.path} because this target is #{@app.config[:target]}.", :yellow
end
end
end # resources.each
resources
end # manipulate_resource_list
############################################################
# Helpers
# Methods defined in this helpers block are available in
# templates.
############################################################
helpers do
#--------------------------------------------------------
# Return the current build target.
# @return [Symbol] Returns the current build target.
#--------------------------------------------------------
def target_name
@app.config[:target]
end
#--------------------------------------------------------
# Is the current target `proposal`?
# @param [String, Symbol] proposal Specifies a proposed
# target.
# @return [Boolean] Returns `true` if the current target
# matches the parameter `proposal`.
#--------------------------------------------------------
def target_name?( proposal )
@app.config[:target] == proposal.to_sym
end
#--------------------------------------------------------
# Does the target have the feature `feature` enabled?
# @param [String, Symbol] feature Specifies a proposed
# feature.
# @return [Boolean] Returns `true` if the current target
# has the features `feature` and the features is
# enabled.
#--------------------------------------------------------
def target_feature?( feature )
features = @app.config[:targets][@app.config[:target]][:features]
features.key?(feature.to_sym) && features[feature.to_sym]
end
#--------------------------------------------------------
# Attempts to return arbitrary key values for the key
# `key` for the current target.
# @param [String, Symbol] key Specifies the desired key
# to look up.
# @return [String, Nil] Returns the value for `key` in
# the `:targets` structure, or `nil` if it doesn’t
# exist.
#--------------------------------------------------------
def target_value( key )
target_values = @app.config[:targets][@app.config[:target]]
target_values.key?(key) ? target_values[key] : nil
end
#--------------------------------------------------------
# Override the built-in `image-tag` helper in order to
# support additional features.
#
# * Automatic target-specific images. Note that this
# only works on local files, and only if enabled
# with the option `:target_magic_images`.
# * Target and feature dependent images using the
# `params` hash.
# * Absolute paths, which Middleman sometimes bungles.
#
# Note that in addition to the options described below,
# `middleman-targets` inherits all of the built-in
# option parameters as well.
#
# @param [String] path The path to the image file.
# @param [Hash] params Optional parameters to pass to
# the helper.
# @option params [String, Symbol] :target This image
# tag will only be applied if the current target is
# `target`.
# @option params [String, Symbol] :feature This image
# tag will only be applied if the current target
# enables the feature `feature`.
# @return [String] A properly formed image tag.
# @group Extended Helpers
#--------------------------------------------------------
def image_tag(path, params={})
params.symbolize_keys!
# We won't return an image at all if a :target or :feature parameter
# was provided, unless we're building that target or feature.
return if params.include?(:target) && !target_name?(params[:target])
return if params.include?(:feature) && !target_feature?(params[:feature])
params.delete(:target)
params.delete(:feature)
# Let's try to find a substitutable file if the current image name
# begins with the :target_magic_word
if @app.config[:target_magic_images]
path = extensions[:MiddlemanTargets].target_specific_proposal( path )
end
# Support automatic alt tags for absolute locations, too. Only do
# this for absolute paths; let the extension do its own thing
# otherwise.
if @app.extensions[:automatic_alt_tags] && path.start_with?('/')
alt_text = File.basename(file[:full_path].to_s, '.*')
alt_text.capitalize!
params[:alt] ||= alt_text
end
super(path, params)
end # @endgroup
end #helpers
############################################################
# Instance Methods
# @group Instance Methods
############################################################
#########################################################
# Returns a target-specific proposed file when given
# a user-specified file, and will return the same file
# if not enabled or doesn't begin with the magic word.
#
# This method allow other implementations of `image-tag`
# to honor this implementation’s use of automatic
# image substitutions.
# @param [String] path Specify the path to the image
# for which you want to provide a substitution. This
# image doesn’t have to exist in fact if suitable
# images exist for the current target.
# @return [String] Returns the substitute image if one
# was found, otherwise it returns the original path.
#########################################################
def target_specific_proposal( path )
return path unless @app.config[:target_magic_images] && File.basename(path).start_with?( @app.config[:target_magic_word])
real_path = path.dup
magic_prefix = "#{app.config[:target_magic_word]}-"
wanted_prefix = "#{app.config[:target]}-"
# Enable absolute paths, too.
real_path = if path.start_with?('/')
File.expand_path(File.join(app.config[:source], real_path))
else
File.expand_path(File.join(app.config[:source], app.config[:images_dir], real_path))
end
proposed_path = real_path.sub( magic_prefix, wanted_prefix )
file = app.files.find(:source, proposed_path)
if file && file[:full_path].exist?
path.sub( magic_prefix, wanted_prefix )
else
path
end
end
#########################################################
# Output colored messages using ANSI codes.
# @!visibility private
#########################################################
def say(message = '', color = :reset)
colors = { :blue => "\033[34m",
:cyan => "\033[36m",
:green => "\033[32m",
:red => "\033[31m",
:yellow => "\033[33m",
:reset => "\033[0m",
}
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
puts message
else
puts colors[color] + message + colors[:reset]
end
end # say
############################################################
# Instance Methods Exposed to Config
# @group Instance Methods Exposed to Config
############################################################
expose_to_config :middleman_target
#########################################################
# Expose the current target to config.rb.
#########################################################
def middleman_target
@app.config[:target]
end
end # class MiddlemanTargets