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.

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