bootstrap-sass-backport 3.2.0.2

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.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +19 -0
  4. data/CHANGELOG.md +146 -0
  5. data/CONTRIBUTING.md +79 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE +21 -0
  8. data/README.md +320 -0
  9. data/Rakefile +54 -0
  10. data/assets/fonts/bootstrap/glyphicons-halflings-regular.eot +0 -0
  11. data/assets/fonts/bootstrap/glyphicons-halflings-regular.svg +229 -0
  12. data/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf +0 -0
  13. data/assets/fonts/bootstrap/glyphicons-halflings-regular.woff +0 -0
  14. data/assets/images/.keep +0 -0
  15. data/assets/javascripts/bootstrap-sprockets.js +12 -0
  16. data/assets/javascripts/bootstrap.js +2107 -0
  17. data/assets/javascripts/bootstrap/affix.js +142 -0
  18. data/assets/javascripts/bootstrap/alert.js +92 -0
  19. data/assets/javascripts/bootstrap/button.js +110 -0
  20. data/assets/javascripts/bootstrap/carousel.js +223 -0
  21. data/assets/javascripts/bootstrap/collapse.js +170 -0
  22. data/assets/javascripts/bootstrap/dropdown.js +151 -0
  23. data/assets/javascripts/bootstrap/modal.js +280 -0
  24. data/assets/javascripts/bootstrap/popover.js +113 -0
  25. data/assets/javascripts/bootstrap/scrollspy.js +170 -0
  26. data/assets/javascripts/bootstrap/tab.js +128 -0
  27. data/assets/javascripts/bootstrap/tooltip.js +457 -0
  28. data/assets/javascripts/bootstrap/transition.js +59 -0
  29. data/assets/stylesheets/_bootstrap-compass.scss +7 -0
  30. data/assets/stylesheets/_bootstrap-mincer.scss +17 -0
  31. data/assets/stylesheets/_bootstrap-sprockets.scss +7 -0
  32. data/assets/stylesheets/_bootstrap.scss +50 -0
  33. data/assets/stylesheets/bootstrap/_alerts.scss +68 -0
  34. data/assets/stylesheets/bootstrap/_badges.scss +57 -0
  35. data/assets/stylesheets/bootstrap/_breadcrumbs.scss +26 -0
  36. data/assets/stylesheets/bootstrap/_button-groups.scss +240 -0
  37. data/assets/stylesheets/bootstrap/_buttons.scss +157 -0
  38. data/assets/stylesheets/bootstrap/_carousel.scss +243 -0
  39. data/assets/stylesheets/bootstrap/_close.scss +35 -0
  40. data/assets/stylesheets/bootstrap/_code.scss +68 -0
  41. data/assets/stylesheets/bootstrap/_component-animations.scss +35 -0
  42. data/assets/stylesheets/bootstrap/_dropdowns.scss +215 -0
  43. data/assets/stylesheets/bootstrap/_forms.scss +538 -0
  44. data/assets/stylesheets/bootstrap/_glyphicons.scss +237 -0
  45. data/assets/stylesheets/bootstrap/_grid.scss +84 -0
  46. data/assets/stylesheets/bootstrap/_input-groups.scss +166 -0
  47. data/assets/stylesheets/bootstrap/_jumbotron.scss +48 -0
  48. data/assets/stylesheets/bootstrap/_labels.scss +66 -0
  49. data/assets/stylesheets/bootstrap/_list-group.scss +131 -0
  50. data/assets/stylesheets/bootstrap/_media.scss +56 -0
  51. data/assets/stylesheets/bootstrap/_mixins.scss +39 -0
  52. data/assets/stylesheets/bootstrap/_modals.scss +150 -0
  53. data/assets/stylesheets/bootstrap/_navbar.scss +659 -0
  54. data/assets/stylesheets/bootstrap/_navs.scss +242 -0
  55. data/assets/stylesheets/bootstrap/_normalize.scss +425 -0
  56. data/assets/stylesheets/bootstrap/_pager.scss +55 -0
  57. data/assets/stylesheets/bootstrap/_pagination.scss +88 -0
  58. data/assets/stylesheets/bootstrap/_panels.scss +243 -0
  59. data/assets/stylesheets/bootstrap/_popovers.scss +133 -0
  60. data/assets/stylesheets/bootstrap/_print.scss +101 -0
  61. data/assets/stylesheets/bootstrap/_progress-bars.scss +105 -0
  62. data/assets/stylesheets/bootstrap/_responsive-embed.scss +34 -0
  63. data/assets/stylesheets/bootstrap/_responsive-utilities.scss +174 -0
  64. data/assets/stylesheets/bootstrap/_scaffolding.scss +150 -0
  65. data/assets/stylesheets/bootstrap/_tables.scss +233 -0
  66. data/assets/stylesheets/bootstrap/_theme.scss +258 -0
  67. data/assets/stylesheets/bootstrap/_thumbnails.scss +38 -0
  68. data/assets/stylesheets/bootstrap/_tooltip.scss +95 -0
  69. data/assets/stylesheets/bootstrap/_type.scss +304 -0
  70. data/assets/stylesheets/bootstrap/_utilities.scss +57 -0
  71. data/assets/stylesheets/bootstrap/_variables.scss +854 -0
  72. data/assets/stylesheets/bootstrap/_wells.scss +29 -0
  73. data/assets/stylesheets/bootstrap/mixins/_alerts.scss +14 -0
  74. data/assets/stylesheets/bootstrap/mixins/_background-variant.scss +11 -0
  75. data/assets/stylesheets/bootstrap/mixins/_border-radius.scss +18 -0
  76. data/assets/stylesheets/bootstrap/mixins/_buttons.scss +50 -0
  77. data/assets/stylesheets/bootstrap/mixins/_center-block.scss +7 -0
  78. data/assets/stylesheets/bootstrap/mixins/_clearfix.scss +22 -0
  79. data/assets/stylesheets/bootstrap/mixins/_forms.scss +84 -0
  80. data/assets/stylesheets/bootstrap/mixins/_gradients.scss +58 -0
  81. data/assets/stylesheets/bootstrap/mixins/_grid-framework.scss +81 -0
  82. data/assets/stylesheets/bootstrap/mixins/_grid.scss +122 -0
  83. data/assets/stylesheets/bootstrap/mixins/_hide-text.scss +21 -0
  84. data/assets/stylesheets/bootstrap/mixins/_image.scss +34 -0
  85. data/assets/stylesheets/bootstrap/mixins/_labels.scss +12 -0
  86. data/assets/stylesheets/bootstrap/mixins/_list-group.scss +31 -0
  87. data/assets/stylesheets/bootstrap/mixins/_nav-divider.scss +10 -0
  88. data/assets/stylesheets/bootstrap/mixins/_nav-vertical-align.scss +9 -0
  89. data/assets/stylesheets/bootstrap/mixins/_opacity.scss +8 -0
  90. data/assets/stylesheets/bootstrap/mixins/_pagination.scss +23 -0
  91. data/assets/stylesheets/bootstrap/mixins/_panels.scss +24 -0
  92. data/assets/stylesheets/bootstrap/mixins/_progress-bar.scss +10 -0
  93. data/assets/stylesheets/bootstrap/mixins/_reset-filter.scss +8 -0
  94. data/assets/stylesheets/bootstrap/mixins/_resize.scss +6 -0
  95. data/assets/stylesheets/bootstrap/mixins/_responsive-visibility.scss +21 -0
  96. data/assets/stylesheets/bootstrap/mixins/_size.scss +10 -0
  97. data/assets/stylesheets/bootstrap/mixins/_tab-focus.scss +9 -0
  98. data/assets/stylesheets/bootstrap/mixins/_table-row.scss +28 -0
  99. data/assets/stylesheets/bootstrap/mixins/_text-emphasis.scss +11 -0
  100. data/assets/stylesheets/bootstrap/mixins/_text-overflow.scss +8 -0
  101. data/assets/stylesheets/bootstrap/mixins/_vendor-prefixes.scss +219 -0
  102. data/bootstrap-sass-backport.gemspec +36 -0
  103. data/bower.json +50 -0
  104. data/composer.json +35 -0
  105. data/lib/bootstrap-sass-backport.rb +71 -0
  106. data/lib/bootstrap-sass/engine.rb +12 -0
  107. data/lib/bootstrap-sass/version.rb +4 -0
  108. data/package.json +30 -0
  109. data/sache.json +5 -0
  110. data/tasks/bower.rake +34 -0
  111. data/tasks/converter.rb +80 -0
  112. data/tasks/converter/char_string_scanner.rb +38 -0
  113. data/tasks/converter/fonts_conversion.rb +16 -0
  114. data/tasks/converter/js_conversion.rb +45 -0
  115. data/tasks/converter/less_conversion.rb +670 -0
  116. data/tasks/converter/logger.rb +57 -0
  117. data/tasks/converter/network.rb +86 -0
  118. data/templates/project/_bootstrap-variables.sass.erb +4 -0
  119. data/templates/project/manifest.rb +26 -0
  120. data/templates/project/styles.sass +3 -0
  121. data/test/compass_test.rb +9 -0
  122. data/test/compilation_test.rb +18 -0
  123. data/test/dummy_node_mincer/apple-touch-icon-144-precomposed.png +0 -0
  124. data/test/dummy_node_mincer/application.css.ejs.scss +6 -0
  125. data/test/dummy_node_mincer/manifest.js +87 -0
  126. data/test/dummy_rails/README.rdoc +3 -0
  127. data/test/dummy_rails/Rakefile +6 -0
  128. data/test/dummy_rails/app/assets/images/.keep +0 -0
  129. data/test/dummy_rails/app/assets/javascripts/application.js +2 -0
  130. data/test/dummy_rails/app/assets/stylesheets/application.css.sass +2 -0
  131. data/test/dummy_rails/app/controllers/application_controller.rb +5 -0
  132. data/test/dummy_rails/app/controllers/pages_controller.rb +4 -0
  133. data/test/dummy_rails/app/helpers/application_helper.rb +2 -0
  134. data/test/dummy_rails/app/views/layouts/application.html.erb +14 -0
  135. data/test/dummy_rails/app/views/pages/root.html.slim +41 -0
  136. data/test/dummy_rails/bin/bundle +3 -0
  137. data/test/dummy_rails/bin/rails +4 -0
  138. data/test/dummy_rails/bin/rake +4 -0
  139. data/test/dummy_rails/config.ru +4 -0
  140. data/test/dummy_rails/config/application.rb +30 -0
  141. data/test/dummy_rails/config/boot.rb +5 -0
  142. data/test/dummy_rails/config/environment.rb +5 -0
  143. data/test/dummy_rails/config/environments/development.rb +26 -0
  144. data/test/dummy_rails/config/environments/production.rb +76 -0
  145. data/test/dummy_rails/config/environments/test.rb +30 -0
  146. data/test/dummy_rails/config/initializers/backtrace_silencers.rb +7 -0
  147. data/test/dummy_rails/config/initializers/filter_parameter_logging.rb +4 -0
  148. data/test/dummy_rails/config/initializers/inflections.rb +16 -0
  149. data/test/dummy_rails/config/initializers/mime_types.rb +5 -0
  150. data/test/dummy_rails/config/initializers/secret_token.rb +18 -0
  151. data/test/dummy_rails/config/initializers/session_store.rb +3 -0
  152. data/test/dummy_rails/config/initializers/wrap_parameters.rb +14 -0
  153. data/test/dummy_rails/config/locales/en.yml +3 -0
  154. data/test/dummy_rails/config/locales/es.yml +3 -0
  155. data/test/dummy_rails/config/routes.rb +3 -0
  156. data/test/dummy_rails/db/test.sqlite3 +0 -0
  157. data/test/dummy_rails/lib/assets/.keep +0 -0
  158. data/test/dummy_rails/log/.keep +0 -0
  159. data/test/dummy_rails/public/404.html +58 -0
  160. data/test/dummy_rails/public/422.html +58 -0
  161. data/test/dummy_rails/public/500.html +57 -0
  162. data/test/dummy_rails/public/favicon.ico +0 -0
  163. data/test/dummy_sass_only/Gemfile +4 -0
  164. data/test/dummy_sass_only/compile.rb +13 -0
  165. data/test/dummy_sass_only/import_all.sass +2 -0
  166. data/test/gemfiles/sass_3_2.gemfile +6 -0
  167. data/test/gemfiles/sass_3_3.gemfile +6 -0
  168. data/test/gemfiles/sass_3_4.gemfile +7 -0
  169. data/test/gemfiles/sass_head.gemfile +6 -0
  170. data/test/node_mincer_test.rb +36 -0
  171. data/test/node_sass_test.rb +16 -0
  172. data/test/pages_test.rb +14 -0
  173. data/test/sass_test.rb +26 -0
  174. data/test/sprockets_rails_test.rb +27 -0
  175. data/test/support/dummy_rails_integration.rb +22 -0
  176. data/test/support/integration_test.rb +22 -0
  177. data/test/support/reporting.rb +17 -0
  178. data/test/test_helper.rb +35 -0
  179. data/test/test_helper_rails.rb +6 -0
  180. metadata +484 -0
