bootstrap-sass-backport 3.2.0.2

Sign up to get free protection for your applications and to get access to all the features.
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