yury-twine 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE +30 -0
- data/README.md +230 -0
- data/bin/twine +7 -0
- data/lib/twine.rb +36 -0
- data/lib/twine/cli.rb +200 -0
- data/lib/twine/encoding.rb +22 -0
- data/lib/twine/formatters.rb +20 -0
- data/lib/twine/formatters/abstract.rb +187 -0
- data/lib/twine/formatters/android.rb +254 -0
- data/lib/twine/formatters/apple.rb +328 -0
- data/lib/twine/output_processor.rb +57 -0
- data/lib/twine/placeholders.rb +54 -0
- data/lib/twine/plugin.rb +62 -0
- data/lib/twine/runner.rb +332 -0
- data/lib/twine/twine_file.rb +266 -0
- data/lib/twine/version.rb +3 -0
- data/test/command_test.rb +14 -0
- data/test/fixtures/consume_loc_drop.zip +0 -0
- data/test/fixtures/enc_utf16be.dummy +0 -0
- data/test/fixtures/enc_utf16be_bom.dummy +0 -0
- data/test/fixtures/enc_utf16le.dummy +0 -0
- data/test/fixtures/enc_utf16le_bom.dummy +0 -0
- data/test/fixtures/enc_utf8.dummy +2 -0
- data/test/fixtures/formatter_android.xml +15 -0
- data/test/fixtures/formatter_apple.strings +20 -0
- data/test/fixtures/formatter_django.po +30 -0
- data/test/fixtures/formatter_flash.properties +15 -0
- data/test/fixtures/formatter_gettext.po +26 -0
- data/test/fixtures/formatter_jquery.json +7 -0
- data/test/fixtures/formatter_tizen.xml +15 -0
- data/test/fixtures/gettext_multiline.po +10 -0
- data/test/fixtures/twine_accent_values.txt +13 -0
- data/test/test_abstract_formatter.rb +165 -0
- data/test/test_cli.rb +304 -0
- data/test/test_consume_loc_drop.rb +27 -0
- data/test/test_consume_localization_file.rb +119 -0
- data/test/test_formatters.rb +363 -0
- data/test/test_generate_all_localization_files.rb +102 -0
- data/test/test_generate_loc_drop.rb +80 -0
- data/test/test_generate_localization_file.rb +91 -0
- data/test/test_output_processor.rb +85 -0
- data/test/test_placeholders.rb +84 -0
- data/test/test_twine_definition.rb +111 -0
- data/test/test_twine_file.rb +58 -0
- data/test/test_validate_twine_file.rb +61 -0
- data/test/twine_file_dsl.rb +46 -0
- data/test/twine_test.rb +48 -0
- metadata +179 -0
@@ -0,0 +1,328 @@
|
|
1
|
+
require 'Nokogiri'
|
2
|
+
|
3
|
+
module Twine
|
4
|
+
module Formatters
|
5
|
+
class Apple < Abstract
|
6
|
+
def format_name
|
7
|
+
'apple'
|
8
|
+
end
|
9
|
+
|
10
|
+
def extension
|
11
|
+
'.strings'
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_handle_directory?(path)
|
15
|
+
Dir.entries(path).any? { |item| /^.+\.lproj$/.match(item) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def can_handle_file?(path)
|
19
|
+
path_arr = path.split(File::SEPARATOR)
|
20
|
+
file_name = path_arr[path_arr.length - 1]
|
21
|
+
return file_name == default_file_name || file_name == default_plural_file_name
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_file_name
|
25
|
+
return 'Localizable.strings'
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_plural_file_name
|
29
|
+
return 'Localizable.stringsdict'
|
30
|
+
end
|
31
|
+
|
32
|
+
def determine_language_given_path(path)
|
33
|
+
path_arr = path.split(File::SEPARATOR)
|
34
|
+
path_arr.each do |segment|
|
35
|
+
match = /^(.+)\.lproj$/.match(segment)
|
36
|
+
if match
|
37
|
+
if match[1] != "Base"
|
38
|
+
return match[1]
|
39
|
+
else
|
40
|
+
return 'en'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
def output_path_for_language(lang)
|
49
|
+
if lang == 'en'
|
50
|
+
"Base.lproj"
|
51
|
+
else
|
52
|
+
"#{lang}.lproj"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def read(io, lang)
|
57
|
+
uncategorized_section = nil
|
58
|
+
if !section_exists('Uncategorized')
|
59
|
+
uncategorized_section = TwineSection.new('Uncategorized')
|
60
|
+
@twine_file.sections.insert(0, uncategorized_section)
|
61
|
+
else
|
62
|
+
uncategorized_section = get_section('Uncategorized')
|
63
|
+
end
|
64
|
+
last_comment = nil
|
65
|
+
while line = io.gets
|
66
|
+
# matches a `key = "value"` line, where key may be quoted or unquoted. The former may also contain escaped characters
|
67
|
+
match = /^\s*((?:"(?:[^"\\]|\\.)+")|(?:[^"\s=]+))\s*=\s*"((?:[^"\\]|\\.)*)"/.match(line)
|
68
|
+
if match
|
69
|
+
key = match[1]
|
70
|
+
key = key[1..-2] if key[0] == '"' and key[-1] == '"'
|
71
|
+
key.gsub!('\\"', '"')
|
72
|
+
value = match[2]
|
73
|
+
value.gsub!('\\"', '"')
|
74
|
+
value.gsub!('%s', '%@')
|
75
|
+
value.gsub!('$s', '$@')
|
76
|
+
set_translation_for_key(uncategorized_section, key, lang, value)
|
77
|
+
if last_comment
|
78
|
+
set_comment_for_key(key, last_comment)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
match = /\/\* (.*) \*\//.match(line)
|
83
|
+
if match
|
84
|
+
last_comment = match[1]
|
85
|
+
else
|
86
|
+
last_comment = nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Handle plural files
|
91
|
+
if (!File.file?(plural_input_file_for_lang(lang)))
|
92
|
+
return
|
93
|
+
end
|
94
|
+
|
95
|
+
doc = File.open(plural_input_file_for_lang(lang)) { |f| Nokogiri::XML(f) }
|
96
|
+
comment = nil
|
97
|
+
key = nil
|
98
|
+
value = nil
|
99
|
+
|
100
|
+
top_level_dict = doc.css("dict").first
|
101
|
+
whole_dicts = top_level_dict.xpath("./dict")
|
102
|
+
plural_keys = top_level_dict.xpath("./key")
|
103
|
+
|
104
|
+
for i in 0 ... whole_dicts.size do
|
105
|
+
current_dict = whole_dicts[i]
|
106
|
+
comment = current_dict.css("string").first.content
|
107
|
+
key = plural_keys[i].content.to_s
|
108
|
+
|
109
|
+
nested_dict = current_dict.xpath("./dict")
|
110
|
+
nested_dict_keys = nested_dict.xpath("./key")
|
111
|
+
nested_dict_strings = nested_dict.xpath("./string")
|
112
|
+
|
113
|
+
section = nil
|
114
|
+
if !section_exists(key)
|
115
|
+
section = TwineSection.new(key)
|
116
|
+
@twine_file.sections.insert(@twine_file.sections.size - 1, section)
|
117
|
+
else
|
118
|
+
section = get_section(key)
|
119
|
+
end
|
120
|
+
|
121
|
+
for j in 0 ... nested_dict_keys.children.size do
|
122
|
+
cur_xml_key = nested_dict_keys.children[j]
|
123
|
+
if !cur_xml_key.content.include? "NSString"
|
124
|
+
modified_key = key + "__" + cur_xml_key.content.to_s
|
125
|
+
|
126
|
+
set_translation_for_key(section, modified_key, lang, nested_dict_strings[j].content.to_s)
|
127
|
+
set_ios_comment_for_key(modified_key, comment)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def plural_input_file_for_lang(lang)
|
134
|
+
path = @options[:input_path]
|
135
|
+
if path.include?(output_path_for_language(lang))
|
136
|
+
if path.include?(default_file_name)
|
137
|
+
path.sub(default_file_name, default_plural_file_name)
|
138
|
+
else
|
139
|
+
path + "/" + default_plural_file_name
|
140
|
+
end
|
141
|
+
else
|
142
|
+
path + output_path_for_language(lang) + "/" + default_plural_file_name
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def plural_output_file_for_lang(lang)
|
147
|
+
path = @options[:output_path]
|
148
|
+
if path.include?(output_path_for_language(lang))
|
149
|
+
if path.include?(default_file_name)
|
150
|
+
path.sub(default_file_name, default_plural_file_name)
|
151
|
+
else
|
152
|
+
path + "/" + default_plural_file_name
|
153
|
+
end
|
154
|
+
else
|
155
|
+
path + output_path_for_language(lang) + "/" + default_plural_file_name
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def format_sections(twine_file, lang)
|
160
|
+
first_plural = true
|
161
|
+
out_file = File.open(plural_output_file_for_lang(lang), "w")
|
162
|
+
|
163
|
+
sections = Array.new(twine_file.sections.size)
|
164
|
+
for i in 0 ... twine_file.sections.size
|
165
|
+
section = twine_file.sections[i]
|
166
|
+
if section.is_uncategorized
|
167
|
+
sections[i] = format_section(section, lang)
|
168
|
+
else
|
169
|
+
if first_plural
|
170
|
+
first_plural = false
|
171
|
+
out_file.puts(format_header_stringsdict)
|
172
|
+
end
|
173
|
+
format_section_plural(section, lang, out_file)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
out_file.puts(format_footer_stringsdict)
|
177
|
+
out_file.close
|
178
|
+
sections.compact.join("\n")
|
179
|
+
end
|
180
|
+
|
181
|
+
def format_section(section, lang)
|
182
|
+
definitions = section.definitions.select { |definition| should_include_definition(definition, lang) }
|
183
|
+
return if definitions.empty?
|
184
|
+
|
185
|
+
result = ""
|
186
|
+
|
187
|
+
if section.name && section.name.length > 0
|
188
|
+
section_header = format_section_header(section)
|
189
|
+
result += "\n#{section_header}" if section_header
|
190
|
+
|
191
|
+
if section.is_uncategorized
|
192
|
+
definitions.map! { |definition| format_definition(definition, lang) }
|
193
|
+
definitions.compact! # remove nil definitions
|
194
|
+
definitions.map! { |definition| "\n#{definition}" } # prepend newline
|
195
|
+
result += definitions.join
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def format_section_plural(section, lang, out_file)
|
201
|
+
plural_key = section.name
|
202
|
+
main_file_contains_key = main_localizable_file_contains_key(plural_key)
|
203
|
+
|
204
|
+
for i in 0 ... section.definitions.size
|
205
|
+
definition = section.definitions[i]
|
206
|
+
value = definition.translation_for_lang_or_nil(lang, @twine_file.language_codes[0])
|
207
|
+
ios_localized_format_key = definition.ios_comment
|
208
|
+
|
209
|
+
if ios_localized_format_key == nil
|
210
|
+
puts "[" + plural_key + "]"
|
211
|
+
|
212
|
+
if main_file_contains_key
|
213
|
+
puts "Needs matching key in Localizable.strings"
|
214
|
+
else
|
215
|
+
puts "This is an Android-only plural"
|
216
|
+
end
|
217
|
+
return
|
218
|
+
end
|
219
|
+
|
220
|
+
if i == 0 && main_file_contains_key
|
221
|
+
out_file.puts(format_plural_start(plural_key, ios_localized_format_key.to_s))
|
222
|
+
end
|
223
|
+
|
224
|
+
if value != nil && main_file_contains_key
|
225
|
+
out_file.puts(format_plural_key_value(definition.key, format_value_plural(value.dup)))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
if main_file_contains_key
|
230
|
+
out_file.puts(format_plural_section_end)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def main_localizable_file_contains_key(key)
|
235
|
+
@twine_file.sections.each do |section|
|
236
|
+
if section.is_uncategorized
|
237
|
+
section.definitions.each do |definition|
|
238
|
+
if definition.key == key
|
239
|
+
return true
|
240
|
+
end
|
241
|
+
end
|
242
|
+
return false
|
243
|
+
end
|
244
|
+
end
|
245
|
+
# Should never reach here
|
246
|
+
return false
|
247
|
+
end
|
248
|
+
|
249
|
+
########### PLURALS START ###########
|
250
|
+
|
251
|
+
def format_header_stringsdict
|
252
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
253
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" +
|
254
|
+
"<plist version=\"1.0\">\n" +
|
255
|
+
" <dict>\n"
|
256
|
+
end
|
257
|
+
|
258
|
+
def format_plural_start(section_name, comment)
|
259
|
+
comment_parts = comment.split("@")
|
260
|
+
|
261
|
+
# TODO: Should this be hardcoded or using Nokogiri somehow?
|
262
|
+
" <key>#{section_name}</key>\n" +
|
263
|
+
" <dict>\n" +
|
264
|
+
" <key>NSStringLocalizedFormatKey</key>\n" +
|
265
|
+
" <string>#{comment}</string>\n" +
|
266
|
+
" <key>#{comment_parts[1]}</key>\n" +
|
267
|
+
" <dict>\n" +
|
268
|
+
" <key>NSStringFormatSpecTypeKey</key>\n" +
|
269
|
+
" <string>NSStringPluralRuleType</string>\n" +
|
270
|
+
" <key>NSStringFormatValueTypeKey</key>\n" +
|
271
|
+
" <string>d</string>\n"
|
272
|
+
end
|
273
|
+
|
274
|
+
def format_plural_key_value(plural_key, plural_string)
|
275
|
+
plural_key_parts = plural_key.rpartition(/.__/)
|
276
|
+
# TODO: Nokogiri?
|
277
|
+
" <key>#{plural_key_parts[plural_key_parts.length - 1]}</key>\n" +
|
278
|
+
" <string>#{plural_string}</string>\n"
|
279
|
+
end
|
280
|
+
|
281
|
+
def format_plural_section_end
|
282
|
+
# TODO: Nokogiri?
|
283
|
+
" </dict>\n" +
|
284
|
+
" </dict>\n"
|
285
|
+
end
|
286
|
+
|
287
|
+
def format_footer_stringsdict
|
288
|
+
# TODO: Nokogiri?
|
289
|
+
" </dict>\n" +
|
290
|
+
"</plist>"
|
291
|
+
end
|
292
|
+
|
293
|
+
########### PLURALS END ###########
|
294
|
+
|
295
|
+
def format_header(lang)
|
296
|
+
"/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
|
297
|
+
end
|
298
|
+
|
299
|
+
def format_section_header(section)
|
300
|
+
"/********** #{section.name} **********/\n"
|
301
|
+
end
|
302
|
+
|
303
|
+
def key_value_pattern
|
304
|
+
"\"%{key}\" = \"%{value}\";\n"
|
305
|
+
end
|
306
|
+
|
307
|
+
def format_comment(definition, lang)
|
308
|
+
"/* #{definition.comment.gsub('*/', '* /')} */\n" if definition.comment
|
309
|
+
end
|
310
|
+
|
311
|
+
def format_key(key)
|
312
|
+
escape_quotes(key)
|
313
|
+
end
|
314
|
+
|
315
|
+
def format_value(value)
|
316
|
+
text = escape_quotes(value)
|
317
|
+
text.gsub("b>", "strong>")
|
318
|
+
end
|
319
|
+
|
320
|
+
def format_value_plural(value)
|
321
|
+
text = escape_quotes(value)
|
322
|
+
text.gsub("b>", "strong>").gsub("<", "<")
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
Twine::Formatters.formatters << Twine::Formatters::Apple.new
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Twine
|
2
|
+
module Processors
|
3
|
+
|
4
|
+
class OutputProcessor
|
5
|
+
def initialize(twine_file, options)
|
6
|
+
@twine_file = twine_file
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def default_language
|
11
|
+
@options[:developer_language] || @twine_file.language_codes[0]
|
12
|
+
end
|
13
|
+
|
14
|
+
def fallback_languages(language)
|
15
|
+
fallback_mapping = {
|
16
|
+
'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
|
17
|
+
}
|
18
|
+
|
19
|
+
[fallback_mapping[language], default_language].flatten.compact
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(language)
|
23
|
+
result = TwineFile.new
|
24
|
+
|
25
|
+
result.language_codes.concat @twine_file.language_codes
|
26
|
+
@twine_file.sections.each do |section|
|
27
|
+
new_section = TwineSection.new section.name
|
28
|
+
|
29
|
+
section.definitions.each do |definition|
|
30
|
+
next unless definition.matches_tags?(@options[:tags], @options[:untagged])
|
31
|
+
|
32
|
+
value = definition.translation_for_lang(language)
|
33
|
+
|
34
|
+
next if value && @options[:include] == :untranslated
|
35
|
+
|
36
|
+
if value.nil? && @options[:include] != :translated
|
37
|
+
value = definition.translation_for_lang(fallback_languages(language))
|
38
|
+
end
|
39
|
+
|
40
|
+
next unless value
|
41
|
+
|
42
|
+
new_definition = definition.dup
|
43
|
+
new_definition.translations[language] = value
|
44
|
+
|
45
|
+
new_section.definitions << new_definition
|
46
|
+
result.definitions_by_key[new_definition.key] = new_definition
|
47
|
+
end
|
48
|
+
|
49
|
+
result.sections << new_section
|
50
|
+
end
|
51
|
+
|
52
|
+
return result
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Twine
|
2
|
+
module Placeholders
|
3
|
+
extend self
|
4
|
+
|
5
|
+
PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH = '([-+ 0#])?(\d+|\*)?(\.(\d+|\*))?(hh?|ll?|L|z|j|t)?'
|
6
|
+
PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH = '(\d+\$)?' + PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH
|
7
|
+
|
8
|
+
# http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
|
9
|
+
# http://stackoverflow.com/questions/4414389/android-xml-percent-symbol
|
10
|
+
# https://github.com/mobiata/twine/pull/106
|
11
|
+
def convert_placeholders_from_twine_to_android(input)
|
12
|
+
placeholder_types = '[diufFeEgGxXoscpaA]'
|
13
|
+
|
14
|
+
# %@ -> %s
|
15
|
+
value = input.gsub(/(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})@/, '\1s')
|
16
|
+
|
17
|
+
placeholder_syntax = PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH + placeholder_types
|
18
|
+
placeholder_regex = /%#{placeholder_syntax}/
|
19
|
+
|
20
|
+
number_of_placeholders = value.scan(placeholder_regex).size
|
21
|
+
|
22
|
+
return value if number_of_placeholders == 0
|
23
|
+
|
24
|
+
# got placeholders -> need to double single percent signs
|
25
|
+
# % -> %% (but %% -> %%, %d -> %d)
|
26
|
+
single_percent_regex = /([^%])(%)(?!(%|#{placeholder_syntax}))/
|
27
|
+
value.gsub! single_percent_regex, '\1%%'
|
28
|
+
|
29
|
+
return value if number_of_placeholders < 2
|
30
|
+
|
31
|
+
# number placeholders
|
32
|
+
non_numbered_placeholder_regex = /%(#{PLACEHOLDER_FLAGS_WIDTH_PRECISION_LENGTH}#{placeholder_types})/
|
33
|
+
|
34
|
+
number_of_non_numbered_placeholders = value.scan(non_numbered_placeholder_regex).size
|
35
|
+
|
36
|
+
return value if number_of_non_numbered_placeholders == 0
|
37
|
+
|
38
|
+
raise Twine::Error.new("The value \"#{input}\" contains numbered and non-numbered placeholders") if number_of_placeholders != number_of_non_numbered_placeholders
|
39
|
+
|
40
|
+
# %d -> %$1d
|
41
|
+
index = 0
|
42
|
+
value.gsub!(non_numbered_placeholder_regex) { "%#{index += 1}$#{$1}" }
|
43
|
+
|
44
|
+
value
|
45
|
+
end
|
46
|
+
|
47
|
+
def convert_placeholders_from_android_to_twine(input)
|
48
|
+
placeholder_regex = /(%#{PLACEHOLDER_PARAMETER_FLAGS_WIDTH_PRECISION_LENGTH})s/
|
49
|
+
|
50
|
+
# %s -> %@
|
51
|
+
input.gsub(placeholder_regex, '\1@')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/twine/plugin.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'safe_yaml/load'
|
2
|
+
|
3
|
+
SafeYAML::OPTIONS[:suppress_warnings] = true
|
4
|
+
|
5
|
+
module Twine
|
6
|
+
class Plugin
|
7
|
+
attr_reader :debug, :config
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@debug = false
|
11
|
+
require_gems
|
12
|
+
end
|
13
|
+
|
14
|
+
###
|
15
|
+
# require gems from the yaml config.
|
16
|
+
#
|
17
|
+
# gems: [twine-plugin1, twine-2]
|
18
|
+
#
|
19
|
+
# also works with single gem
|
20
|
+
#
|
21
|
+
# gems: twine-plugin1
|
22
|
+
#
|
23
|
+
def require_gems
|
24
|
+
# ./twine.yml # current working directory
|
25
|
+
# ~/.twine # home directory
|
26
|
+
# /etc/twine.yml # etc
|
27
|
+
cwd_config = join_path Dir.pwd, 'twine.yml'
|
28
|
+
home_config = join_path Dir.home, '.twine'
|
29
|
+
etc_config = '/etc/twine.yml'
|
30
|
+
|
31
|
+
config_order = [cwd_config, home_config, etc_config]
|
32
|
+
|
33
|
+
puts "Config order: #{config_order}" if debug
|
34
|
+
|
35
|
+
config_order.each do |config_file|
|
36
|
+
next unless valid_file config_file
|
37
|
+
puts "Loading: #{config_file}" if debug
|
38
|
+
@config = SafeYAML.load_file config_file
|
39
|
+
puts "Config yaml: #{config}" if debug
|
40
|
+
break
|
41
|
+
end
|
42
|
+
|
43
|
+
return unless config
|
44
|
+
|
45
|
+
# wrap gems in an array. if nil then array will be empty
|
46
|
+
Kernel.Array(config['gems']).each do |gem_path|
|
47
|
+
puts "Requiring: #{gem_path}" if debug
|
48
|
+
require gem_path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def valid_file path
|
55
|
+
File.exist?(path) && File.readable?(path) && !File.directory?(path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def join_path *paths
|
59
|
+
File.expand_path File.join *paths
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|