@@ -0,0 +1,670 @@
1
+ require_relative 'char_string_scanner'
2
+
3
+ # This is the script used to automatically convert all of twbs/bootstrap LESS to Sass.
4
+ #
5
+ # Most differences are fixed by regexps and other forms of string substitution.
6
+ # There are Bootstrap-specific workarounds for the lack of parent selectors, recursion, mixin namespaces, extend within @media, etc in Sass 3.2.
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 will get vararg definitions in SCSS (not supported by LESS):
30
+ NESTED_MIXINS = {'#gradient' => 'gradient'}
31
+
32
+ # These mixins will get vararg definitions in SCSS (not supported by LESS):
33
+ VARARG_MIXINS = %w(
34
+ scale transition transition-duration transition-property transition-transform box-shadow
35
+ )
36
+
37
+ # Convert a snippet of bootstrap LESS to Scss
38
+ def convert_less(less)
39
+ load_shared
40
+ less = convert_to_scss(less)
41
+ less = yield(less) if block_given?
42
+ less
43
+ end
44
+
45
+ def load_shared
46
+ @shared_mixins ||= begin
47
+ log_status ' Reading shared mixins from mixins.less'
48
+ read_mixins read_files('less', bootstrap_less_files.grep(/mixins\//)).values.join("\n"), nested: NESTED_MIXINS
49
+ end
50
+ end
51
+
52
+ def process_stylesheet_assets
53
+ log_status 'Processing stylesheets...'
54
+ files = read_files('less', bootstrap_less_files)
55
+ save_to = @save_to[:scss]
56
+
57
+ log_status ' Converting LESS files to Scss:'
58
+ files.each do |name, file|
59
+ log_processing name
60
+ # apply common conversions
61
+ file = convert_less(file)
62
+ if name.start_with?('mixins/')
63
+ file = varargify_mixin_definitions(file, *VARARG_MIXINS)
64
+ %w(responsive-(in)?visibility input-size text-emphasis-variant bg-variant).each do |mixin|
65
+ file = parameterize_mixin_parent_selector file, mixin if file =~ /#{mixin}/
66
+ end
67
+ NESTED_MIXINS.each do |sel, name|
68
+ file = flatten_mixins(file, sel, name) if /#{Regexp.escape(sel)}/ =~ file
69
+ end
70
+ file = replace_all file, /(?<=[.-])\$state/, '#{$state}' if file =~ /[.-]\$state/
71
+ end
72
+ case name
73
+ when 'mixins/buttons.less'
74
+ file = replace_all file, /(\.dropdown-toggle)&/, '&\1'
75
+ when 'mixins/list-group.less'
76
+ file = replace_rules(file, ' .list-group-item-') { |rule| extract_nested_rule rule, 'a&' }
77
+ when 'mixins/gradients.less'
78
+ file = replace_ms_filters(file)
79
+ file = deinterpolate_vararg_mixins(file)
80
+ when 'mixins/vendor-prefixes.less'
81
+ # remove second scale mixins as this is handled via vararg in the first one
82
+ file = replace_rules(file, Regexp.escape('@mixin scale($ratioX, $ratioY...)')) { '' }
83
+ when 'mixins/grid-framework.less'
84
+ file = convert_grid_mixins file
85
+ when 'component-animations.less'
86
+ file = extract_nested_rule file, "#{SELECTOR_RE}&\\.in"
87
+ when 'responsive-utilities.less'
88
+ file = apply_mixin_parent_selector file, '\.(?:visible|hidden)'
89
+ when 'variables.less'
90
+ file = insert_default_vars(file)
91
+ file = unindent <<-SCSS + "\n" + file, 14
92
+ // When true, asset path helpers are used, otherwise the regular CSS `url()` is used.
93
+ // When there no function is defined, `fn('')` is parsed as string that equals the right hand side
94
+ // NB: in Sass 3.3 there is a native function: function-exists(twbs-font-path)
95
+ $bootstrap-sass-asset-helper: #{sass_fn_exists('twbs-font-path')} !default;
96
+ SCSS
97
+ file = replace_all file, %r{(\$icon-font-path): \s*"(.*)" (!default);}, "\n" + unindent(<<-SCSS, 14)
98
+ // [converter] Asset helpers such as Sprockets and Node.js Mincer do not resolve relative paths
99
+ \\1: if($bootstrap-sass-asset-helper, "bootstrap/", "\\2bootstrap/") \\3;
100
+ SCSS
101
+ when 'close.less'
102
+ # extract .close { button& {...} } rule
103
+ file = extract_nested_rule file, 'button&'
104
+ when 'dropdowns.less'
105
+ file = replace_all file, /@extend \.dropdown-menu-right;/, 'right: 0; left: auto;'
106
+ file = replace_all file, /@extend \.dropdown-menu-left;/, 'left: 0; right: auto;'
107
+ when 'forms.less'
108
+ file = extract_nested_rule file, 'textarea&'
109
+ file = apply_mixin_parent_selector(file, '\.input-(?:sm|lg)')
110
+ when 'navbar.less'
111
+ file = replace_all file, /(\s*)\.navbar-(right|left)\s*\{\s*@extend\s*\.pull-(right|left);\s*/, "\\1.navbar-\\2 {\\1 float: \\2 !important;\\1"
112
+ when 'tables.less'
113
+ file = replace_all file, /(@include\s*table-row-variant\()(\w+)/, "\\1'\\2'"
114
+ when 'thumbnails.less', 'labels.less', 'badges.less'
115
+ file = extract_nested_rule file, 'a&'
116
+ when 'glyphicons.less'
117
+ file = bootstrap_font_files.map { |p| %Q(//= depend_on "bootstrap/#{File.basename(p)}") } * "\n" + "\n" + file
118
+ file = replace_rules(file, '@font-face') { |rule|
119
+ rule = replace_all rule, /(\$icon-font(?:-\w+)+)/, '#{\1}'
120
+ replace_asset_url rule, :font
121
+ }
122
+ when 'type.less'
123
+ file = apply_mixin_parent_selector(file, '\.(text|bg)-(success|primary|info|warning|danger)')
124
+ # .bg-primary will not get patched automatically as it includes an additional rule. fudge for now
125
+ file = replace_all(file, " @include bg-variant($brand-primary);\n}", "}\n@include bg-variant('.bg-primary', $brand-primary);")
126
+ end
127
+
128
+ path = File.join save_to, name.sub(/\.less$/, '.scss')
129
+ path = File.join File.dirname(path), '_' + File.basename(path)
130
+ save_file(path, file)
131
+ log_processed File.basename(path)
132
+ end
133
+
134
+ # move bootstrap/_bootstrap.scss to _bootstrap.scss adjusting import paths
135
+ main_from = "#{save_to}/_bootstrap.scss"
136
+ main_to = File.expand_path("#{save_to}/../_bootstrap.scss")
137
+ save_file main_to, File.read(main_from).gsub(/ "/, ' "bootstrap/')
138
+ File.delete(main_from)
139
+ end
140
+
141
+ def bootstrap_less_files
142
+ @bootstrap_less_files ||= get_paths_by_type('less', /\.less$/) +
143
+ get_paths_by_type('mixins', /\.less$/,
144
+ get_tree(get_tree_sha('mixins', get_tree(get_tree_sha('less'))))).map { |p| "mixins/#{p}" }
145
+ end
146
+
147
+ # apply general less to scss conversion
148
+ def convert_to_scss(file)
149
+ # get local mixin names before converting the definitions
150
+ mixins = @shared_mixins + read_mixins(file)
151
+ file = replace_vars(file)
152
+ file = replace_mixin_definitions(file)
153
+ file = replace_mixins(file, mixins)
154
+ file = replace_spin(file)
155
+ file = replace_fadein(file)
156
+ file = replace_image_urls(file)
157
+ file = replace_escaping(file)
158
+ file = convert_less_ampersand(file)
159
+ file = deinterpolate_vararg_mixins(file)
160
+ file = replace_calculation_semantics(file)
161
+ file = replace_file_imports(file)
162
+ file
163
+ end
164
+
165
+ def sass_fn_exists(fn)
166
+ %Q{(#{fn}("") != unquote('#{fn}("")'))}
167
+ end
168
+
169
+ def replace_asset_url(rule, type)
170
+ replace_all rule, /url\((.*?)\)/, "url(if($bootstrap-sass-asset-helper, twbs-#{type}-path(\\1), \\1))"
171
+ end
172
+
173
+ # convert recursively evaluated selector $list to @for loop
174
+ def mixin_all_grid_columns(css, selector: raise('pass class'), from: 1, to: raise('pass to'))
175
+ mxn_def = css.each_line.first.strip
176
+ # inject local variables as default arguments
177
+ # this is to avoid overwriting outer variables with the same name with Sass <= 3.3
178
+ # see also: https://github.com/twbs/bootstrap-sass/issues/636
179
+ locals = <<-SASS.strip
180
+ $i: #{from}, $list: "#{selector}"
181
+ SASS
182
+ mxn_def.sub!(/(\(?)(\)\s*\{)/) { "#{$1}#{', ' if $1.empty?}#{locals}#{$2}" }
183
+ step_body = (css =~ /\$list \{\n(.*?)\n[ ]*\}/m) && $1
184
+ <<-SASS
185
+ // [converter] This is defined recursively in LESS, but Sass supports real loops
186
+ #{mxn_def}
187
+ @for $i from (#{from} + 1) through #{to} {
188
+ $list: "\#{$list}, #{selector}";
189
+ }
190
+ \#{$list} {
191
+ #{unindent step_body, 2}
192
+ }
193
+ }
194
+ SASS
195
+ end
196
+
197
+ # convert grid mixins LESS when => SASS @if
198
+ def convert_grid_mixins(file)
199
+ file = replace_rules file, /@mixin make-grid-columns/, comments: false do |css, pos|
200
+ mixin_all_grid_columns css, selector: '.col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}', to: '$grid-columns'
201
+ end
202
+ file = replace_rules file, /@mixin float-grid-columns/, comments: false do |css, pos|
203
+ mixin_all_grid_columns css, selector: '.col-#{$class}-#{$i}', to: '$grid-columns'
204
+ end
205
+ file = replace_rules file, /@mixin calc-grid-column/ do |css|
206
+ css = indent css.gsub(/.*when (.*?) {/, '@if \1 {').gsub(/(\$[\w-]+)\s+=\s+(\w+)/, '\1 == \2').gsub(/(?<=-)(\$[a-z]+)/, '#{\1}')
207
+ if css =~ /== width/
208
+ css = "@mixin calc-grid-column($index, $class, $type) {\n#{css}"
209
+ elsif css =~ /== offset/
210
+ css += "\n}"
211
+ end
212
+ css
213
+ end
214
+ file = replace_rules file, /@mixin loop-grid-columns/ do |css|
215
+ unindent <<-SASS, 8
216
+ // [converter] This is defined recursively in LESS, but Sass supports real loops
217
+ @mixin loop-grid-columns($columns, $class, $type) {
218
+ @for $i from 0 through $columns {
219
+ @include calc-grid-column($i, $class, $type);
220
+ }
221
+ }
222
+ SASS
223
+ end
224
+ file
225
+ end
226
+
227
+
228
+ # We need to keep a list of shared mixin names in order to convert the includes correctly
229
+ # Before doing any processing we read shared mixins from a file
230
+ # If a mixin is nested, it gets prefixed in the list (e.g. #gradient > .horizontal to 'gradient-horizontal')
231
+ def read_mixins(mixins_file, nested: {})
232
+ mixins = get_mixin_names(mixins_file, silent: true)
233
+ nested.each do |selector, prefix|
234
+ # we use replace_rules without replacing anything just to use the parsing algorithm
235
+ replace_rules(mixins_file, selector) { |rule|
236
+ mixins += get_mixin_names(unindent(unwrap_rule_block(rule)), silent: true).map { |name| "#{prefix}-#{name}" }
237
+ rule
238
+ }
239
+ end
240
+ mixins.uniq!
241
+ mixins.sort!
242
+ log_file_info "mixins: #{mixins * ', '}" unless mixins.empty?
243
+ mixins
244
+ end
245
+
246
+ def get_mixin_names(file, opts = {})
247
+ names = get_css_selectors(file).join("\n" * 2).scan(/^\.([\w-]+)\(#{LESS_MIXIN_DEF_ARGS_RE}\)(?: when.*?)?[ ]*\{/).map(&:first).uniq.sort
248
+ log_file_info "mixin defs: #{names * ', '}" unless opts[:silent] || names.empty?
249
+ names
250
+ end
251
+
252
+ # margin: a -b
253
+ # LESS: sets 2 values
254
+ # SASS: sets 1 value (a-b)
255
+ # This wraps a and -b so they evaluates to 2 values in SASS
256
+ def replace_calculation_semantics(file)
257
+ # split_prop_val.call('(@navbar-padding-vertical / 2) -@navbar-padding-horizontal')
258
+ # #=> ["(navbar-padding-vertical / 2)", "-navbar-padding-horizontal"]
259
+ split_prop_val = proc { |val|
260
+ s = CharStringScanner.new(val)
261
+ r = []
262
+ buff = ''
263
+ d = 0
264
+ prop_char = %r([\$\w\-/\*\+%!])
265
+ while (token = s.scan_next(/([\)\(]|\s+|#{prop_char}+)/))
266
+ buff << token
267
+ case token
268
+ when '('
269
+ d += 1
270
+ when ')'
271
+ d -= 1
272
+ if d == 0
273
+ r << buff
274
+ buff = ''
275
+ end
276
+ when /\s/
277
+ if d == 0 && !buff.strip.empty?
278
+ r << buff
279
+ buff = ''
280
+ end
281
+ end
282
+ end
283
+ r << buff unless buff.empty?
284
+ r.map(&:strip)
285
+ }
286
+
287
+ replace_rules file do |rule|
288
+ replace_properties rule do |props|
289
+ props.gsub /(?<!\w)([\w-]+):(.*?);/ do |m|
290
+ prop, vals = $1, split_prop_val.call($2)
291
+ next m unless vals.length >= 2 && vals.any? { |v| v =~ /^[\+\-]\$/ }
292
+ transformed = vals.map { |v| v.strip =~ %r(^\(.*\)$) ? v : "(#{v})" }
293
+ log_transform "property #{prop}: #{transformed * ' '}", from: 'wrap_calculation'
294
+ "#{prop}: #{transformed * ' '};"
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ # @import "file.less" to "#{target_path}file;"
301
+ def replace_file_imports(less, target_path = '')
302
+ less.gsub %r([@\$]import ["|']([\w\-/]+).less["|'];),
303
+ %Q(@import "#{target_path}\\1";)
304
+ end
305
+
306
+ def replace_all(file, regex, replacement = nil, &block)
307
+ log_transform regex, replacement
308
+ new_file = file.gsub(regex, replacement, &block)
309
+ raise "replace_all #{regex}, #{replacement} NO MATCH" if file == new_file
310
+ new_file
311
+ end
312
+
313
+ # @mixin a() { tr& { color:white } }
314
+ # to:
315
+ # @mixin a($parent) { tr#{$parent} { color: white } }
316
+ def parameterize_mixin_parent_selector(file, rule_sel)
317
+ log_transform rule_sel
318
+ param = '$parent'
319
+ replace_rules(file, '^\s*@mixin\s*' + rule_sel) do |mxn_css|
320
+ mxn_css.sub! /(?=@mixin)/, "// [converter] $parent hack\n"
321
+ # insert param into mixin def
322
+ mxn_css.sub!(/(@mixin [\w-]+)\(([\$\w\-,\s]*)\)/) { "#{$1}(#{param}#{', ' if $2 && !$2.empty?}#{$2})" }
323
+ # wrap properties in #{$parent} { ... }
324
+ replace_properties(mxn_css) { |props|
325
+ next props if props.strip.empty?
326
+ spacer = ' ' * indent_width(props)
327
+ "#{spacer}\#{#{param}} {\n#{indent(props.sub(/\s+\z/, ''), 2)}\n#{spacer}}"
328
+ }
329
+ # change nested& rules to nested#{$parent}
330
+ replace_rules(mxn_css, /.*&[ ,:]/) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
331
+ end
332
+ end
333
+
334
+ # extracts rule immediately after it's parent, and adjust the selector
335
+ # .x { textarea& { ... }}
336
+ # to:
337
+ # .x { ... }
338
+ # textarea.x { ... }
339
+ def extract_nested_rule(file, selector, new_selector = nil)
340
+ matches = []
341
+ # first find the rules, and remove them
342
+ file = replace_rules(file, "\s*#{selector}", comments: true) { |rule, pos, css|
343
+ new_sel = new_selector || "#{get_selector(rule).gsub(/&/, selector_for_pos(css, pos.begin))}"
344
+ matches << [rule, pos, new_sel]
345
+ indent "// [converter] extracted #{get_selector(rule)} to #{new_sel}".tr("\n", ' ').squeeze(' '), indent_width(rule)
346
+ }
347
+ raise "extract_nested_rule: no such selector: #{selector}" if matches.empty?
348
+ # replace rule selector with new_selector
349
+ matches.each do |m|
350
+ m[0].sub! /(#{COMMENT_RE}*)^(\s*).*?(\s*){/m, "\\1\\2#{m[2]}\\3{"
351
+ log_transform selector, m[2]
352
+ end
353
+ replace_substrings_at file,
354
+ matches.map { |_, pos| close_brace_pos(file, pos.begin, 1) + 1 },
355
+ matches.map { |rule, _| "\n\n" + unindent(rule) }
356
+ end
357
+
358
+ # .visible-sm { @include responsive-visibility() }
359
+ # to:
360
+ # @include responsive-visibility('.visible-sm')
361
+ def apply_mixin_parent_selector(file, rule_sel)
362
+ log_transform rule_sel
363
+ replace_rules file, '\s*' + rule_sel, comments: false do |rule, rule_pos, css|
364
+ body = unwrap_rule_block(rule.dup).strip
365
+ next rule unless body =~ /^@include \w+/m || body =~ /^@media/ && body =~ /\{\s*@include/
366
+ rule =~ /(#{COMMENT_RE}*)([#{SELECTOR_CHAR}\s*]+?)#{RULE_OPEN_BRACE_RE}/
367
+ cmt, sel = $1, $2.strip
368
+ # take one up selector chain if this is an &. selector
369
+ if sel.start_with?('&')
370
+ parent_sel = selector_for_pos(css, rule_pos.begin)
371
+ sel = parent_sel + sel[1..-1]
372
+ end
373
+ # unwrap, and replace @include
374
+ unindent unwrap_rule_block(rule).gsub(/(@include [\w-]+)\(([\$\w\-,\s]*)\)/) {
375
+ args = $2
376
+ "#{cmt}#{$1}('#{sel.gsub(/\s+/, ' ')}'#{', ' if args && !args.empty?}#{args})"
377
+ }
378
+ end
379
+ end
380
+
381
+ # #gradient > { @mixin horizontal ... }
382
+ # to:
383
+ # @mixin gradient-horizontal
384
+ def flatten_mixins(file, container, prefix)
385
+ log_transform container, prefix
386
+ replace_rules file, Regexp.escape(container) do |mixins_css|
387
+ unindent unwrap_rule_block(mixins_css).gsub(/@mixin\s*([\w-]+)/, "@mixin #{prefix}-\\1")
388
+ end
389
+ end
390
+
391
+ # @include and @extend from LESS:
392
+ # .mixin() -> @include mixin()
393
+ # #scope > .mixin() -> @include scope-mixin()
394
+ # &:extend(.mixin all) -> @include mixin()
395
+ def replace_mixins(less, mixin_names)
396
+ mixin_pattern = /(\s+)(([#|\.][\w-]+\s*>\s*)*)\.([\w-]+\(.*\))(?!\s\{)/
397
+
398
+ less = less.gsub(mixin_pattern) do |match|
399
+ matches = match.scan(mixin_pattern).flatten
400
+ scope = matches[1] || ''
401
+ if scope != ''
402
+ scope = scope.scan(/[\w-]+/).join('-') + '-'
403
+ end
404
+ mixin_name = match.scan(/\.([\w-]+)\(.*\)\s?\{?/).first
405
+ if mixin_name && mixin_names.include?("#{scope}#{mixin_name.first}")
406
+ "#{matches.first}@include #{scope}#{matches.last}".gsub(/; \$/, ", $").sub(/;\)$/, ')')
407
+ else
408
+ "#{matches.first}@extend .#{scope}#{matches.last.gsub(/\(\)/, '')}"
409
+ end
410
+ end
411
+
412
+ less.gsub /&:extend\((#{SELECTOR_RE})(?: all)?\)/ do
413
+ selector = $1
414
+ selector =~ /\.([\w-]+)/
415
+ mixin = $1
416
+ if mixin && mixin_names.include?(mixin)
417
+ "@include #{mixin}()"
418
+ else
419
+ "@extend #{selector}"
420
+ end
421
+ end
422
+ end
423
+
424
+ # change Microsoft filters to SASS calling convention
425
+ def replace_ms_filters(file)
426
+ log_transform
427
+ file.gsub(
428
+ /filter: e\(%\("progid:DXImageTransform.Microsoft.gradient\(startColorstr='%d', endColorstr='%d', GradientType=(\d)\)",argb\(([\-$\w]+)\),argb\(([\-$\w]+)\)\)\);/,
429
+ %Q(filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='\#{ie-hex-str(\\2)}', endColorstr='\#{ie-hex-str(\\3)}', GradientType=\\1);)
430
+ )
431
+ end
432
+
433
+ # unwraps topmost rule block
434
+ # #sel { a: b; }
435
+ # to:
436
+ # a: b;
437
+ def unwrap_rule_block(css)
438
+ css[(css =~ RULE_OPEN_BRACE_RE) + 1..-1].sub(/\n?}\s*\z/m, '')
439
+ end
440
+
441
+ def replace_mixin_definitions(less)
442
+ less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
443
+ "#{$1}@mixin #{$2.tr(';', ',')}#{$3}".sub(/,\)/, ')')
444
+ }
445
+ end
446
+
447
+ def replace_vars(less)
448
+ less = less.dup
449
+ # skip header comment
450
+ less =~ %r(\A/\*(.*?)\*/)m
451
+ from = $~ ? $~.to_s.length : 0
452
+ less[from..-1] = less[from..-1].
453
+ gsub(/(?!@mixin|@media|@page|@keyframes|@font-face|@-\w)@/, '$').
454
+ # variables that would be ignored by gsub above: e.g. @page-header-border-color
455
+ gsub(/@(page[\w-]+)/, '$\1')
456
+ less
457
+ end
458
+
459
+ def replace_spin(less)
460
+ less.gsub(/(?![\-$@.])spin(?!-)/, 'adjust-hue')
461
+ end
462
+
463
+ def replace_fadein(less)
464
+ less.gsub(/(?![\-$@.])fadein\((.*?),\s*(.*?)%\)/) { "fade_in(#{$1}, #{$2.to_i / 100.0})" }
465
+ end
466
+
467
+ def replace_image_urls(less)
468
+ less.gsub(/background-image: url\("?(.*?)"?\);/) { |s| replace_asset_url s, :image }
469
+ end
470
+
471
+ def replace_escaping(less)
472
+ less = less.gsub(/~"([^"]+)"/, '#{\1}') # Get rid of ~"" escape
473
+ less.gsub!(/\$\{([^}]+)\}/, '$\1') # Get rid of @{} escape
474
+ less.gsub!(/"([^"\n]*)(\$[\w\-]+)([^"\n]*)"/, '"\1#{\2}\3"') # interpolate variable in string, e.g. url("$file-1x") => url("#{$file-1x}")
475
+ less.gsub(/(\W)e\(%\("?([^"]*)"?\)\)/, '\1\2') # Get rid of e(%("")) escape
476
+ end
477
+
478
+ def insert_default_vars(scss)
479
+ log_transform
480
+ scss.gsub(/^(\$.+);/, '\1 !default;')
481
+ end
482
+
483
+ # Converts &-
484
+ def convert_less_ampersand(less)
485
+ regx = /^\.badge\s*\{[\s\/\w\(\)]+(&{1}-{1})\w.*?^}$/m
486
+
487
+ tmp = ''
488
+ less.scan(/^(\s*&)(-[\w\[\]]+\s*\{.+})$/) do |ampersand, css|
489
+ tmp << ".badge#{css}\n"
490
+ end
491
+
492
+ less.gsub(regx, tmp)
493
+ end
494
+
495
+ # unindent by n spaces
496
+ def unindent(txt, n = 2)
497
+ txt.gsub /^[ ]{#{n}}/, ''
498
+ end
499
+
500
+ # indent by n spaces
501
+ def indent(txt, n = 2)
502
+ spaces = ' ' * n
503
+ txt.gsub /^/, spaces
504
+ end
505
+
506
+ # get indent length from the first line of txt
507
+ def indent_width(txt)
508
+ txt.match(/\A\s*/).to_s.length
509
+ end
510
+
511
+ # @mixin transition($transition) {
512
+ # to:
513
+ # @mixin transition($transition...) {
514
+ def varargify_mixin_definitions(scss, *mixins)
515
+ scss = scss.dup
516
+ replaced = []
517
+ mixins.each do |mixin|
518
+ if scss.gsub! /(@mixin\s*#{Regexp.quote(mixin)})\((#{SCSS_MIXIN_DEF_ARGS_RE})\)/, '\1(\2...)'
519
+ replaced << mixin
520
+ end
521
+ end
522
+ log_transform *replaced unless replaced.empty?
523
+ scss
524
+ end
525
+
526
+ # @include transition(#{border-color ease-in-out .15s, box-shadow ease-in-out .15s})
527
+ # to
528
+ # @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s)
529
+ def deinterpolate_vararg_mixins(scss)
530
+ scss = scss.dup
531
+ VARARG_MIXINS.each do |mixin|
532
+ if scss.gsub! /(@include\s*#{Regexp.quote(mixin)})\(\s*\#\{([^}]+)\}\s*\)/, '\1(\2)'
533
+ log_transform mixin
534
+ end
535
+ end
536
+ scss
537
+ end
538
+
539
+ # get full selector for rule_block
540
+ def get_selector(rule_block)
541
+ sel = /^\s*(#{SELECTOR_RE}?)\s*\{/.match(rule_block) && $1 && $1.strip
542
+ sel.sub /\s*\{\n\s.*/m, ''
543
+ end
544
+
545
+ # replace CSS rule blocks matching rule_prefix with yield(rule_block, rule_pos)
546
+ # will also include immediately preceding comments in rule_block
547
+ #
548
+ # option :comments -- include immediately preceding comments in rule_block
549
+ #
550
+ # replace_rules(".a{ \n .b{} }", '.b') { |rule, pos| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
551
+ def replace_rules(less, rule_prefix = SELECTOR_RE, options = {}, &block)
552
+ options = {comments: true}.merge(options || {})
553
+ less = less.dup
554
+ s = CharStringScanner.new(less)
555
+ rule_re = /(?:#{rule_prefix}[#{SELECTOR_CHAR})=(\s]*?#{RULE_OPEN_BRACE_RE})/
556
+ if options[:comments]
557
+ rule_start_re = /(?:#{COMMENT_RE}*)^#{rule_re}/
558
+ else
559
+ rule_start_re = /^#{rule_re}/
560
+ end
561
+
562
+ positions = []
563
+ while (rule_start = s.scan_next(rule_start_re))
564
+ pos = s.pos
565
+ positions << (pos - rule_start.length..close_brace_pos(less, pos - 1))
566
+ end
567
+ replace_substrings_at(less, positions, &block)
568
+ less
569
+ end
570
+
571
+ # Get a all top-level selectors (with {)
572
+ def get_css_selectors(css, opts = {})
573
+ s = CharStringScanner.new(css)
574
+ selectors = []
575
+ while s.scan_next(RULE_OPEN_BRACE_RE)
576
+ brace_pos = s.pos
577
+ def_pos = css_def_pos(css, brace_pos+1, -1)
578
+ sel = css[def_pos.begin..brace_pos - 1].dup
579
+ sel.strip! if opts[:strip]
580
+ selectors << sel
581
+ sel.dup.strip
582
+ s.pos = close_brace_pos(css, brace_pos, 1) + 1
583
+ end
584
+ selectors
585
+ end
586
+
587
+ # replace in the top-level selector
588
+ # replace_in_selector('a {a: {a: a} } a {}', /a/, 'b') => 'b {a: {a: a} } b {}'
589
+ def replace_in_selector(css, pattern, sub)
590
+ # scan for selector positions in css
591
+ s = CharStringScanner.new(css)
592
+ prev_pos = 0
593
+ sel_pos = []
594
+ while (brace = s.scan_next(RULE_OPEN_BRACE_RE))
595
+ pos = s.pos
596
+ sel_pos << (prev_pos .. pos - 1)
597
+ s.pos = close_brace_pos(css, s.pos - 1) + 1
598
+ prev_pos = pos
599
+ end
600
+ replace_substrings_at(css, sel_pos) { |s| s.gsub(pattern, sub) }
601
+ end
602
+
603
+
604
+ # replace first level properties in the css with yields
605
+ # replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
606
+ def replace_properties(css, &block)
607
+ s = CharStringScanner.new(css)
608
+ s.skip_until /#{RULE_OPEN_BRACE_RE}\n?/
609
+ from = s.pos
610
+ m = s.scan_next(/\s*#{SELECTOR_RE}#{RULE_OPEN_BRACE_RE}/) || s.scan_next(/\s*#{RULE_CLOSE_BRACE_RE}/)
611
+ to = s.pos - m.length - 1
612
+ replace_substrings_at css, [(from .. to)], &block
613
+ end
614
+
615
+
616
+ # immediate selector of css at pos
617
+ def selector_for_pos(css, pos, depth = -1)
618
+ css[css_def_pos(css, pos, depth)].dup.strip
619
+ end
620
+
621
+ # get the pos of css def at pos (search backwards)
622
+ def css_def_pos(css, pos, depth = -1)
623
+ to = open_brace_pos(css, pos, depth)
624
+ prev_def = to - (css[0..to].reverse.index(RULE_CLOSE_BRACE_RE_REVERSE) || to) + 1
625
+ from = prev_def + 1 + (css[prev_def + 1..-1] =~ %r(^\s*[^\s/]))
626
+ (from..to - 1)
627
+ end
628
+
629
+ # next matching brace for brace at from
630
+ def close_brace_pos(css, from, depth = 0)
631
+ s = CharStringScanner.new(css[from..-1])
632
+ while (b = s.scan_next(BRACE_RE))
633
+ depth += (b == '}' ? -1 : +1)
634
+ break if depth.zero?
635
+ end
636
+ raise "match not found for {" unless depth.zero?
637
+ from + s.pos - 1
638
+ end
639
+
640
+ # opening brace position from +from+ (search backwards)
641
+ def open_brace_pos(css, from, depth = 0)
642
+ s = CharStringScanner.new(css[0..from].reverse)
643
+ while (b = s.scan_next(BRACE_RE_REVERSE))
644
+ depth += (b == '{' ? +1 : -1)
645
+ break if depth.zero?
646
+ end
647
+ raise "matching { brace not found" unless depth.zero?
648
+ from - s.pos + 1
649
+ end
650
+
651
+ # insert substitutions into text at positions (Range or Fixnum)
652
+ # substitutions can be passed as array or as yields from the &block called with |substring, position, text|
653
+ # position is a range (begin..end)
654
+ def replace_substrings_at(text, positions, replacements = nil, &block)
655
+ offset = 0
656
+ positions.each_with_index do |p, i|
657
+ p = (p...p) if p.is_a?(Fixnum)
658
+ from = p.begin + offset
659
+ to = p.end + offset
660
+ p = p.exclude_end? ? (from...to) : (from..to)
661
+ # block returns the substitution, e.g.: { |text, pos| text[pos].upcase }
662
+ r = replacements ? replacements[i] : block.call(text[p], p, text)
663
+ text[p] = r
664
+ # add the change in length to offset
665
+ offset += r.size - (p.end - p.begin + (p.exclude_end? ? 0 : 1))
666
+ end
667
+ text
668
+ end
669
+ end
670
+ end