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
@@ -0,0 +1,38 @@
1
+ # regular string scanner works with bytes
2
+ # this one works with chars and provides #scan_next
3
+ class Converter
4
+ class CharStringScanner
5
+ extend Forwardable
6
+
7
+ def initialize(*args)
8
+ @s = StringScanner.new(*args)
9
+ end
10
+
11
+ def_delegators :@s, :scan_until, :skip_until, :string
12
+
13
+ # advance scanner to pos after the next match of pattern and return the match
14
+ def scan_next(pattern)
15
+ return unless @s.scan_until(pattern)
16
+ @s.matched
17
+ end
18
+
19
+ def pos
20
+ byte_to_str_pos @s.pos
21
+ end
22
+
23
+ def pos=(i)
24
+ @s.pos = str_to_byte_pos i
25
+ i
26
+ end
27
+
28
+ private
29
+
30
+ def byte_to_str_pos(pos)
31
+ @s.string.byteslice(0, pos).length
32
+ end
33
+
34
+ def str_to_byte_pos(pos)
35
+ @s.string.slice(0, pos).bytesize
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ class Converter
2
+ module FontsConversion
3
+ def process_font_assets
4
+ log_status 'Processing fonts...'
5
+ files = read_files('fonts', bootstrap_font_files)
6
+ save_at = @save_at[:fonts]
7
+ files.each do |name, content|
8
+ save_file "#{save_at}/#{name}", content
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ class Converter
2
+ module JsConversion
3
+ def process_javascript_assets
4
+ log_status 'Processing javascripts...'
5
+ save_at = @save_at[:js]
6
+ read_files('js', bootstrap_js_files).each do |name, file|
7
+ save_file("#{save_at}/#{name}", file)
8
+ end
9
+ log_processed "#{bootstrap_js_files * ' '}"
10
+
11
+ log_status 'Updating javascript manifest'
12
+ content = ''
13
+ bootstrap_js_files.each do |name|
14
+ name = name.gsub(/\.js$/, '')
15
+ content << "//= require bootstrap/#{name}\n"
16
+ end
17
+ path = 'vendor/assets/javascripts/bootstrap.js'
18
+ save_file(path, content)
19
+ log_processed path
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,612 @@
1
+ require_relative 'char_string_scanner'
2
+
3
+ # This module transforms LESS into SCSS.
4
+ # It is implemented via lots of string manipulation: scanning back and forwards for regexps and doing substitions.
5
+ # Since it does not parse the LESS into an AST, bits of it may assume LESS to be formatted a certain way, and only limited,
6
+ # static analysis can be performed. This approach has so far been enough to automatically convert all of twbs/bootstrap.
7
+ class Converter
8
+ module LessConversion
9
+ # Some regexps for matching bits of SCSS:
10
+ selector_char = '\[\]$\w\-{}#,.:&>@'
11
+ # 1 selector (the part before the {)
12
+ SELECTOR_RE = /[#{selector_char}]+[#{selector_char}\s]*/
13
+ # 1 // comment
14
+ COMMENT_RE = %r((?:^[ \t]*//[^\n]*\n))
15
+ # 1 {, except when part of @{ and #{
16
+ RULE_OPEN_BRACE_RE = /(?<![@#\$])\{/
17
+ # same as the one above, but in reverse (on a reversed string)
18
+ RULE_OPEN_BRACE_RE_REVERSE = /\{(?![@#\$])/
19
+ # match closed brace, except when \w precedes }, or when }[.'"]. a heurestic to exclude } that are not selector body close }
20
+ RULE_CLOSE_BRACE_RE = /(?<!\w)\}(?![.'"])/
21
+ RULE_CLOSE_BRACE_RE_REVERSE = /(?<![.'"])\}(?!\w)/
22
+ # match any brace that opens or closes a properties body
23
+ BRACE_RE = /#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m
24
+ BRACE_RE_REVERSE = /#{RULE_OPEN_BRACE_RE_REVERSE}|#{RULE_CLOSE_BRACE_RE_REVERSE}/m
25
+ # valid
26
+ SCSS_MIXIN_DEF_ARGS_RE = /[\w\-,\s$:#%()]*/
27
+ LESS_MIXIN_DEF_ARGS_RE = /[\w\-,;.\s@:#%()]*/
28
+
29
+ # These mixins are nested (not supported by SCSS), and need to flattened:
30
+ NESTED_MIXINS = {'#gradient' => 'gradient'}
31
+
32
+ # These mixins will get vararg definitions in SCSS (not supported by LESS):
33
+ VARARG_MIXINS = %w(
34
+ transition transition-transform box-shadow
35
+ )
36
+
37
+ def process_stylesheet_assets
38
+ log_status "Processing stylesheets..."
39
+ files = read_files('less', bootstrap_less_files)
40
+
41
+ # read common mixin definitions (incl. nested mixins) from mixins.less
42
+ read_shared_mixins! files['mixins.less']
43
+ @shared_mixins << 'make-grid'
44
+
45
+ # convert each file
46
+ files.each do |name, file|
47
+ log_processing name
48
+ # apply common conversions
49
+ file = convert_to_scss(file)
50
+ case name
51
+ when 'mixins.less'
52
+ NESTED_MIXINS.each do |selector, prefix|
53
+ file = flatten_mixins(file, selector, prefix)
54
+ end
55
+ file = varargify_mixin_definitions(file, *VARARG_MIXINS)
56
+ file = deinterpolate_vararg_mixins(file)
57
+ file = parameterize_mixin_parent_selector file, 'responsive-(in)?visibility'
58
+ file = parameterize_mixin_parent_selector file, 'input-size'
59
+ file = replace_ms_filters(file)
60
+ file = replace_all file, /\.\$state/, '.#{$state}'
61
+ file = replace_all file, /,\s*\.open \.dropdown-toggle& \{(.*?)\}/m,
62
+ " {\\1}\n .open & { &.dropdown-toggle {\\1} }"
63
+ file = convert_grid_mixins file
64
+ when 'responsive-utilities.less'
65
+ file = apply_mixin_parent_selector(file, '&\.(visible|hidden)')
66
+ file = apply_mixin_parent_selector(file, '(?<!&)\.(visible|hidden)')
67
+ file = replace_rules(file, ' @media') { |r| unindent(r, 2) }
68
+ when 'variables.less'
69
+ file = insert_default_vars(file)
70
+ file = replace_all file, /(\$icon-font-path:).*(!default)/, '\1 "bootstrap/" \2'
71
+ when 'close.less'
72
+ # extract .close { button& {...} } rule
73
+ file = extract_nested_rule file, 'button&'
74
+ when 'modals.less'
75
+ # pre 3.0.1:
76
+ if file =~ /body&,/
77
+ file = replace_all file, /body&,(.*?)(\{.*?\})/m, "\\1\\2\nbody& \\2"
78
+ file = extract_nested_rule file, 'body&'
79
+ end
80
+ when 'dropdowns.less'
81
+ file = replace_all file, /(\s*)@extend \.pull-right-dropdown-menu;/, "\\1right: 0;\\1left: auto;"
82
+ when 'forms.less'
83
+ file = extract_nested_rule file, 'textarea&'
84
+ file = apply_mixin_parent_selector(file, '\.input-(?:sm|lg)')
85
+ when 'navbar.less'
86
+ file = replace_all file, /(\s*)\.navbar-(right|left)\s*\{\s*@extend\s*\.pull-(right|left);\s*/, "\\1.navbar-\\2 {\\1 float: \\2 !important;\\1"
87
+ when 'tables.less'
88
+ file = replace_all file, /(@include\s*table-row-variant\()(\w+)/, "\\1'\\2'"
89
+ when 'list-group.less'
90
+ file = extract_nested_rule file, 'a&'
91
+ when 'glyphicons.less'
92
+ file = replace_rules(file, '@font-face') { |rule|
93
+ rule = replace_all rule, /(\$icon-font-\w+)/, '#{\1}'
94
+ replace_all rule, /url\(/, 'font-url('
95
+ }
96
+ end
97
+
98
+ name = name.sub(/\.less$/, '.scss')
99
+ save_at = @save_at[:scss]
100
+ path = "#{save_at}/#{'_' unless name == 'bootstrap.scss'}#{name}"
101
+ save_file(path, file)
102
+ log_processed File.basename(path)
103
+ end
104
+ end
105
+
106
+ # apply general less to scss conversion
107
+ def convert_to_scss(file)
108
+ # mixins may also be defined in the file. get mixin names before doing any processing
109
+ mixin_names = (@shared_mixins + get_mixin_names(file)).uniq
110
+ file = replace_vars(file)
111
+ file = replace_file_imports(file)
112
+ file = replace_mixin_definitions file
113
+ file = replace_mixins file, mixin_names
114
+ # replace_less_extend does not seem to do anything. @glebm
115
+ file = replace_less_extend(file)
116
+ file = replace_spin(file)
117
+ file = replace_image_urls(file)
118
+ file = replace_image_paths(file)
119
+ file = replace_escaping(file)
120
+ file = convert_less_ampersand(file)
121
+ file = deinterpolate_vararg_mixins(file)
122
+ file = replace_calculation_semantics(file)
123
+ file
124
+ end
125
+
126
+ # convert grid mixins LESS when => SASS @if
127
+ def convert_grid_mixins(file)
128
+ file = replace_rules file, /@mixin make-grid-columns/, comments: false do |css, pos|
129
+ mxn_def = css.each_line.first
130
+ classes = if css =~ /-columns-float/
131
+ '.col-#{$class}-#{$i}'
132
+ else
133
+ '.col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}'
134
+ end
135
+ body = (css =~ /\$list \{\n(.*?)\n[ ]*\}/m) && $1
136
+ unindent <<-SASS, 8
137
+ // [converter] Grid converted to use SASS cycles (LESS uses recursive nested mixin defs not supported by SASS)
138
+ #{mxn_def.strip}
139
+ $list: '';
140
+ @for $i from 1 to $grid-columns {
141
+ $list: "#{classes}, \#{$list}";
142
+ }
143
+ $i: $grid-columns;
144
+ $list: "\#{$list}, #{classes}";
145
+ \#{$list} {
146
+ #{unindent body}
147
+ }
148
+ }
149
+ SASS
150
+ end
151
+ file = replace_rules file, /@mixin calc-grid/ do |css|
152
+ css = indent css.gsub(/.*when \((.*?)\) {/, '@if \1 {').gsub(/(?<=\$type) = (\w+)/, ' == \1').gsub(/(?<=-)(\$[a-z]+)/, '#{\1}')
153
+ if css =~ /== width/
154
+ css = "@mixin calc-grid($index, $class, $type) {\n#{css}"
155
+ elsif css =~ /== offset/
156
+ css += "\n}"
157
+ end
158
+ css
159
+ end
160
+ file = replace_rules file, /@mixin make-grid\(/ do |css|
161
+ unindent <<-SASS, 8
162
+ // [converter] This is defined recursively in LESS, but SASS supports real loops
163
+ @mixin make-grid($columns, $class, $type) {
164
+ @for $i from 1 through $columns {
165
+ @include calc-grid($i, $class, $type);
166
+ }
167
+ }
168
+ SASS
169
+ end
170
+ file
171
+ end
172
+
173
+
174
+ # We need to keep a list of shared mixin names in order to convert the includes correctly
175
+ # Before doing any processing we read shared mixins from a file
176
+ # If a mixin is nested, it gets prefixed in the list (e.g. #gradient > .horizontal to 'gradient-horizontal')
177
+ def read_shared_mixins!(mixins_file)
178
+ log_status " Reading shared mixins from mixins.less"
179
+ @shared_mixins = get_mixin_names(mixins_file, silent: true)
180
+ NESTED_MIXINS.each do |selector, prefix|
181
+ # we use replace_rules without replacing anything just to use the parsing algorithm
182
+ replace_rules(mixins_file, selector) { |rule|
183
+ @shared_mixins += get_mixin_names(unindent(unwrap_rule_block(rule)), silent: true).map { |name| "#{prefix}-#{name}" }
184
+ rule
185
+ }
186
+ end
187
+ @shared_mixins.sort!
188
+ log_file_info "shared mixins: #{@shared_mixins * ', '}"
189
+ @shared_mixins
190
+ end
191
+
192
+ def get_mixin_names(file, opts = {})
193
+ names = get_css_selectors(file).join("\n" * 2).scan(/^\.([\w-]+)\(#{LESS_MIXIN_DEF_ARGS_RE}\)[ ]*\{/).map(&:first).uniq.sort
194
+ log_file_info "mixin defs: #{names * ', '}" unless opts[:silent] || names.empty?
195
+ names
196
+ end
197
+
198
+ # margin: a -b
199
+ # LESS: sets 2 values
200
+ # SASS: sets 1 value (a-b)
201
+ # This wraps a and -b so they evaluates to 2 values in SASS
202
+ def replace_calculation_semantics(file)
203
+ # split_prop_val.call('(@navbar-padding-vertical / 2) -@navbar-padding-horizontal')
204
+ # #=> ["(navbar-padding-vertical / 2)", "-navbar-padding-horizontal"]
205
+ split_prop_val = proc { |val|
206
+ s = CharStringScanner.new(val)
207
+ r = []
208
+ buff = ''
209
+ d = 0
210
+ prop_char = %r([\$\w\-/\*\+%!])
211
+ while (token = s.scan_next(/([\)\(]|\s+|#{prop_char}+)/))
212
+ buff << token
213
+ case token
214
+ when '('
215
+ d += 1
216
+ when ')'
217
+ d -= 1
218
+ if d == 0
219
+ r << buff
220
+ buff = ''
221
+ end
222
+ when /\s/
223
+ if d == 0 && !buff.strip.empty?
224
+ r << buff
225
+ buff = ''
226
+ end
227
+ end
228
+ end
229
+ r << buff unless buff.empty?
230
+ r.map(&:strip)
231
+ }
232
+
233
+ replace_rules file do |rule|
234
+ replace_properties rule do |props|
235
+ props.gsub /(?<!\w)([\w-]+):(.*?);/ do |m|
236
+ prop, vals = $1, split_prop_val.call($2)
237
+ next m unless vals.length >= 2 && vals.any? { |v| v =~ /^[\+\-]\$/ }
238
+ transformed = vals.map { |v| v.strip =~ %r(^\(.*\)$) ? v : "(#{v})" }
239
+ log_transform "property #{prop}: #{transformed * ' '}"
240
+ "#{prop}: #{transformed * ' '};"
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ # @import "file.less" to "#{target_path}file;"
247
+ def replace_file_imports(less, target_path = 'bootstrap/')
248
+ less.gsub %r([@\$]import ["|']([\w-]+).less["|'];),
249
+ %Q(@import "#{target_path}\\1";)
250
+ end
251
+
252
+ def replace_all(file, regex, replacement = nil, &block)
253
+ log_transform regex, replacement
254
+ new_file = file.gsub(regex, replacement, &block)
255
+ raise "replace_all #{regex}, #{replacement} NO MATCH" if file == new_file
256
+ new_file
257
+ end
258
+
259
+ # @mixin a() { tr& { color:white } }
260
+ # to:
261
+ # @mixin a($parent) { tr#{$parent} { color: white } }
262
+ def parameterize_mixin_parent_selector(file, rule_sel)
263
+ log_transform rule_sel
264
+ param = '$parent'
265
+ replace_rules(file, '^[ \t]*@mixin\s*' + rule_sel) do |mxn_css|
266
+ mxn_css.sub! /(?=@mixin)/, "// [converter] $parent hack\n"
267
+ # insert param into mixin def
268
+ mxn_css.sub!(/(@mixin [\w-]+)\(([\$\w\-,\s]*)\)/) { "#{$1}(#{param}#{', ' if $2 && !$2.empty?}#{$2})" }
269
+ # wrap properties in #{$parent} { ... }
270
+ replace_properties(mxn_css) { |props| props.strip.empty? ? props : " \#{#{param}} { #{props.strip} }\n " }
271
+ # change nested& rules to nested#{$parent}
272
+ replace_rules(mxn_css, /.*&[ ,]/) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
273
+ end
274
+ end
275
+
276
+ # extracts rule immediately after it's parent, and adjust the selector
277
+ # .x { textarea& { ... }}
278
+ # to:
279
+ # .x { ... }
280
+ # textarea.x { ... }
281
+ def extract_nested_rule(file, selector, new_selector = nil)
282
+ matches = []
283
+ # first find the rules, and remove them
284
+ file = replace_rules(file, "\s*#{selector}", comments: true) { |rule, pos, css|
285
+ matches << [rule, pos]
286
+ new_selector ||= "#{get_selector(rule).sub(/&$/, '')}#{selector_for_pos(css, pos.begin)}"
287
+ indent "// [converter] extracted #{get_selector(rule)} to #{new_selector}", indent_width(rule)
288
+ }
289
+ log_transform selector, new_selector
290
+ # replace rule selector with new_selector
291
+ matches.each do |m|
292
+ m[0].sub! /(#{COMMENT_RE}*)^(\s*).*?(\s*){/m, "\\1\\2#{new_selector}\\3{"
293
+ end
294
+ replace_substrings_at file,
295
+ matches.map { |_, pos| close_brace_pos(file, pos.begin, 1) + 1 },
296
+ matches.map { |rule, _| "\n\n" + unindent(rule) }
297
+ end
298
+
299
+ # .visible-sm { @include responsive-visibility() }
300
+ # to:
301
+ # @include responsive-visibility('.visible-sm')
302
+ def apply_mixin_parent_selector(file, rule_sel)
303
+ log_transform rule_sel
304
+ replace_rules file, '\s*' + rule_sel, comments: false do |rule, rule_pos, css|
305
+ body = unwrap_rule_block(rule.dup).strip
306
+ next rule unless body =~ /^@include \w+/m || body =~ /^@media/ && body =~ /\{\s*@include/
307
+ rule =~ /(#{COMMENT_RE}*)(#{SELECTOR_RE})\{/
308
+ cmt, sel = $1, $2.strip
309
+ # take one up selector chain if this is an &. selector
310
+ if sel.start_with?('&')
311
+ parent_sel = selector_for_pos(css, rule_pos.begin)
312
+ sel = parent_sel + sel[1..-1]
313
+ end
314
+ # unwrap, and replace @include
315
+ unindent unwrap_rule_block(rule).gsub(/(@include [\w-]+)\(([\$\w\-,\s]*)\)/) {
316
+ "#{cmt}#{$1}('#{sel}'#{', ' if $2 && !$2.empty?}#{$2})"
317
+ }
318
+ end
319
+ end
320
+
321
+ # #gradient > { @mixin horizontal ... }
322
+ # to:
323
+ # @mixin gradient-horizontal
324
+ def flatten_mixins(file, container, prefix)
325
+ log_transform container, prefix
326
+ replace_rules file, Regexp.escape(container) do |mixins_css|
327
+ unindent unwrap_rule_block(mixins_css).gsub(/@mixin\s*([\w-]+)/, "@mixin #{prefix}-\\1")
328
+ end
329
+ end
330
+
331
+ # Replaces the following:
332
+ # .mixin() -> @include mixin()
333
+ # #scope > .mixin() -> @include scope-mixin()
334
+ def replace_mixins(less, mixin_names)
335
+ mixin_pattern = /(\s+)(([#|\.][\w-]+\s*>\s*)*)\.([\w-]+\(.*\))(?!\s\{)/
336
+
337
+ less.gsub(mixin_pattern) do |match|
338
+ matches = match.scan(mixin_pattern).flatten
339
+ scope = matches[1] || ''
340
+ if scope != ''
341
+ scope = scope.scan(/[\w-]+/).join('-') + '-'
342
+ end
343
+ mixin_name = match.scan(/\.([\w-]+)\(.*\)\s?\{?/).first
344
+ if mixin_name && mixin_names.include?("#{scope}#{mixin_name.first}")
345
+ "#{matches.first}@include #{scope}#{matches.last}".gsub(/; \$/, ", $").sub(/;\)$/, ')')
346
+ else
347
+ "#{matches.first}@extend .#{scope}#{matches.last.gsub(/\(\)/, '')}"
348
+ end
349
+ end
350
+ end
351
+
352
+ # change Microsoft filters to SASS calling convention
353
+ def replace_ms_filters(file)
354
+ log_transform
355
+ file.gsub(
356
+ /filter: e\(%\("progid:DXImageTransform.Microsoft.gradient\(startColorstr='%d', endColorstr='%d', GradientType=(\d)\)",argb\(([\-$\w]+)\),argb\(([\-$\w]+)\)\)\);/,
357
+ %Q(filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='\#{ie-hex-str(\\2)}', endColorstr='\#{ie-hex-str(\\3)}', GradientType=\\1);)
358
+ )
359
+ end
360
+
361
+ # unwraps topmost rule block
362
+ # #sel { a: b; }
363
+ # to:
364
+ # a: b;
365
+ def unwrap_rule_block(css)
366
+ css[(css =~ RULE_OPEN_BRACE_RE) + 1..-1].sub(/\n?}\s*\z/m, '')
367
+ end
368
+
369
+ def replace_mixin_definitions(less)
370
+ less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
371
+ "#{$1}@mixin #{$2.tr(';', ',')}#{$3}".sub(/,\)/, ')')
372
+ }
373
+ end
374
+
375
+ def replace_vars(less)
376
+ less = less.dup
377
+ # skip header comment
378
+ less =~ %r(\A/\*(.*?)\*/)m
379
+ from = $~ ? $~.to_s.length : 0
380
+ less[from..-1] = less[from..-1].
381
+ gsub(/(?!@mixin|@media|@page|@keyframes|@font-face|@-\w)@/, '$').
382
+ # variables that would be ignored by gsub above: e.g. @page-header-border-color
383
+ gsub(/@(page[\w-]+)/, '$\1')
384
+ less
385
+ end
386
+
387
+ # #gradient > .horizontal()
388
+ # to:
389
+ # @include .horizontal-gradient()
390
+ def replace_less_extend(less)
391
+ less.gsub(/\#(\w+) \> \.([\w-]*)(\(.*\));?/, '@include \1-\2\3;')
392
+ end
393
+
394
+ def replace_spin(less)
395
+ less.gsub(/(?![\-$@.])spin(?!-)/, 'adjust-hue')
396
+ end
397
+
398
+ def replace_image_urls(less)
399
+ less.gsub(/background-image: url\("?(.*?)"?\);/) { |s| "background-image: image-url(\"#{$1}\");" }
400
+ end
401
+
402
+ def replace_image_paths(less)
403
+ less.gsub('../img/', '')
404
+ end
405
+
406
+ def replace_escaping(less)
407
+ less = less.gsub(/\~"([^"]+)"/, '#{\1}') # Get rid of ~"" escape
408
+ less.gsub!(/\$\{([^}]+)\}/, '$\1') # Get rid of @{} escape
409
+ less.gsub!(/"([^"\n]*)(\$[\w\-]+)([^"\n]*)"/, '"\1#{\2}\3"') # interpolate variable in string, e.g. url("$file-1x") => url("#{$file-1x}")
410
+ less.gsub(/(\W)e\(%\("?([^"]*)"?\)\)/, '\1\2') # Get rid of e(%("")) escape
411
+ end
412
+
413
+ def insert_default_vars(scss)
414
+ log_transform
415
+ scss.gsub(/^(\$.+);/, '\1 !default;')
416
+ end
417
+
418
+ # Converts &-
419
+ def convert_less_ampersand(less)
420
+ regx = /^\.badge\s*\{[\s\/\w\(\)]+(&{1}-{1})\w.*?^}$/m
421
+
422
+ tmp = ''
423
+ less.scan(/^(\s*&)(-[\w\[\]]+\s*{.+})$/) do |ampersand, css|
424
+ tmp << ".badge#{css}\n"
425
+ end
426
+
427
+ less.gsub(regx, tmp)
428
+ end
429
+
430
+ # unindent by n spaces
431
+ def unindent(txt, n = 2)
432
+ txt.gsub /^[ ]{#{n}}/, ''
433
+ end
434
+
435
+ # indent by n spaces
436
+ def indent(txt, n = 2)
437
+ spaces = ' ' * n
438
+ txt.gsub /^/, spaces
439
+ end
440
+
441
+ # get indent length from the first line of txt
442
+ def indent_width(txt)
443
+ txt.match(/\A\s*/).to_s.length
444
+ end
445
+
446
+ # @mixin transition($transition) {
447
+ # to:
448
+ # @mixin transition($transition...) {
449
+ def varargify_mixin_definitions(scss, *mixins)
450
+ log_transform *mixins
451
+ scss = scss.dup
452
+ mixins.each do |mixin|
453
+ scss.gsub! /(@mixin\s*#{Regexp.quote(mixin)})\((#{SCSS_MIXIN_DEF_ARGS_RE})\)/, '\1(\2...)'
454
+ end
455
+ scss
456
+ end
457
+
458
+ # @include transition(#{border-color ease-in-out .15s, box-shadow ease-in-out .15s})
459
+ # to
460
+ # @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s)
461
+ def deinterpolate_vararg_mixins(scss)
462
+ scss = scss.dup
463
+ VARARG_MIXINS.each do |mixin|
464
+ if scss.gsub! /(@include\s*#{Regexp.quote(mixin)})\(\s*\#\{([^}]+)\}\s*\)/, '\1(\2)'
465
+ log_transform mixin
466
+ end
467
+ end
468
+ scss
469
+ end
470
+
471
+ # get full selector for rule_block
472
+ def get_selector(rule_block)
473
+ /^\s*(#{SELECTOR_RE}?)\s*\{/.match(rule_block) && $1 && $1.strip
474
+ end
475
+
476
+ # replace CSS rule blocks matching rule_prefix with yield(rule_block, rule_pos)
477
+ # will also include immediately preceding comments in rule_block
478
+ #
479
+ # option :comments -- include immediately preceding comments in rule_block
480
+ #
481
+ # replace_rules(".a{ \n .b{} }", '.b') { |rule, pos| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
482
+ def replace_rules(less, rule_prefix = SELECTOR_RE, options = {}, &block)
483
+ options = {comments: true}.merge(options || {})
484
+ less = less.dup
485
+ s = CharStringScanner.new(less)
486
+ rule_re = /(?:#{rule_prefix}[^{]*#{RULE_OPEN_BRACE_RE})/
487
+ if options[:comments]
488
+ rule_start_re = /(?:#{COMMENT_RE}*)^#{rule_re}/
489
+ else
490
+ rule_start_re = /^#{rule_re}/
491
+ end
492
+
493
+ positions = []
494
+ while (rule_start = s.scan_next(rule_start_re))
495
+ pos = s.pos
496
+ positions << (pos - rule_start.length..close_brace_pos(less, pos - 1))
497
+ end
498
+ replace_substrings_at(less, positions, &block)
499
+ less
500
+ end
501
+
502
+ # Get a all top-level selectors (with {)
503
+ def get_css_selectors(css, opts = {})
504
+ s = CharStringScanner.new(css)
505
+ selectors = []
506
+ while s.scan_next(RULE_OPEN_BRACE_RE)
507
+ brace_pos = s.pos
508
+ def_pos = css_def_pos(css, brace_pos+1, -1)
509
+ sel = css[def_pos.begin..brace_pos - 1].dup
510
+ sel.strip! if opts[:strip]
511
+ selectors << sel
512
+ sel.dup.strip
513
+ s.pos = close_brace_pos(css, brace_pos, 1) + 1
514
+ end
515
+ selectors
516
+ end
517
+
518
+ # replace in the top-level selector
519
+ # replace_in_selector('a {a: {a: a} } a {}', /a/, 'b') => 'b {a: {a: a} } b {}'
520
+ def replace_in_selector(css, pattern, sub)
521
+ # scan for selector positions in css
522
+ s = CharStringScanner.new(css)
523
+ prev_pos = 0
524
+ sel_pos = []
525
+ while (brace = s.scan_next(RULE_OPEN_BRACE_RE))
526
+ pos = s.pos
527
+ sel_pos << (prev_pos .. pos - 1)
528
+ s.pos = close_brace_pos(css, s.pos - 1) + 1
529
+ prev_pos = pos
530
+ end
531
+ replace_substrings_at(css, sel_pos) { |s| s.gsub(pattern, sub) }
532
+ end
533
+
534
+
535
+ # replace first level properties in the css with yields
536
+ # replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
537
+ def replace_properties(css, &block)
538
+ s = CharStringScanner.new(css)
539
+ s.skip_until /#{RULE_OPEN_BRACE_RE}\n?/
540
+ prev_pos = s.pos
541
+ depth = 0
542
+ pos = []
543
+ while (b = s.scan_next(/#{SELECTOR_RE}#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m))
544
+ s_pos = s.pos
545
+ depth += (b == '}' ? -1 : +1)
546
+ if depth == 1
547
+ if b == '}'
548
+ prev_pos = s_pos
549
+ else
550
+ pos << (prev_pos .. s_pos - b.length - 1)
551
+ end
552
+ end
553
+ end
554
+ replace_substrings_at css, pos, &block
555
+ end
556
+
557
+
558
+ # immediate selector of css at pos
559
+ def selector_for_pos(css, pos, depth = -1)
560
+ css[css_def_pos(css, pos, depth)].dup.strip
561
+ end
562
+
563
+ # get the pos of css def at pos (search backwards)
564
+ def css_def_pos(css, pos, depth = -1)
565
+ to = open_brace_pos(css, pos, depth)
566
+ prev_def = to - (css[0..to].reverse.index('}') || to) + 1
567
+ from = prev_def + 1 + (css[prev_def + 1..-1] =~ %r(^\s*[^\s/]))
568
+ (from..to - 1)
569
+ end
570
+
571
+ # next matching brace for brace at from
572
+ def close_brace_pos(css, from, depth = 0)
573
+ s = CharStringScanner.new(css[from..-1])
574
+ while (b = s.scan_next(BRACE_RE))
575
+ depth += (b == '}' ? -1 : +1)
576
+ break if depth.zero?
577
+ end
578
+ raise "match not found for {" unless depth.zero?
579
+ from + s.pos - 1
580
+ end
581
+
582
+ # opening brace position from +from+ (search backwards)
583
+ def open_brace_pos(css, from, depth = 0)
584
+ s = CharStringScanner.new(css[0..from].reverse)
585
+ while (b = s.scan_next(BRACE_RE_REVERSE))
586
+ depth += (b == '{' ? +1 : -1)
587
+ break if depth.zero?
588
+ end
589
+ raise "matching { brace not found" unless depth.zero?
590
+ from - s.pos + 1
591
+ end
592
+
593
+ # insert substitutions into text at positions (Range or Fixnum)
594
+ # substitutions can be passed as array or as yields from the &block called with |substring, position, text|
595
+ # position is a range (begin..end)
596
+ def replace_substrings_at(text, positions, replacements = nil, &block)
597
+ offset = 0
598
+ positions.each_with_index do |p, i|
599
+ p = (p...p) if p.is_a?(Fixnum)
600
+ from = p.begin + offset
601
+ to = p.end + offset
602
+ p = p.exclude_end? ? (from...to) : (from..to)
603
+ # block returns the substitution, e.g.: { |text, pos| text[pos].upcase }
604
+ r = replacements ? replacements[i] : block.call(text[p], p, text)
605
+ text[p] = r
606
+ # add the change in length to offset
607
+ offset += r.size - (p.end - p.begin + (p.exclude_end? ? 0 : 1))
608
+ end
609
+ text
610
+ end
611
+ end
612
+ end