bootstrap-sass 3.0.0.0 → 3.0.1.0.rc
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.
Potentially problematic release.
This version of bootstrap-sass might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/bootstrap-sass/version.rb +2 -2
- data/tasks/converter.rb +16 -773
- data/tasks/converter/char_string_scanner.rb +38 -0
- data/tasks/converter/fonts_conversion.rb +12 -0
- data/tasks/converter/js_conversion.rb +22 -0
- data/tasks/converter/less_conversion.rb +612 -0
- data/tasks/converter/logger.rb +64 -0
- data/tasks/converter/network.rb +110 -0
- data/test/compilation_test.rb +5 -1
- data/test/dummy/app/views/pages/root.html.slim +10 -2
- data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot +0 -0
- data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg +200 -199
- data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf +0 -0
- data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff +0 -0
- data/vendor/assets/javascripts/bootstrap/affix.js +2 -2
- data/vendor/assets/javascripts/bootstrap/alert.js +1 -1
- data/vendor/assets/javascripts/bootstrap/button.js +1 -1
- data/vendor/assets/javascripts/bootstrap/carousel.js +2 -2
- data/vendor/assets/javascripts/bootstrap/collapse.js +2 -2
- data/vendor/assets/javascripts/bootstrap/dropdown.js +2 -2
- data/vendor/assets/javascripts/bootstrap/modal.js +2 -2
- data/vendor/assets/javascripts/bootstrap/popover.js +2 -2
- data/vendor/assets/javascripts/bootstrap/scrollspy.js +2 -2
- data/vendor/assets/javascripts/bootstrap/tab.js +3 -3
- data/vendor/assets/javascripts/bootstrap/tooltip.js +2 -2
- data/vendor/assets/javascripts/bootstrap/transition.js +1 -1
- data/vendor/assets/stylesheets/bootstrap/_alerts.scss +1 -1
- data/vendor/assets/stylesheets/bootstrap/_breadcrumbs.scss +2 -2
- data/vendor/assets/stylesheets/bootstrap/_button-groups.scss +6 -1
- data/vendor/assets/stylesheets/bootstrap/_buttons.scss +2 -4
- data/vendor/assets/stylesheets/bootstrap/_carousel.scss +32 -10
- data/vendor/assets/stylesheets/bootstrap/_code.scss +5 -8
- data/vendor/assets/stylesheets/bootstrap/_dropdowns.scss +1 -2
- data/vendor/assets/stylesheets/bootstrap/_forms.scss +16 -3
- data/vendor/assets/stylesheets/bootstrap/_glyphicons.scss +16 -11
- data/vendor/assets/stylesheets/bootstrap/_grid.scss +32 -285
- data/vendor/assets/stylesheets/bootstrap/_input-groups.scss +9 -0
- data/vendor/assets/stylesheets/bootstrap/_jumbotron.scss +2 -2
- data/vendor/assets/stylesheets/bootstrap/_list-group.scss +15 -17
- data/vendor/assets/stylesheets/bootstrap/_mixins.scss +160 -29
- data/vendor/assets/stylesheets/bootstrap/_modals.scss +2 -16
- data/vendor/assets/stylesheets/bootstrap/_navbar.scss +10 -7
- data/vendor/assets/stylesheets/bootstrap/_navs.scss +53 -20
- data/vendor/assets/stylesheets/bootstrap/_normalize.scss +16 -6
- data/vendor/assets/stylesheets/bootstrap/_pagination.scss +2 -0
- data/vendor/assets/stylesheets/bootstrap/_panels.scss +31 -7
- data/vendor/assets/stylesheets/bootstrap/_print.scss +6 -1
- data/vendor/assets/stylesheets/bootstrap/_progress-bars.scss +4 -7
- data/vendor/assets/stylesheets/bootstrap/_responsive-utilities.scss +57 -68
- data/vendor/assets/stylesheets/bootstrap/_scaffolding.scss +1 -12
- data/vendor/assets/stylesheets/bootstrap/_tables.scss +40 -40
- data/vendor/assets/stylesheets/bootstrap/_theme.scss +26 -11
- data/vendor/assets/stylesheets/bootstrap/_thumbnails.scss +6 -6
- data/vendor/assets/stylesheets/bootstrap/_tooltip.scss +8 -8
- data/vendor/assets/stylesheets/bootstrap/_type.scss +71 -30
- data/vendor/assets/stylesheets/bootstrap/_utilities.scss +15 -1
- data/vendor/assets/stylesheets/bootstrap/_variables.scss +56 -39
- data/vendor/assets/stylesheets/bootstrap/bootstrap.scss +0 -10
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12e4060b2ae1d151a7e2de1e9fd65daedfc720a5
|
4
|
+
data.tar.gz: 6ea5e7065f2cdee8cd1f06918661dbe60d622139
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2e53efa06afc231a19ab780638ac312cb91a7c8cb1c80ad192cbffe70f50341a80fd1216b55e467e5ac56992f47855d9c3c20ec1a215767ff0e6f331ac0f452
|
7
|
+
data.tar.gz: 8cca5905200e5167e222b8eb863f00f2d4ed69c4d8d802662cfd2e23b866370e3de6c0c828573153246ac44eb0863be2b2aaf49c337e8002c157eea2e6df45a9
|
data/tasks/converter.rb
CHANGED
@@ -21,13 +21,23 @@ require 'forwardable'
|
|
21
21
|
require 'term/ansicolor'
|
22
22
|
require 'fileutils'
|
23
23
|
|
24
|
+
require_relative 'converter/fonts_conversion'
|
25
|
+
require_relative 'converter/less_conversion'
|
26
|
+
require_relative 'converter/js_conversion'
|
27
|
+
require_relative 'converter/logger'
|
28
|
+
require_relative 'converter/network'
|
29
|
+
|
24
30
|
class Converter
|
25
31
|
extend Forwardable
|
26
|
-
|
27
|
-
|
28
|
-
|
32
|
+
include Network
|
33
|
+
include LessConversion
|
34
|
+
include JsConversion
|
35
|
+
include FontsConversion
|
29
36
|
|
30
37
|
def initialize(branch)
|
38
|
+
@git_data_api_host = 'https://api.github.com/repos'
|
39
|
+
@git_raw_host = 'https://raw.github.com'
|
40
|
+
|
31
41
|
@repo = 'twbs/bootstrap'
|
32
42
|
@repo_url = "https://github.com/#@repo"
|
33
43
|
@branch = branch || 'master'
|
@@ -49,781 +59,14 @@ class Converter
|
|
49
59
|
store_version
|
50
60
|
end
|
51
61
|
|
52
|
-
def
|
53
|
-
|
54
|
-
files = read_files('fonts', bootstrap_font_files)
|
55
|
-
save_at = @save_at[:fonts]
|
56
|
-
files.each do |name, content|
|
57
|
-
save_file "#{save_at}/#{name}", content
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
NESTED_MIXINS = {'#gradient' => 'gradient'}
|
62
|
-
VARARG_MIXINS = %w(transition transition-transform box-shadow)
|
63
|
-
def process_stylesheet_assets
|
64
|
-
log_status "Processing stylesheets..."
|
65
|
-
files = read_files('less', bootstrap_less_files)
|
66
|
-
|
67
|
-
# read common mixin definitions (incl. nested mixins) from mixins.less
|
68
|
-
read_shared_mixins! files['mixins.less']
|
69
|
-
|
70
|
-
# convert each file
|
71
|
-
files.each do |name, file|
|
72
|
-
log_processing name
|
73
|
-
# apply common conversions
|
74
|
-
file = convert_to_scss(file)
|
75
|
-
case name
|
76
|
-
when 'mixins.less'
|
77
|
-
NESTED_MIXINS.each do |selector, prefix|
|
78
|
-
file = flatten_mixins(file, selector, prefix)
|
79
|
-
end
|
80
|
-
file = varargify_mixin_definitions(file, *VARARG_MIXINS)
|
81
|
-
file = deinterpolate_vararg_mixins(file)
|
82
|
-
file = parameterize_mixin_parent_selector file, 'responsive-(in)?visibility'
|
83
|
-
file = parameterize_mixin_parent_selector file, 'input-size'
|
84
|
-
file = replace_ms_filters(file)
|
85
|
-
file = replace_all file, /\.\$state/, '.#{$state}'
|
86
|
-
file = replace_all file, /,\s*\.open \.dropdown-toggle& \{(.*?)\}/m,
|
87
|
-
" {\\1}\n .open & { &.dropdown-toggle {\\1} }"
|
88
|
-
when 'responsive-utilities.less'
|
89
|
-
file = apply_mixin_parent_selector(file, '&\.(visible|hidden)')
|
90
|
-
file = apply_mixin_parent_selector(file, '(?<!&)\.(visible|hidden)')
|
91
|
-
file = replace_rules(file, ' @media') { |r| unindent(r, 2) }
|
92
|
-
when 'variables.less'
|
93
|
-
file = insert_default_vars(file)
|
94
|
-
file = replace_all file, /(\$icon-font-path:).*(!default)/, '\1 "bootstrap/" \2'
|
95
|
-
when 'close.less'
|
96
|
-
# extract .close { button& {...} } rule
|
97
|
-
file = extract_nested_rule file, 'button&'
|
98
|
-
when 'modals.less'
|
99
|
-
file = replace_all file, /body&,(.*?)(\{.*?\})/m, "\\1\\2\nbody& \\2"
|
100
|
-
file = extract_nested_rule file, 'body&'
|
101
|
-
when 'dropdowns.less'
|
102
|
-
file = replace_all file, /(\s*)@extend \.pull-right-dropdown-menu;/, "\\1right: 0;\\1left: auto;"
|
103
|
-
when 'forms.less'
|
104
|
-
file = extract_nested_rule file, 'textarea&'
|
105
|
-
file = apply_mixin_parent_selector(file, '\.input-(?:sm|lg)')
|
106
|
-
when 'navbar.less'
|
107
|
-
file = replace_all file, /(\s*)\.navbar-(right|left)\s*\{\s*@extend\s*\.pull-(right|left);\s*/, "\\1.navbar-\\2 {\\1 float: \\2 !important;\\1"
|
108
|
-
when 'tables.less'
|
109
|
-
file = replace_all file, /(@include\s*table-row-variant\()(\w+)/, "\\1'\\2'"
|
110
|
-
when 'list-group.less'
|
111
|
-
file = extract_nested_rule file, 'a&'
|
112
|
-
when 'glyphicons.less'
|
113
|
-
file = replace_rules(file, '@font-face') { |rule|
|
114
|
-
rule = replace_all rule, /(\$icon-font-\w+)/, '#{\1}'
|
115
|
-
replace_all rule, /url\(/, 'font-url('
|
116
|
-
}
|
117
|
-
end
|
118
|
-
|
119
|
-
name = name.sub(/\.less$/, '.scss')
|
120
|
-
save_at = @save_at[:scss]
|
121
|
-
path = "#{save_at}/#{'_' unless name == 'bootstrap.scss'}#{name}"
|
122
|
-
save_file(path, file)
|
123
|
-
log_processed File.basename(path)
|
124
|
-
end
|
62
|
+
def save_file(path, content, mode='w')
|
63
|
+
File.open(path, mode) { |file| file.write(content) }
|
125
64
|
end
|
126
65
|
|
66
|
+
# Update version.rb file with BOOTSTRAP_SHA
|
127
67
|
def store_version
|
128
68
|
path = 'lib/bootstrap-sass/version.rb'
|
129
69
|
content = File.read(path).sub(/BOOTSTRAP_SHA\s*=\s*['"][\w]+['"]/, "BOOTSTRAP_SHA = '#@branch_sha'")
|
130
70
|
File.open(path, 'w') { |f| f.write(content) }
|
131
71
|
end
|
132
|
-
|
133
|
-
def process_javascript_assets
|
134
|
-
log_status "Processing javascripts..."
|
135
|
-
save_at = @save_at[:js]
|
136
|
-
read_files('js', bootstrap_js_files).each do |name, file|
|
137
|
-
save_file("#{save_at}/#{name}", file)
|
138
|
-
end
|
139
|
-
log_processed "#{bootstrap_js_files * ' '}"
|
140
|
-
|
141
|
-
log_status "Updating javascript manifest"
|
142
|
-
content = ''
|
143
|
-
bootstrap_js_files.each do |name|
|
144
|
-
name = name.gsub(/\.js$/, '')
|
145
|
-
content << "//= require bootstrap/#{name}\n"
|
146
|
-
end
|
147
|
-
path = "vendor/assets/javascripts/bootstrap.js"
|
148
|
-
save_file(path, content)
|
149
|
-
log_processed path
|
150
|
-
end
|
151
|
-
|
152
|
-
private
|
153
|
-
|
154
|
-
def read_files(path, files)
|
155
|
-
full_path = "#{GIT_RAW}/#@repo/#@branch_sha/#{path}"
|
156
|
-
if (contents = read_cached_files(path, files))
|
157
|
-
log_http_get_files files, full_path, true
|
158
|
-
else
|
159
|
-
log_http_get_files files, full_path, false
|
160
|
-
contents = {}
|
161
|
-
files.map do |name|
|
162
|
-
Thread.start {
|
163
|
-
content = open("#{full_path}/#{name}").read
|
164
|
-
Thread.exclusive { contents[name] = content }
|
165
|
-
}
|
166
|
-
end.each(&:join)
|
167
|
-
write_cached_files path, contents
|
168
|
-
end
|
169
|
-
contents
|
170
|
-
end
|
171
|
-
|
172
|
-
def read_cached_files(path, files)
|
173
|
-
full_path = "#@cache_path/#@branch_sha/#{path}"
|
174
|
-
contents = {}
|
175
|
-
if File.directory?(full_path)
|
176
|
-
files.each do |name|
|
177
|
-
contents[name] = File.read("#{full_path}/#{name}", mode: 'rb') || ''
|
178
|
-
end
|
179
|
-
contents
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def write_cached_files(path, files)
|
184
|
-
full_path = "./#@cache_path/#@branch_sha/#{path}"
|
185
|
-
FileUtils.mkdir_p full_path
|
186
|
-
files.each do |name, content|
|
187
|
-
File.open("#{full_path}/#{name}", 'wb') { |f| f.write content}
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
|
192
|
-
def get_file(url)
|
193
|
-
cache_path = "./#@cache_path#{URI(url).path}"
|
194
|
-
FileUtils.mkdir_p File.dirname(cache_path)
|
195
|
-
if File.exists?(cache_path)
|
196
|
-
log_http_get_file url, true
|
197
|
-
File.read(cache_path, mode: 'rb')
|
198
|
-
else
|
199
|
-
log_http_get_file url, false
|
200
|
-
content = open(url).read
|
201
|
-
File.open(cache_path, 'wb') { |f| f.write content }
|
202
|
-
content
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
# get sha of the branch (= the latest commit)
|
207
|
-
def get_branch_sha
|
208
|
-
cmd = "git ls-remote '#@repo_url' | awk '/#@branch/ {print $1}'"
|
209
|
-
puts cmd
|
210
|
-
@branch_sha ||= %x[#{cmd}].chomp
|
211
|
-
raise 'Could not get branch sha!' unless $?.success?
|
212
|
-
@branch_sha
|
213
|
-
end
|
214
|
-
|
215
|
-
# Get the sha of a dir
|
216
|
-
def get_tree_sha(dir)
|
217
|
-
get_trees['tree'].find { |t| t['path'] == dir }['sha']
|
218
|
-
end
|
219
|
-
|
220
|
-
def get_trees
|
221
|
-
@trees ||= get_json("#{GIT_DATA}/#@repo/git/trees/#@branch_sha")
|
222
|
-
end
|
223
|
-
|
224
|
-
def bootstrap_font_files
|
225
|
-
@bootstrap_font_files ||= begin
|
226
|
-
files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('fonts')}"
|
227
|
-
files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.(eot|svg|ttf|woff)$/ }.map { |f| f['path'] }
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
def bootstrap_less_files
|
232
|
-
@bootstrap_less_files ||= begin
|
233
|
-
files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('less')}"
|
234
|
-
files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.less$/ }.map { |f| f['path'] }
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
def bootstrap_js_files
|
239
|
-
@bootstrap_js_files ||= begin
|
240
|
-
files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('js')}"
|
241
|
-
files = files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.js$/ }.map { |f| f['path'] }
|
242
|
-
files.sort_by { |f|
|
243
|
-
case f
|
244
|
-
# tooltip depends on popover and must be loaded earlier
|
245
|
-
when /tooltip/ then 1
|
246
|
-
when /popover/ then 2
|
247
|
-
else
|
248
|
-
0
|
249
|
-
end
|
250
|
-
}
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
# We need to keep a list of shared mixin names in order to convert the includes correctly
|
255
|
-
# Before doing any processing we read shared mixins from a file
|
256
|
-
# If a mixin is nested, it gets prefixed in the list (e.g. #gradient > .horizontal to 'gradient-horizontal')
|
257
|
-
def read_shared_mixins!(mixins_file)
|
258
|
-
log_status " Reading shared mixins from mixins.less"
|
259
|
-
@shared_mixins = get_mixin_names(mixins_file, silent: true)
|
260
|
-
NESTED_MIXINS.each do |selector, prefix|
|
261
|
-
# we use replace_rules without replacing anything just to use the parsing algorithm
|
262
|
-
replace_rules(mixins_file, selector) { |rule|
|
263
|
-
@shared_mixins += get_mixin_names(unindent(unwrap_rule_block(rule)), silent: true).map { |name| "#{prefix}-#{name}" }
|
264
|
-
rule
|
265
|
-
}
|
266
|
-
end
|
267
|
-
@shared_mixins.sort!
|
268
|
-
log_file_info "shared mixins: #{@shared_mixins * ', '}"
|
269
|
-
@shared_mixins
|
270
|
-
end
|
271
|
-
|
272
|
-
def get_mixin_names(file, opts = {})
|
273
|
-
names = get_css_selectors(file).join("\n" * 2).scan(/^\.([\w-]+)\(#{LESS_MIXIN_DEF_ARGS_RE}\)[ ]*\{/).map(&:first).uniq.sort
|
274
|
-
log_file_info "mixin defs: #{names * ', '}" unless opts[:silent] || names.empty?
|
275
|
-
names
|
276
|
-
end
|
277
|
-
|
278
|
-
def convert_to_scss(file)
|
279
|
-
# mixins may also be defined in the file. get mixin names before doing any processing
|
280
|
-
mixin_names = (@shared_mixins + get_mixin_names(file)).uniq
|
281
|
-
file = replace_vars(file)
|
282
|
-
file = replace_file_imports(file)
|
283
|
-
file = replace_mixin_definitions file
|
284
|
-
file = replace_mixins file, mixin_names
|
285
|
-
# replace_less_extend does not seem to do anything. @glebm
|
286
|
-
file = replace_less_extend(file)
|
287
|
-
file = replace_spin(file)
|
288
|
-
file = replace_image_urls(file)
|
289
|
-
file = replace_image_paths(file)
|
290
|
-
file = replace_escaping(file)
|
291
|
-
file = convert_less_ampersand(file)
|
292
|
-
file = deinterpolate_vararg_mixins(file)
|
293
|
-
file = replace_calculation_semantics(file)
|
294
|
-
file
|
295
|
-
end
|
296
|
-
|
297
|
-
# margin: a -b
|
298
|
-
# LESS: sets 2 values
|
299
|
-
# SASS: sets 1 value (a-b)
|
300
|
-
# This wraps a and -b so they evaluates to 2 values in SASS
|
301
|
-
def replace_calculation_semantics(file)
|
302
|
-
# split_prop_val.call('(@navbar-padding-vertical / 2) -@navbar-padding-horizontal')
|
303
|
-
# #=> ["(navbar-padding-vertical / 2)", "-navbar-padding-horizontal"]
|
304
|
-
split_prop_val = proc { |val|
|
305
|
-
s = CharStringScanner.new(val)
|
306
|
-
r = []
|
307
|
-
buff = ''
|
308
|
-
d = 0
|
309
|
-
prop_char = %r([\$\w\-/\*\+%!])
|
310
|
-
while (token = s.scan_next(/([\)\(]|\s+|#{prop_char}+)/))
|
311
|
-
buff << token
|
312
|
-
case token
|
313
|
-
when '('
|
314
|
-
d += 1
|
315
|
-
when ')'
|
316
|
-
d -= 1
|
317
|
-
if d == 0
|
318
|
-
r << buff
|
319
|
-
buff = ''
|
320
|
-
end
|
321
|
-
when /\s/
|
322
|
-
if d == 0 && !buff.strip.empty?
|
323
|
-
r << buff
|
324
|
-
buff = ''
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
r << buff unless buff.empty?
|
329
|
-
r.map(&:strip)
|
330
|
-
}
|
331
|
-
|
332
|
-
replace_rules file do |rule|
|
333
|
-
replace_properties rule do |props|
|
334
|
-
props.gsub /(?<!\w)([\w-]+):(.*?);/ do |m|
|
335
|
-
prop, vals = $1, split_prop_val.call($2)
|
336
|
-
next m unless vals.length >= 2 && vals.any? { |v| v =~ /^[\+\-]\$/ }
|
337
|
-
transformed = vals.map { |v| v.strip =~ %r(^\(.*\)$) ? v : "(#{v})" }
|
338
|
-
log_transform "property #{prop}: #{transformed * ' '}"
|
339
|
-
"#{prop}: #{transformed * ' '};"
|
340
|
-
end
|
341
|
-
end
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
345
|
-
def save_file(path, content, mode='w')
|
346
|
-
File.open(path, mode) { |file| file.write(content) }
|
347
|
-
end
|
348
|
-
|
349
|
-
# @import "file.less" to "#{target_path}file;"
|
350
|
-
def replace_file_imports(less, target_path = 'bootstrap/')
|
351
|
-
less.gsub %r([@\$]import ["|']([\w-]+).less["|'];),
|
352
|
-
%Q(@import "#{target_path}\\1";)
|
353
|
-
end
|
354
|
-
|
355
|
-
def replace_all(file, regex, replacement = nil, &block)
|
356
|
-
log_transform regex, replacement
|
357
|
-
new_file = file.gsub(regex, replacement, &block)
|
358
|
-
raise "replace_all #{regex}, #{replacement} NO MATCH" if file == new_file
|
359
|
-
new_file
|
360
|
-
end
|
361
|
-
|
362
|
-
# @mixin a() { tr& { color:white } }
|
363
|
-
# to:
|
364
|
-
# @mixin a($parent) { tr#{$parent} { color: white } }
|
365
|
-
def parameterize_mixin_parent_selector(file, rule_sel)
|
366
|
-
log_transform rule_sel
|
367
|
-
param = '$parent'
|
368
|
-
replace_rules(file, '^[ \t]*@mixin\s*' + rule_sel) do |mxn_css|
|
369
|
-
mxn_css.sub! /(?=@mixin)/, "// [converter] $parent hack\n"
|
370
|
-
# insert param into mixin def
|
371
|
-
mxn_css.sub!(/(@mixin [\w-]+)\(([\$\w\-,\s]*)\)/) { "#{$1}(#{param}#{', ' if $2 && !$2.empty?}#{$2})" }
|
372
|
-
# wrap properties in #{$parent} { ... }
|
373
|
-
replace_properties(mxn_css) { |props| " \#{#{param}} { #{props.strip} }\n " }
|
374
|
-
# change nested& rules to nested#{$parent}
|
375
|
-
replace_rules(mxn_css, /.*[^\s ]&/) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
|
376
|
-
end
|
377
|
-
end
|
378
|
-
|
379
|
-
# extracts rule immediately after it's parent, and adjust the selector
|
380
|
-
# .x { textarea& { ... }}
|
381
|
-
# to:
|
382
|
-
# .x { ... }
|
383
|
-
# textarea.x { ... }
|
384
|
-
def extract_nested_rule(file, selector, new_selector = nil)
|
385
|
-
matches = []
|
386
|
-
# first find the rules, and remove them
|
387
|
-
file = replace_rules(file, "\s*#{selector}", comments: true) { |rule, pos, css|
|
388
|
-
matches << [rule, pos]
|
389
|
-
new_selector ||= "#{get_selector(rule).sub(/&$/, '')}#{selector_for_pos(css, pos.begin)}"
|
390
|
-
indent "// [converter] extracted #{get_selector(rule)} to #{new_selector}", indent_width(rule)
|
391
|
-
}
|
392
|
-
log_transform selector, new_selector
|
393
|
-
# replace rule selector with new_selector
|
394
|
-
matches.each do |m|
|
395
|
-
m[0].sub! /(#{COMMENT_RE}*)^(\s*).*?(\s*){/m, "\\1\\2#{new_selector}\\3{"
|
396
|
-
end
|
397
|
-
replace_substrings_at file,
|
398
|
-
matches.map { |_, pos| close_brace_pos(file, pos.begin, 1) + 1 },
|
399
|
-
matches.map { |rule, _| "\n\n" + unindent(rule) }
|
400
|
-
end
|
401
|
-
|
402
|
-
# .visible-sm { @include responsive-visibility() }
|
403
|
-
# to:
|
404
|
-
# @include responsive-visibility('.visible-sm')
|
405
|
-
def apply_mixin_parent_selector(file, rule_sel)
|
406
|
-
log_transform rule_sel
|
407
|
-
replace_rules file, '\s*' + rule_sel, comments: false do |rule, rule_pos, css|
|
408
|
-
body = unwrap_rule_block(rule.dup).strip
|
409
|
-
next rule unless body =~ /^@include \w+/m || body =~ /^@media/ && body =~ /\{\s*@include/
|
410
|
-
rule =~ /(#{COMMENT_RE}*)(#{SELECTOR_RE})\{/
|
411
|
-
cmt, sel = $1, $2.strip
|
412
|
-
# take one up selector chain if this is an &. selector
|
413
|
-
if sel.start_with?('&')
|
414
|
-
parent_sel = selector_for_pos(css, rule_pos.begin)
|
415
|
-
sel = parent_sel + sel[1..-1]
|
416
|
-
end
|
417
|
-
# unwrap, and replace @include
|
418
|
-
unindent unwrap_rule_block(rule).gsub(/(@include [\w-]+)\(([\$\w\-,\s]*)\)/) {
|
419
|
-
"#{cmt}#{$1}('#{sel}'#{', ' if $2 && !$2.empty?}#{$2})"
|
420
|
-
}
|
421
|
-
end
|
422
|
-
end
|
423
|
-
|
424
|
-
# #gradient > { @mixin horizontal ... }
|
425
|
-
# to:
|
426
|
-
# @mixin gradient-horizontal
|
427
|
-
def flatten_mixins(file, container, prefix)
|
428
|
-
log_transform container, prefix
|
429
|
-
replace_rules file, Regexp.escape(container) do |mixins_css|
|
430
|
-
unindent unwrap_rule_block(mixins_css).gsub(/@mixin\s*([\w-]+)/, "@mixin #{prefix}-\\1")
|
431
|
-
end
|
432
|
-
end
|
433
|
-
|
434
|
-
# Replaces the following:
|
435
|
-
# .mixin() -> @include mixin()
|
436
|
-
# #scope > .mixin() -> @include scope-mixin()
|
437
|
-
def replace_mixins(less, mixin_names)
|
438
|
-
mixin_pattern = /(\s+)(([#|\.][\w-]+\s*>\s*)*)\.([\w-]+\(.*\))(?!\s\{)/
|
439
|
-
|
440
|
-
less.gsub(mixin_pattern) do |match|
|
441
|
-
matches = match.scan(mixin_pattern).flatten
|
442
|
-
scope = matches[1] || ''
|
443
|
-
if scope != ''
|
444
|
-
scope = scope.scan(/[\w-]+/).join('-') + '-'
|
445
|
-
end
|
446
|
-
mixin_name = match.scan(/\.([\w-]+)\(.*\)\s?\{?/).first
|
447
|
-
if mixin_name && mixin_names.include?("#{scope}#{mixin_name.first}")
|
448
|
-
"#{matches.first}@include #{scope}#{matches.last}".gsub(/; \$/, ", $").sub(/;\)$/, ')')
|
449
|
-
else
|
450
|
-
"#{matches.first}@extend .#{scope}#{matches.last.gsub(/\(\)/, '')}"
|
451
|
-
end
|
452
|
-
end
|
453
|
-
end
|
454
|
-
|
455
|
-
# change Microsoft filters to SASS calling convention
|
456
|
-
def replace_ms_filters(file)
|
457
|
-
log_transform
|
458
|
-
file.gsub(
|
459
|
-
/filter: e\(%\("progid:DXImageTransform.Microsoft.gradient\(startColorstr='%d', endColorstr='%d', GradientType=(\d)\)",argb\(([\-$\w]+)\),argb\(([\-$\w]+)\)\)\);/,
|
460
|
-
%Q(filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='\#{ie-hex-str(\\2)}', endColorstr='\#{ie-hex-str(\\3)}', GradientType=\\1);)
|
461
|
-
)
|
462
|
-
end
|
463
|
-
|
464
|
-
# unwraps topmost rule block
|
465
|
-
# #sel { a: b; }
|
466
|
-
# to:
|
467
|
-
# a: b;
|
468
|
-
def unwrap_rule_block(css)
|
469
|
-
css[(css =~ RULE_OPEN_BRACE_RE) + 1..-1].sub(/\n?}\s*\z/m, '')
|
470
|
-
end
|
471
|
-
|
472
|
-
def replace_mixin_definitions(less)
|
473
|
-
less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
|
474
|
-
"#{$1}@mixin #{$2.tr(';', ',')}#{$3}".sub(/,\)/, ')')
|
475
|
-
}
|
476
|
-
end
|
477
|
-
|
478
|
-
def replace_vars(less)
|
479
|
-
less = less.dup
|
480
|
-
# skip header comment
|
481
|
-
less =~ %r(\A/\*(.*?)\*/)m
|
482
|
-
from = $~ ? $~.to_s.length : 0
|
483
|
-
less[from..-1] = less[from..-1].
|
484
|
-
gsub(/(?!@mixin|@media|@page|@keyframes|@font-face|@-\w)@/, '$').
|
485
|
-
# variables that would be ignored by gsub above: e.g. @page-header-border-color
|
486
|
-
gsub(/@(page[\w-]+)/, '$\1')
|
487
|
-
less
|
488
|
-
end
|
489
|
-
|
490
|
-
# #gradient > .horizontal()
|
491
|
-
# to:
|
492
|
-
# @include .horizontal-gradient()
|
493
|
-
def replace_less_extend(less)
|
494
|
-
less.gsub(/\#(\w+) \> \.([\w-]*)(\(.*\));?/, '@include \1-\2\3;')
|
495
|
-
end
|
496
|
-
|
497
|
-
def replace_spin(less)
|
498
|
-
less.gsub(/(?![\-$@.])spin(?!-)/, 'adjust-hue')
|
499
|
-
end
|
500
|
-
|
501
|
-
def replace_image_urls(less)
|
502
|
-
less.gsub(/background-image: url\("?(.*?)"?\);/) {|s| "background-image: image-url(\"#{$1}\");" }
|
503
|
-
end
|
504
|
-
|
505
|
-
def replace_image_paths(less)
|
506
|
-
less.gsub('../img/', '')
|
507
|
-
end
|
508
|
-
|
509
|
-
def replace_escaping(less)
|
510
|
-
less = less.gsub(/\~"([^"]+)"/, '#{\1}') # Get rid of ~"" escape
|
511
|
-
less.gsub!(/\$\{([^}]+)\}/, '$\1') # Get rid of @{} escape
|
512
|
-
less.gsub!(/"([^"\n]*)(\$[\w\-]+)([^"\n]*)"/, '"\1#{\2}\3"') # interpolate variable in string, e.g. url("$file-1x") => url("#{$file-1x}")
|
513
|
-
less.gsub(/(\W)e\(%\("?([^"]*)"?\)\)/, '\1\2') # Get rid of e(%("")) escape
|
514
|
-
end
|
515
|
-
|
516
|
-
def insert_default_vars(scss)
|
517
|
-
log_transform
|
518
|
-
scss.gsub(/^(\$.+);/, '\1 !default;')
|
519
|
-
end
|
520
|
-
|
521
|
-
# Converts &-
|
522
|
-
def convert_less_ampersand(less)
|
523
|
-
regx = /^\.badge\s*\{[\s\/\w\(\)]+(&{1}-{1})\w.*?^}$/m
|
524
|
-
|
525
|
-
tmp = ''
|
526
|
-
less.scan(/^(\s*&)(-[\w\[\]]+\s*{.+})$/) do |ampersand, css|
|
527
|
-
tmp << ".badge#{css}\n"
|
528
|
-
end
|
529
|
-
|
530
|
-
less.gsub(regx, tmp)
|
531
|
-
end
|
532
|
-
|
533
|
-
# unindent by n spaces
|
534
|
-
def unindent(txt, n = 2)
|
535
|
-
txt.gsub /^[ ]{#{n}}/, ''
|
536
|
-
end
|
537
|
-
|
538
|
-
# indent by n spaces
|
539
|
-
def indent(txt, n = 2)
|
540
|
-
"#{' ' * n}#{txt}"
|
541
|
-
end
|
542
|
-
|
543
|
-
# get indent length from the first line of txt
|
544
|
-
def indent_width(txt)
|
545
|
-
txt.match(/\A\s*/).to_s.length
|
546
|
-
end
|
547
|
-
|
548
|
-
# @mixin transition($transition) {
|
549
|
-
# to:
|
550
|
-
# @mixin transition($transition...) {
|
551
|
-
def varargify_mixin_definitions(scss, *mixins)
|
552
|
-
log_transform *mixins
|
553
|
-
scss = scss.dup
|
554
|
-
mixins.each do |mixin|
|
555
|
-
scss.gsub! /(@mixin\s*#{Regexp.quote(mixin)})\((#{SCSS_MIXIN_DEF_ARGS_RE})\)/, '\1(\2...)'
|
556
|
-
end
|
557
|
-
scss
|
558
|
-
end
|
559
|
-
|
560
|
-
# @include transition(#{border-color ease-in-out .15s, box-shadow ease-in-out .15s})
|
561
|
-
# to
|
562
|
-
# @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s)
|
563
|
-
def deinterpolate_vararg_mixins(scss)
|
564
|
-
scss = scss.dup
|
565
|
-
VARARG_MIXINS.each do |mixin|
|
566
|
-
if scss.gsub! /(@include\s*#{Regexp.quote(mixin)})\(\s*\#\{([^}]+)\}\s*\)/, '\1(\2)'
|
567
|
-
log_transform mixin
|
568
|
-
end
|
569
|
-
end
|
570
|
-
scss
|
571
|
-
end
|
572
|
-
|
573
|
-
# get full selector for rule_block
|
574
|
-
def get_selector(rule_block)
|
575
|
-
/^\s*(#{SELECTOR_RE}?)\s*\{/.match(rule_block) && $1 && $1.strip
|
576
|
-
end
|
577
|
-
|
578
|
-
# replace CSS rule blocks matching rule_prefix with yield(rule_block, rule_pos)
|
579
|
-
# will also include immediately preceding comments in rule_block
|
580
|
-
#
|
581
|
-
# option :comments -- include immediately preceding comments in rule_block
|
582
|
-
#
|
583
|
-
# replace_rules(".a{ \n .b{} }", '.b') { |rule, pos| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
|
584
|
-
def replace_rules(less, rule_prefix = SELECTOR_RE, options = {}, &block)
|
585
|
-
options = {comments: true}.merge(options || {})
|
586
|
-
less = less.dup
|
587
|
-
s = CharStringScanner.new(less)
|
588
|
-
rule_re = /(?:#{rule_prefix}[^{]*#{RULE_OPEN_BRACE_RE})/
|
589
|
-
if options[:comments]
|
590
|
-
rule_start_re = /(?:#{COMMENT_RE}*)^#{rule_re}/
|
591
|
-
else
|
592
|
-
rule_start_re = /^#{rule_re}/
|
593
|
-
end
|
594
|
-
|
595
|
-
positions = []
|
596
|
-
while (rule_start = s.scan_next(rule_start_re))
|
597
|
-
pos = s.pos
|
598
|
-
positions << (pos - rule_start.length..close_brace_pos(less, pos - 1))
|
599
|
-
end
|
600
|
-
replace_substrings_at(less, positions, &block)
|
601
|
-
less
|
602
|
-
end
|
603
|
-
|
604
|
-
# Get a all top-level selectors (with {)
|
605
|
-
def get_css_selectors(css, opts = {})
|
606
|
-
s = CharStringScanner.new(css)
|
607
|
-
selectors = []
|
608
|
-
while s.scan_next(RULE_OPEN_BRACE_RE)
|
609
|
-
brace_pos = s.pos
|
610
|
-
def_pos = css_def_pos(css, brace_pos+1, -1)
|
611
|
-
sel = css[def_pos.begin..brace_pos - 1].dup
|
612
|
-
sel.strip! if opts[:strip]
|
613
|
-
selectors << sel
|
614
|
-
sel.dup.strip
|
615
|
-
s.pos = close_brace_pos(css, brace_pos, 1) + 1
|
616
|
-
end
|
617
|
-
selectors
|
618
|
-
end
|
619
|
-
|
620
|
-
# replace in the top-level selector
|
621
|
-
# replace_in_selector('a {a: {a: a} } a {}', /a/, 'b') => 'b {a: {a: a} } b {}'
|
622
|
-
def replace_in_selector(css, pattern, sub)
|
623
|
-
# scan for selector positions in css
|
624
|
-
s = CharStringScanner.new(css)
|
625
|
-
prev_pos = 0
|
626
|
-
sel_pos = []
|
627
|
-
while (brace = s.scan_next(RULE_OPEN_BRACE_RE))
|
628
|
-
pos = s.pos
|
629
|
-
sel_pos << (prev_pos .. pos - 1)
|
630
|
-
s.pos = close_brace_pos(css, s.pos - 1) + 1
|
631
|
-
prev_pos = pos
|
632
|
-
end
|
633
|
-
replace_substrings_at(css, sel_pos) { |s| s.gsub(pattern, sub) }
|
634
|
-
end
|
635
|
-
|
636
|
-
|
637
|
-
sel_chars = '\[\]$\w\-{}#,.:&>@'
|
638
|
-
SELECTOR_RE = /[#{sel_chars}]+[#{sel_chars}\s]*/
|
639
|
-
COMMENT_RE = %r((?:^[ \t]*//[^\n]*\n))
|
640
|
-
RULE_OPEN_BRACE_RE = /(?<![@#\$])\{/
|
641
|
-
RULE_OPEN_BRACE_RE_REVERSE = /\{(?![@#\$])/
|
642
|
-
RULE_CLOSE_BRACE_RE = /(?<!\w)\}(?![.'"])/
|
643
|
-
RULE_CLOSE_BRACE_RE_REVERSE = /(?<![.'"])\}(?!\w)/
|
644
|
-
BRACE_RE = /#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m
|
645
|
-
BRACE_RE_REVERSE = /#{RULE_OPEN_BRACE_RE_REVERSE}|#{RULE_CLOSE_BRACE_RE_REVERSE}/m
|
646
|
-
SCSS_MIXIN_DEF_ARGS_RE = /[\w\-,\s$:#%]*/
|
647
|
-
LESS_MIXIN_DEF_ARGS_RE = /[\w\-,;\s@:#%]*/
|
648
|
-
|
649
|
-
# replace first level properties in the css with yields
|
650
|
-
# replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
|
651
|
-
def replace_properties(css, &block)
|
652
|
-
s = CharStringScanner.new(css)
|
653
|
-
s.skip_until /#{RULE_OPEN_BRACE_RE}\n?/
|
654
|
-
prev_pos = s.pos
|
655
|
-
depth = 0
|
656
|
-
pos = []
|
657
|
-
while (b = s.scan_next(/#{SELECTOR_RE}#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m))
|
658
|
-
s_pos = s.pos
|
659
|
-
depth += (b == '}' ? -1 : +1)
|
660
|
-
if depth == 1
|
661
|
-
if b == '}'
|
662
|
-
prev_pos = s_pos
|
663
|
-
else
|
664
|
-
pos << (prev_pos .. s_pos - b.length - 1)
|
665
|
-
end
|
666
|
-
end
|
667
|
-
end
|
668
|
-
replace_substrings_at css, pos, &block
|
669
|
-
end
|
670
|
-
|
671
|
-
|
672
|
-
# immediate selector of css at pos
|
673
|
-
def selector_for_pos(css, pos, depth = -1)
|
674
|
-
css[css_def_pos(css, pos, depth)].dup.strip
|
675
|
-
end
|
676
|
-
|
677
|
-
# get the pos of css def at pos (search backwards)
|
678
|
-
def css_def_pos(css, pos, depth = -1)
|
679
|
-
to = open_brace_pos(css, pos, depth)
|
680
|
-
prev_def = to - (css[0..to].reverse.index('}') || to) + 1
|
681
|
-
from = prev_def + 1 + (css[prev_def + 1..-1] =~ %r(^\s*[^\s/]))
|
682
|
-
(from..to - 1)
|
683
|
-
end
|
684
|
-
|
685
|
-
# next matching brace for brace at from
|
686
|
-
def close_brace_pos(css, from, depth = 0)
|
687
|
-
s = CharStringScanner.new(css[from..-1])
|
688
|
-
while (b = s.scan_next(BRACE_RE))
|
689
|
-
depth += (b == '}' ? -1 : +1)
|
690
|
-
break if depth.zero?
|
691
|
-
end
|
692
|
-
raise "match not found for {" unless depth.zero?
|
693
|
-
from + s.pos - 1
|
694
|
-
end
|
695
|
-
|
696
|
-
# opening brace position from +from+ (search backwards)
|
697
|
-
def open_brace_pos(css, from, depth = 0)
|
698
|
-
s = CharStringScanner.new(css[0..from].reverse)
|
699
|
-
while (b = s.scan_next(BRACE_RE_REVERSE))
|
700
|
-
depth += (b == '{' ? +1 : -1)
|
701
|
-
break if depth.zero?
|
702
|
-
end
|
703
|
-
raise "matching { brace not found" unless depth.zero?
|
704
|
-
from - s.pos + 1
|
705
|
-
end
|
706
|
-
|
707
|
-
# insert substitutions into text at positions (Range or Fixnum)
|
708
|
-
# substitutions can be passed as array or as yields from the &block called with |substring, position, text|
|
709
|
-
# position is a range (begin..end)
|
710
|
-
def replace_substrings_at(text, positions, replacements = nil, &block)
|
711
|
-
offset = 0
|
712
|
-
positions.each_with_index do |p, i|
|
713
|
-
p = (p...p) if p.is_a?(Fixnum)
|
714
|
-
from = p.begin + offset
|
715
|
-
to = p.end + offset
|
716
|
-
p = p.exclude_end? ? (from...to) : (from..to)
|
717
|
-
# block returns the substitution, e.g.: { |text, pos| text[pos].upcase }
|
718
|
-
r = replacements ? replacements[i] : block.call(text[p], p, text)
|
719
|
-
text[p] = r
|
720
|
-
# add the change in length to offset
|
721
|
-
offset += r.size - (p.end - p.begin + (p.exclude_end? ? 0 : 1))
|
722
|
-
end
|
723
|
-
text
|
724
|
-
end
|
725
|
-
|
726
|
-
def get_json(url)
|
727
|
-
JSON.parse get_file(url)
|
728
|
-
end
|
729
|
-
|
730
|
-
# regular string scanner works with bytes
|
731
|
-
# this one works with chars and provides #scan_next
|
732
|
-
class CharStringScanner
|
733
|
-
extend Forwardable
|
734
|
-
|
735
|
-
def initialize(*args)
|
736
|
-
@s = StringScanner.new(*args)
|
737
|
-
end
|
738
|
-
|
739
|
-
def_delegators :@s, :scan_until, :skip_until, :string
|
740
|
-
|
741
|
-
# advance scanner to pos after the next match of pattern and return the match
|
742
|
-
def scan_next(pattern)
|
743
|
-
return unless @s.scan_until(pattern)
|
744
|
-
@s.matched
|
745
|
-
end
|
746
|
-
|
747
|
-
def pos
|
748
|
-
byte_to_str_pos @s.pos
|
749
|
-
end
|
750
|
-
|
751
|
-
def pos=(i)
|
752
|
-
@s.pos = str_to_byte_pos i
|
753
|
-
i
|
754
|
-
end
|
755
|
-
|
756
|
-
private
|
757
|
-
|
758
|
-
def byte_to_str_pos(pos)
|
759
|
-
@s.string.byteslice(0, pos).length
|
760
|
-
end
|
761
|
-
|
762
|
-
def str_to_byte_pos(pos)
|
763
|
-
@s.string.slice(0, pos).bytesize
|
764
|
-
end
|
765
|
-
end
|
766
|
-
|
767
|
-
class Logger
|
768
|
-
include Term::ANSIColor
|
769
|
-
|
770
|
-
def initialize(env)
|
771
|
-
@env = env
|
772
|
-
puts bold "Convert Bootstrap LESS to SASS"
|
773
|
-
puts " repo : #{env[:repo]}"
|
774
|
-
puts " branch : #{env[:branch]} #{dark "#{env[:repo]}/tree/#{env[:branch_sha]}"}"
|
775
|
-
puts " save to: #{@env[:save_at].to_json}"
|
776
|
-
puts " twbs cache: #{@env[:cache_path]}"
|
777
|
-
puts dark "-" * 60
|
778
|
-
end
|
779
|
-
|
780
|
-
def log_status(status)
|
781
|
-
puts bold status
|
782
|
-
end
|
783
|
-
|
784
|
-
def log_file_info(s)
|
785
|
-
puts " #{magenta s}"
|
786
|
-
end
|
787
|
-
|
788
|
-
def log_transform(*args)
|
789
|
-
puts "#{cyan " #{caller[1][/`.*'/][1..-2].sub(/^block in /, '')}"}#{cyan ": #{args * ', '}" unless args.empty?}"
|
790
|
-
end
|
791
|
-
|
792
|
-
def log_processing(name)
|
793
|
-
puts yellow " #{File.basename(name)}"
|
794
|
-
end
|
795
|
-
|
796
|
-
def log_processed(name)
|
797
|
-
puts green " #{name}"
|
798
|
-
end
|
799
|
-
|
800
|
-
def log_http_get_file(url, cached = false)
|
801
|
-
s = " #{'CACHED ' if cached}GET #{url}..."
|
802
|
-
if cached
|
803
|
-
puts dark green s
|
804
|
-
else
|
805
|
-
puts dark cyan s
|
806
|
-
end
|
807
|
-
end
|
808
|
-
|
809
|
-
def log_http_get_files(files, from, cached = false)
|
810
|
-
s = " #{'CACHED ' if cached}GET #{files.length} files from #{from} #{files * ' '}..."
|
811
|
-
if cached
|
812
|
-
puts dark green s
|
813
|
-
else
|
814
|
-
puts dark cyan s
|
815
|
-
end
|
816
|
-
end
|
817
|
-
|
818
|
-
def puts(*args)
|
819
|
-
STDOUT.puts *args unless @silence
|
820
|
-
end
|
821
|
-
|
822
|
-
def silence_log
|
823
|
-
@silence = true
|
824
|
-
yield
|
825
|
-
ensure
|
826
|
-
@silence = false
|
827
|
-
end
|
828
|
-
end
|
829
72
|
end
|