bootstrap-sass 3.0.0.0 → 3.0.1.0.rc

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bootstrap-sass might be problematic. Click here for more details.

Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bootstrap-sass/version.rb +2 -2
  3. data/tasks/converter.rb +16 -773
  4. data/tasks/converter/char_string_scanner.rb +38 -0
  5. data/tasks/converter/fonts_conversion.rb +12 -0
  6. data/tasks/converter/js_conversion.rb +22 -0
  7. data/tasks/converter/less_conversion.rb +612 -0
  8. data/tasks/converter/logger.rb +64 -0
  9. data/tasks/converter/network.rb +110 -0
  10. data/test/compilation_test.rb +5 -1
  11. data/test/dummy/app/views/pages/root.html.slim +10 -2
  12. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot +0 -0
  13. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg +200 -199
  14. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf +0 -0
  15. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff +0 -0
  16. data/vendor/assets/javascripts/bootstrap/affix.js +2 -2
  17. data/vendor/assets/javascripts/bootstrap/alert.js +1 -1
  18. data/vendor/assets/javascripts/bootstrap/button.js +1 -1
  19. data/vendor/assets/javascripts/bootstrap/carousel.js +2 -2
  20. data/vendor/assets/javascripts/bootstrap/collapse.js +2 -2
  21. data/vendor/assets/javascripts/bootstrap/dropdown.js +2 -2
  22. data/vendor/assets/javascripts/bootstrap/modal.js +2 -2
  23. data/vendor/assets/javascripts/bootstrap/popover.js +2 -2
  24. data/vendor/assets/javascripts/bootstrap/scrollspy.js +2 -2
  25. data/vendor/assets/javascripts/bootstrap/tab.js +3 -3
  26. data/vendor/assets/javascripts/bootstrap/tooltip.js +2 -2
  27. data/vendor/assets/javascripts/bootstrap/transition.js +1 -1
  28. data/vendor/assets/stylesheets/bootstrap/_alerts.scss +1 -1
  29. data/vendor/assets/stylesheets/bootstrap/_breadcrumbs.scss +2 -2
  30. data/vendor/assets/stylesheets/bootstrap/_button-groups.scss +6 -1
  31. data/vendor/assets/stylesheets/bootstrap/_buttons.scss +2 -4
  32. data/vendor/assets/stylesheets/bootstrap/_carousel.scss +32 -10
  33. data/vendor/assets/stylesheets/bootstrap/_code.scss +5 -8
  34. data/vendor/assets/stylesheets/bootstrap/_dropdowns.scss +1 -2
  35. data/vendor/assets/stylesheets/bootstrap/_forms.scss +16 -3
  36. data/vendor/assets/stylesheets/bootstrap/_glyphicons.scss +16 -11
  37. data/vendor/assets/stylesheets/bootstrap/_grid.scss +32 -285
  38. data/vendor/assets/stylesheets/bootstrap/_input-groups.scss +9 -0
  39. data/vendor/assets/stylesheets/bootstrap/_jumbotron.scss +2 -2
  40. data/vendor/assets/stylesheets/bootstrap/_list-group.scss +15 -17
  41. data/vendor/assets/stylesheets/bootstrap/_mixins.scss +160 -29
  42. data/vendor/assets/stylesheets/bootstrap/_modals.scss +2 -16
  43. data/vendor/assets/stylesheets/bootstrap/_navbar.scss +10 -7
  44. data/vendor/assets/stylesheets/bootstrap/_navs.scss +53 -20
  45. data/vendor/assets/stylesheets/bootstrap/_normalize.scss +16 -6
  46. data/vendor/assets/stylesheets/bootstrap/_pagination.scss +2 -0
  47. data/vendor/assets/stylesheets/bootstrap/_panels.scss +31 -7
  48. data/vendor/assets/stylesheets/bootstrap/_print.scss +6 -1
  49. data/vendor/assets/stylesheets/bootstrap/_progress-bars.scss +4 -7
  50. data/vendor/assets/stylesheets/bootstrap/_responsive-utilities.scss +57 -68
  51. data/vendor/assets/stylesheets/bootstrap/_scaffolding.scss +1 -12
  52. data/vendor/assets/stylesheets/bootstrap/_tables.scss +40 -40
  53. data/vendor/assets/stylesheets/bootstrap/_theme.scss +26 -11
  54. data/vendor/assets/stylesheets/bootstrap/_thumbnails.scss +6 -6
  55. data/vendor/assets/stylesheets/bootstrap/_tooltip.scss +8 -8
  56. data/vendor/assets/stylesheets/bootstrap/_type.scss +71 -30
  57. data/vendor/assets/stylesheets/bootstrap/_utilities.scss +15 -1
  58. data/vendor/assets/stylesheets/bootstrap/_variables.scss +56 -39
  59. data/vendor/assets/stylesheets/bootstrap/bootstrap.scss +0 -10
  60. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f1e4cbf951a9a343944ac462a39d6b43bebfc6ec
4
- data.tar.gz: b62ae642dcf8ab632639863c06ba4a780de0f182
3
+ metadata.gz: 12e4060b2ae1d151a7e2de1e9fd65daedfc720a5
4
+ data.tar.gz: 6ea5e7065f2cdee8cd1f06918661dbe60d622139
5
5
  SHA512:
6
- metadata.gz: d0a03a10a35d447a05d4be0b408d130667660bd6883b115c8f1c104bedfb0763b81539de9de31d64007cb14c9da376116e0b1d6d5005bb89951d7d44fefae60d
7
- data.tar.gz: 5c054a06f4bae587b0565b892181a607c2bd155afa53be377e5c89f4a676ddc57d3d920d29e22aa07b614c85ac6752ac3f78c805fab8e8a3f7c6851dfa987a68
6
+ metadata.gz: e2e53efa06afc231a19ab780638ac312cb91a7c8cb1c80ad192cbffe70f50341a80fd1216b55e467e5ac56992f47855d9c3c20ec1a215767ff0e6f331ac0f452
7
+ data.tar.gz: 8cca5905200e5167e222b8eb863f00f2d4ed69c4d8d802662cfd2e23b866370e3de6c0c828573153246ac44eb0863be2b2aaf49c337e8002c157eea2e6df45a9
@@ -1,4 +1,4 @@
1
1
  module Bootstrap
2
- VERSION = '3.0.0.0'
3
- BOOTSTRAP_SHA = 'e8a1df5f060bf7e6631554648e0abde150aedbe4'
2
+ VERSION = '3.0.1.0.rc'
3
+ BOOTSTRAP_SHA = '867e2bef8d9c9b901022899227b306a532f5baf3'
4
4
  end
@@ -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
- GIT_DATA = 'https://api.github.com/repos'
28
- GIT_RAW = 'https://raw.github.com'
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 process_font_assets
53
- log_status "Processing fonts..."
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